@mohasinac/appkit 4.29.0 → 4.30.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/dist/_internal/client/features/layout/SidebarCollapseToggle.js +1 -1
- package/dist/_internal/server/jobs/handlers/_helpers.d.ts +22 -0
- package/dist/_internal/server/jobs/handlers/_helpers.js +25 -1
- package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
- package/dist/_internal/shared/features/blog/schema.js +2 -1
- package/dist/_internal/shared/features/brands/schema.d.ts +2 -2
- package/dist/_internal/shared/features/brands/schema.js +3 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +3 -3
- package/dist/_internal/shared/features/categories/bundle-schemas.js +2 -1
- package/dist/_internal/shared/features/events/schema.d.ts +2 -2
- package/dist/_internal/shared/features/events/schema.js +2 -1
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/schema.js +3 -2
- package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
- package/dist/_internal/shared/features/reviews/schema.js +2 -1
- package/dist/_internal/shared/types/action-result.d.ts +0 -2
- package/dist/client/api/surface-error.d.ts +9 -0
- package/dist/client/api/surface-error.js +29 -3
- package/dist/client/api/useApiMutation.d.ts +10 -0
- package/dist/client/api/useApiMutation.js +2 -1
- package/dist/core/server-action.d.ts +3 -14
- package/dist/core/server-action.js +11 -2
- package/dist/errors/error-envelope.d.ts +73 -0
- package/dist/errors/error-envelope.js +96 -0
- package/dist/errors/error-handler.js +14 -7
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/components/UserSidebar.js +2 -2
- package/dist/features/account/components/UserSupportView.js +2 -6
- package/dist/features/account/hooks/useProfile.d.ts +6 -0
- package/dist/features/account/hooks/useProfile.js +1 -0
- package/dist/features/admin/components/AdminAdEditorView.js +1 -3
- package/dist/features/admin/components/AdminAddressClustersView.js +1 -3
- package/dist/features/admin/components/AdminAddressEditorView.js +2 -4
- package/dist/features/admin/components/AdminAddressesView.js +1 -3
- package/dist/features/admin/components/AdminAllEventEntriesView.js +1 -3
- package/dist/features/admin/components/AdminBidsView.js +1 -3
- package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
- package/dist/features/admin/components/AdminBrandEditorView.js +4 -5
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +1 -3
- package/dist/features/admin/components/AdminCategoryEditorView.js +2 -4
- package/dist/features/admin/components/AdminContactEditorView.js +1 -3
- package/dist/features/admin/components/AdminContactView.js +2 -6
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -4
- package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -9
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -4
- package/dist/features/admin/components/AdminFeatureEditorView.js +0 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +2 -4
- package/dist/features/admin/components/AdminNavEditorView.js +1 -3
- package/dist/features/admin/components/AdminNavigationView.js +3 -3
- package/dist/features/admin/components/AdminNewsletterView.js +1 -3
- package/dist/features/admin/components/AdminNotificationsView.js +2 -6
- package/dist/features/admin/components/AdminOffersView.js +1 -3
- package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
- package/dist/features/admin/components/AdminPaymentClustersView.js +1 -3
- package/dist/features/admin/components/AdminPaymentMethodsView.js +1 -3
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +2 -4
- package/dist/features/admin/components/AdminProductEditorView.js +2 -4
- package/dist/features/admin/components/AdminReturnRequestsView.js +2 -6
- package/dist/features/admin/components/AdminReviewsView.js +2 -6
- package/dist/features/admin/components/AdminScammerEditorView.js +2 -6
- package/dist/features/admin/components/AdminSectionsView.js +2 -6
- package/dist/features/admin/components/AdminSessionsView.js +1 -3
- package/dist/features/admin/components/AdminSidebar.js +2 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +1 -3
- package/dist/features/admin/components/AdminStoresView.js +2 -6
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -6
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +7 -21
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -4
- package/dist/features/admin/components/AdminUserEditorView.js +6 -18
- package/dist/features/admin/components/AdminUsersView.js +2 -6
- package/dist/features/admin/components/BrandQuickCreateForm.js +2 -4
- package/dist/features/admin/components/CategoryQuickCreateForm.js +2 -4
- package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
- package/dist/features/auth/components/ForgotPasswordView.js +1 -1
- package/dist/features/auth/components/LoginForm.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +1 -1
- package/dist/features/auth/components/ResetPasswordView.js +1 -1
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/repository/session.repository.js +13 -1
- package/dist/features/blog/actions/blog-actions.d.ts +14 -14
- package/dist/features/blog/actions/blog-actions.js +6 -8
- package/dist/features/blog/schemas/index.d.ts +1 -1
- package/dist/features/blog/schemas/index.js +2 -4
- package/dist/features/categories/api/[id]/route.js +2 -1
- package/dist/features/categories/api/route.js +2 -1
- package/dist/features/events/schemas/index.d.ts +1 -1
- package/dist/features/events/schemas/index.js +2 -4
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/filters/AsyncFacetSection.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +1 -1
- package/dist/features/filters/RangeFilter.js +1 -1
- package/dist/features/filters/SwitchFilter.js +1 -1
- package/dist/features/homepage/api/carousel/[id]/route.js +4 -3
- package/dist/features/homepage/api/carousel/route.js +4 -3
- package/dist/features/homepage/api/route.js +3 -2
- package/dist/features/homepage/components/GoogleReviewsSection.js +1 -1
- package/dist/features/homepage/components/HeroBanner.js +1 -1
- package/dist/features/homepage/components/HeroCarousel.js +1 -1
- package/dist/features/homepage/components/SocialPostCard.js +1 -1
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +4 -4
- package/dist/features/layout/BottomNavbar.js +2 -2
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +24 -12
- package/dist/features/media/AvatarUpload.js +28 -10
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/modals/ImageEditor.js +1 -1
- package/dist/features/products/components/MakeOfferButton.js +1 -1
- package/dist/features/reviews/api/[id]/route.js +3 -2
- package/dist/features/reviews/api/route.js +3 -2
- package/dist/features/search/components/Search.js +2 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +3 -3
- package/dist/features/seller/components/SellerSidebar.js +2 -2
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +1 -1
- package/dist/features/stores/repository/store.repository.d.ts +21 -0
- package/dist/features/stores/repository/store.repository.js +32 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -1
- package/dist/next/api/api-response.d.ts +29 -12
- package/dist/next/api/api-response.js +9 -8
- package/dist/next/api/apiHandler.d.ts +6 -1
- package/dist/next/api/apiHandler.js +13 -1
- package/dist/next/api/routeHandler.js +6 -6
- package/dist/next/middleware/pii-scrubber.js +5 -2
- package/dist/providers/db-firebase/base.repository.d.ts +14 -0
- package/dist/providers/db-firebase/base.repository.js +22 -0
- package/dist/providers/db-firebase/sieve.d.ts +14 -1
- package/dist/providers/db-firebase/sieve.js +3 -1
- package/dist/security/pii-encrypt.d.ts +0 -4
- package/dist/security/pii-encrypt.js +79 -13
- package/dist/security/pii-mask.d.ts +39 -1
- package/dist/security/pii-mask.js +33 -4
- package/dist/security/pii-redact.d.ts +0 -13
- package/dist/security/pii-redact.js +3 -1
- package/dist/security/pii-schemas.d.ts +0 -2
- package/dist/security/pii-schemas.js +16 -2
- package/dist/security/settings-encryption.js +31 -3
- package/dist/seed/stores-seed-data.js +18 -0
- package/dist/seed/test-utils.js +6 -2
- package/dist/ui/DataTable.js +2 -2
- package/dist/ui/components/CollapsibleSection.js +1 -1
- package/dist/ui/components/ImageGallery.js +1 -1
- package/dist/ui/components/ListingLayout.js +1 -1
- package/dist/ui/forms/FormShell.d.ts +0 -19
- package/dist/ui/forms/FormShell.js +4 -23
- package/dist/ui/forms/apply-zod-issues.d.ts +34 -0
- package/dist/ui/forms/apply-zod-issues.js +38 -0
- package/dist/ui/forms/index.d.ts +2 -1
- package/dist/ui/forms/index.js +2 -1
- package/dist/ui/icons/icon-registry.d.ts +8 -0
- package/dist/ui/icons/icon-registry.js +13 -1
- package/dist/utils/action-response.d.ts +1 -1
- package/dist/utils/media-url.d.ts +49 -0
- package/dist/utils/media-url.js +72 -1
- package/dist/validation/schemas.d.ts +15 -2
- package/dist/validation/schemas.js +18 -22
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +10 -2
|
@@ -14,5 +14,5 @@ export function SidebarCollapseToggle({ expanded, onToggle, expandedLabel = "Col
|
|
|
14
14
|
// Chevron rotation direction mirrors along with the dock side so it keeps
|
|
15
15
|
// pointing "into" the collapsed panel regardless of which edge it's on.
|
|
16
16
|
const chevronRotated = edge === "left" ? !expanded : expanded;
|
|
17
|
-
return (_jsx(IconButton, { type: "button", onClick: onToggle, "aria-label": expanded ? expandedLabel : collapsedLabel, "aria-expanded": expanded, variant: "ghost", style: HANDLE_STYLE, className: `w-9 shrink-0 ${roundedClass} shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110 active:scale-[0.96]`, icon: _jsx("svg", { className: `w-4 h-4 drop-shadow-sm transition-transform duration-300 ${chevronRotated ? "rotate-180" : ""}`, style: CHEVRON_STYLE, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }));
|
|
17
|
+
return (_jsx(IconButton, { type: "button", onClick: onToggle, "aria-label": expanded ? expandedLabel : collapsedLabel, "aria-expanded": expanded, variant: "ghost", style: HANDLE_STYLE, className: `w-9 shrink-0 ${roundedClass} shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110 active:scale-[0.96]`, icon: _jsx("svg", { className: `w-4 h-4 drop-shadow-sm transition-transform duration-300 ${chevronRotated ? "rotate-180" : ""} shrink-0`, style: CHEVRON_STYLE, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }));
|
|
18
18
|
}
|
|
@@ -10,5 +10,27 @@ export { getAdminRealtimeDb };
|
|
|
10
10
|
* Query docs tagged isTestData:true in a collection. `cutoff: null` returns every
|
|
11
11
|
* tester-sandbox doc regardless of expiry (manual force-purge); a real Date returns
|
|
12
12
|
* only the ones past testDataExpiresAt (the scheduled 7-day sweep).
|
|
13
|
+
*
|
|
14
|
+
* ## 🛑 `isTestData` without `testDataExpiresAt` means PERMANENT, never purge
|
|
15
|
+
*
|
|
16
|
+
* The flag has two uses, and only one of them is a sandbox fixture:
|
|
17
|
+
*
|
|
18
|
+
* · a fixture — `isTestData: true` PLUS `testDataExpiresAt`. Disposable,
|
|
19
|
+
* swept on the 7-day cycle. Every doc in `features/tester/seed-data/` is
|
|
20
|
+
* this shape; the invariant was checked across all six files.
|
|
21
|
+
* · permanently hidden — `isTestData: true` and NO expiry. Real, permanent
|
|
22
|
+
* content that simply must not reach the public or the sitemap.
|
|
23
|
+
* `store-tester-qa-seller` is the case that forced this distinction.
|
|
24
|
+
*
|
|
25
|
+
* The scheduled path already separates them: `testDataExpiresAt <= cutoff`
|
|
26
|
+
* excludes docs lacking the field. The FORCE path did not — `cutoff: null`
|
|
27
|
+
* drops that clause and returned everything, so `tester:purge-sandbox --force`
|
|
28
|
+
* would have deleted a permanent seed store. Hence the filter below.
|
|
29
|
+
*
|
|
30
|
+
* Filtered in memory rather than as a query clause: a Firestore inequality on
|
|
31
|
+
* `testDataExpiresAt` would exclude every doc missing it, which is the trap
|
|
32
|
+
* documented in CLAUDE.md's anti-pattern table — correct here by luck, wrong
|
|
33
|
+
* the moment someone reuses the shape. In-memory keeps the intent legible, and
|
|
34
|
+
* the read is already bounded at 200.
|
|
13
35
|
*/
|
|
14
36
|
export declare function getTestDataRefs(db: FirebaseFirestore.Firestore, collection: string, cutoff: Date | null): Promise<FirebaseFirestore.DocumentReference[]>;
|
|
@@ -22,6 +22,28 @@ export { getAdminRealtimeDb };
|
|
|
22
22
|
* Query docs tagged isTestData:true in a collection. `cutoff: null` returns every
|
|
23
23
|
* tester-sandbox doc regardless of expiry (manual force-purge); a real Date returns
|
|
24
24
|
* only the ones past testDataExpiresAt (the scheduled 7-day sweep).
|
|
25
|
+
*
|
|
26
|
+
* ## 🛑 `isTestData` without `testDataExpiresAt` means PERMANENT, never purge
|
|
27
|
+
*
|
|
28
|
+
* The flag has two uses, and only one of them is a sandbox fixture:
|
|
29
|
+
*
|
|
30
|
+
* · a fixture — `isTestData: true` PLUS `testDataExpiresAt`. Disposable,
|
|
31
|
+
* swept on the 7-day cycle. Every doc in `features/tester/seed-data/` is
|
|
32
|
+
* this shape; the invariant was checked across all six files.
|
|
33
|
+
* · permanently hidden — `isTestData: true` and NO expiry. Real, permanent
|
|
34
|
+
* content that simply must not reach the public or the sitemap.
|
|
35
|
+
* `store-tester-qa-seller` is the case that forced this distinction.
|
|
36
|
+
*
|
|
37
|
+
* The scheduled path already separates them: `testDataExpiresAt <= cutoff`
|
|
38
|
+
* excludes docs lacking the field. The FORCE path did not — `cutoff: null`
|
|
39
|
+
* drops that clause and returned everything, so `tester:purge-sandbox --force`
|
|
40
|
+
* would have deleted a permanent seed store. Hence the filter below.
|
|
41
|
+
*
|
|
42
|
+
* Filtered in memory rather than as a query clause: a Firestore inequality on
|
|
43
|
+
* `testDataExpiresAt` would exclude every doc missing it, which is the trap
|
|
44
|
+
* documented in CLAUDE.md's anti-pattern table — correct here by luck, wrong
|
|
45
|
+
* the moment someone reuses the shape. In-memory keeps the intent legible, and
|
|
46
|
+
* the read is already bounded at 200.
|
|
25
47
|
*/
|
|
26
48
|
export async function getTestDataRefs(db, collection, cutoff) {
|
|
27
49
|
let q = db
|
|
@@ -30,5 +52,7 @@ export async function getTestDataRefs(db, collection, cutoff) {
|
|
|
30
52
|
if (cutoff)
|
|
31
53
|
q = q.where("testDataExpiresAt", "<=", cutoff);
|
|
32
54
|
const snap = await q.limit(200).get();
|
|
33
|
-
return snap.docs
|
|
55
|
+
return snap.docs
|
|
56
|
+
.filter((d) => cutoff !== null || d.get("testDataExpiresAt") != null)
|
|
57
|
+
.map((d) => d.ref);
|
|
34
58
|
}
|
|
@@ -4,7 +4,7 @@ export declare const blogPostInputSchema: z.ZodObject<{
|
|
|
4
4
|
slug: z.ZodString;
|
|
5
5
|
excerpt: z.ZodOptional<z.ZodString>;
|
|
6
6
|
content: z.ZodString;
|
|
7
|
-
coverImage: z.ZodOptional<z.ZodString
|
|
7
|
+
coverImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
8
8
|
youtubeId: z.ZodOptional<z.ZodString>;
|
|
9
9
|
category: z.ZodString;
|
|
10
10
|
tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -44,7 +44,7 @@ export declare const blogPostUpdateSchema: z.ZodObject<Omit<{
|
|
|
44
44
|
slug: z.ZodOptional<z.ZodString>;
|
|
45
45
|
excerpt: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
46
46
|
content: z.ZodOptional<z.ZodString>;
|
|
47
|
-
coverImage: z.ZodOptional<z.ZodOptional<z.ZodString
|
|
47
|
+
coverImage: z.ZodOptional<z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>>;
|
|
48
48
|
youtubeId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
49
49
|
category: z.ZodOptional<z.ZodString>;
|
|
50
50
|
tags: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, "many">>>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { BLOG_TITLE_MAX_LENGTH, BLOG_EXCERPT_MAX_LENGTH } from "./config";
|
|
3
|
+
import { mediaUrlSchema } from "../../../../validation/schemas";
|
|
3
4
|
export const blogPostInputSchema = z.object({
|
|
4
5
|
title: z.string().min(3).max(BLOG_TITLE_MAX_LENGTH),
|
|
5
6
|
slug: z.string().min(1).regex(/^blog-[a-z0-9-]+$/, "Slug must start with 'blog-'"),
|
|
6
7
|
excerpt: z.string().max(BLOG_EXCERPT_MAX_LENGTH).optional(),
|
|
7
8
|
content: z.string().max(100000),
|
|
8
|
-
coverImage:
|
|
9
|
+
coverImage: mediaUrlSchema.optional(),
|
|
9
10
|
youtubeId: z.string().optional(),
|
|
10
11
|
category: z.string().min(1),
|
|
11
12
|
tags: z.array(z.string()).max(10).default([]),
|
|
@@ -3,7 +3,7 @@ export declare const brandInputSchema: z.ZodObject<{
|
|
|
3
3
|
name: z.ZodString;
|
|
4
4
|
slug: z.ZodString;
|
|
5
5
|
description: z.ZodOptional<z.ZodString>;
|
|
6
|
-
logoURL: z.ZodOptional<z.ZodString
|
|
6
|
+
logoURL: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
7
7
|
website: z.ZodOptional<z.ZodString>;
|
|
8
8
|
country: z.ZodOptional<z.ZodString>;
|
|
9
9
|
founded: z.ZodOptional<z.ZodNumber>;
|
|
@@ -34,7 +34,7 @@ export declare const brandUpdateSchema: z.ZodObject<Omit<{
|
|
|
34
34
|
name: z.ZodOptional<z.ZodString>;
|
|
35
35
|
slug: z.ZodOptional<z.ZodString>;
|
|
36
36
|
description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
37
|
-
logoURL: z.ZodOptional<z.ZodOptional<z.ZodString
|
|
37
|
+
logoURL: z.ZodOptional<z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>>;
|
|
38
38
|
website: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
39
39
|
country: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
40
40
|
founded: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { BRAND_NAME_MAX_LENGTH, BRAND_DESCRIPTION_MAX_LENGTH } from "./config";
|
|
3
|
+
import { mediaUrlSchema } from "../../../../validation/schemas";
|
|
3
4
|
export const brandInputSchema = z.object({
|
|
4
5
|
name: z.string().min(1).max(BRAND_NAME_MAX_LENGTH),
|
|
5
6
|
slug: z.string().min(1).regex(/^brand-[a-z0-9-]+$/, "Slug must start with 'brand-' and contain only lowercase letters, numbers, and hyphens"),
|
|
6
7
|
description: z.string().max(BRAND_DESCRIPTION_MAX_LENGTH).optional(),
|
|
7
|
-
logoURL:
|
|
8
|
+
logoURL: mediaUrlSchema.optional(),
|
|
9
|
+
// `website` is a genuine EXTERNAL link, not stored media — stays `.url()`.
|
|
8
10
|
website: z.string().url().optional(),
|
|
9
11
|
country: z.string().optional(),
|
|
10
12
|
founded: z.number().int().min(1800).max(new Date().getFullYear()).optional(),
|
|
@@ -60,7 +60,7 @@ export declare const bundleQueryRuleSchema: z.ZodDiscriminatedUnion<"type", [z.Z
|
|
|
60
60
|
orderBy?: "price-asc" | "price-desc" | "createdAt-desc" | undefined;
|
|
61
61
|
}>]>;
|
|
62
62
|
export declare const bundleDisplaySchema: z.ZodObject<{
|
|
63
|
-
coverImage: z.ZodOptional<z.ZodString
|
|
63
|
+
coverImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
64
64
|
icon: z.ZodOptional<z.ZodString>;
|
|
65
65
|
color: z.ZodOptional<z.ZodString>;
|
|
66
66
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -148,7 +148,7 @@ export declare const bundleCreateSchema: z.ZodObject<{
|
|
|
148
148
|
drawCount?: number | undefined;
|
|
149
149
|
}>, "many">>;
|
|
150
150
|
display: z.ZodOptional<z.ZodObject<{
|
|
151
|
-
coverImage: z.ZodOptional<z.ZodString
|
|
151
|
+
coverImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
152
152
|
icon: z.ZodOptional<z.ZodString>;
|
|
153
153
|
color: z.ZodOptional<z.ZodString>;
|
|
154
154
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -295,7 +295,7 @@ export declare const bundleUpdateSchema: z.ZodObject<{
|
|
|
295
295
|
drawCount?: number | undefined;
|
|
296
296
|
}>, "many">>;
|
|
297
297
|
display: z.ZodOptional<z.ZodObject<{
|
|
298
|
-
coverImage: z.ZodOptional<z.ZodString
|
|
298
|
+
coverImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
299
299
|
icon: z.ZodOptional<z.ZodString>;
|
|
300
300
|
color: z.ZodOptional<z.ZodString>;
|
|
301
301
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { BUNDLE_MAX_ITEMS, BUNDLE_DRAW_COUNT_MAX, BUNDLE_KIND_SPECIAL, BUNDLE_KIND_BRAND, } from "./bundle-config";
|
|
12
|
+
import { mediaUrlSchema } from "../../../../validation/schemas";
|
|
12
13
|
const productIdsSchema = z.array(z.string().min(1)).min(0).max(BUNDLE_MAX_ITEMS);
|
|
13
14
|
/**
|
|
14
15
|
* Per-member detail — mirrors BundleItemDetail in the Firestore schema.
|
|
@@ -39,7 +40,7 @@ export const bundleQueryRuleSchema = z.discriminatedUnion("type", [
|
|
|
39
40
|
}),
|
|
40
41
|
]);
|
|
41
42
|
export const bundleDisplaySchema = z.object({
|
|
42
|
-
coverImage:
|
|
43
|
+
coverImage: mediaUrlSchema.optional(),
|
|
43
44
|
icon: z.string().optional(),
|
|
44
45
|
color: z.string().optional(),
|
|
45
46
|
});
|
|
@@ -8,7 +8,7 @@ export declare const eventInputSchema: z.ZodObject<{
|
|
|
8
8
|
startsAt: z.ZodString;
|
|
9
9
|
endsAt: z.ZodString;
|
|
10
10
|
maxEntries: z.ZodOptional<z.ZodNumber>;
|
|
11
|
-
imageUrl: z.ZodOptional<z.ZodString
|
|
11
|
+
imageUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
12
12
|
location: z.ZodOptional<z.ZodString>;
|
|
13
13
|
isOnline: z.ZodDefault<z.ZodBoolean>;
|
|
14
14
|
entryFee: z.ZodDefault<z.ZodNumber>;
|
|
@@ -48,7 +48,7 @@ export declare const eventUpdateSchema: z.ZodObject<Omit<{
|
|
|
48
48
|
startsAt: z.ZodOptional<z.ZodString>;
|
|
49
49
|
endsAt: z.ZodOptional<z.ZodString>;
|
|
50
50
|
maxEntries: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
51
|
-
imageUrl: z.ZodOptional<z.ZodOptional<z.ZodString
|
|
51
|
+
imageUrl: z.ZodOptional<z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>>;
|
|
52
52
|
location: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
53
53
|
isOnline: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
54
54
|
entryFee: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { EVENT_TITLE_MAX_LENGTH, EVENT_DESCRIPTION_MAX_LENGTH } from "./config";
|
|
3
|
+
import { mediaUrlSchema } from "../../../../validation/schemas";
|
|
3
4
|
export const eventInputSchema = z.object({
|
|
4
5
|
title: z.string().min(3).max(EVENT_TITLE_MAX_LENGTH),
|
|
5
6
|
slug: z.string().min(1).regex(/^event-[a-z0-9-]+$/, "Slug must start with 'event-'"),
|
|
@@ -9,7 +10,7 @@ export const eventInputSchema = z.object({
|
|
|
9
10
|
startsAt: z.string().datetime({ offset: true }),
|
|
10
11
|
endsAt: z.string().datetime({ offset: true }),
|
|
11
12
|
maxEntries: z.number().int().min(1).optional(),
|
|
12
|
-
imageUrl:
|
|
13
|
+
imageUrl: mediaUrlSchema.optional(),
|
|
13
14
|
location: z.string().max(300).optional(),
|
|
14
15
|
isOnline: z.boolean().default(false),
|
|
15
16
|
entryFee: z.number().min(0).default(0),
|
|
@@ -8,8 +8,8 @@ export declare const productInputSchema: z.ZodObject<{
|
|
|
8
8
|
currency: z.ZodDefault<z.ZodString>;
|
|
9
9
|
stockQuantity: z.ZodDefault<z.ZodNumber>;
|
|
10
10
|
availableQuantity: z.ZodDefault<z.ZodNumber>;
|
|
11
|
-
mainImage: z.ZodString
|
|
12
|
-
images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
11
|
+
mainImage: z.ZodEffects<z.ZodString, string, string>;
|
|
12
|
+
images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
13
13
|
condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
|
|
14
14
|
tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
15
15
|
seoTitle: z.ZodOptional<z.ZodString>;
|
|
@@ -81,8 +81,8 @@ export declare const productUpdateSchema: z.ZodObject<{
|
|
|
81
81
|
currency: z.ZodOptional<z.ZodDefault<z.ZodString>>;
|
|
82
82
|
stockQuantity: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
83
83
|
availableQuantity: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
84
|
-
mainImage: z.ZodOptional<z.ZodString
|
|
85
|
-
images: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, "many">>>;
|
|
84
|
+
mainImage: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
85
|
+
images: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>>;
|
|
86
86
|
condition: z.ZodOptional<z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>>;
|
|
87
87
|
tags: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString, "many">>>;
|
|
88
88
|
seoTitle: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
@@ -154,8 +154,8 @@ export declare const auctionInputSchema: z.ZodObject<{
|
|
|
154
154
|
currency: z.ZodDefault<z.ZodString>;
|
|
155
155
|
stockQuantity: z.ZodDefault<z.ZodNumber>;
|
|
156
156
|
availableQuantity: z.ZodDefault<z.ZodNumber>;
|
|
157
|
-
mainImage: z.ZodString
|
|
158
|
-
images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
157
|
+
mainImage: z.ZodEffects<z.ZodString, string, string>;
|
|
158
|
+
images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
159
159
|
condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
|
|
160
160
|
tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
161
161
|
seoTitle: z.ZodOptional<z.ZodString>;
|
|
@@ -255,8 +255,8 @@ export declare const preOrderInputSchema: z.ZodObject<{
|
|
|
255
255
|
currency: z.ZodDefault<z.ZodString>;
|
|
256
256
|
stockQuantity: z.ZodDefault<z.ZodNumber>;
|
|
257
257
|
availableQuantity: z.ZodDefault<z.ZodNumber>;
|
|
258
|
-
mainImage: z.ZodString
|
|
259
|
-
images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
258
|
+
mainImage: z.ZodEffects<z.ZodString, string, string>;
|
|
259
|
+
images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
260
260
|
condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
|
|
261
261
|
tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
262
262
|
seoTitle: z.ZodOptional<z.ZodString>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { mediaUrlSchema } from "../../../../validation/schemas";
|
|
2
3
|
const priceSchema = z.number().min(0, "Price must be non-negative");
|
|
3
4
|
const conditionSchema = z.enum(["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]);
|
|
4
5
|
export const productInputSchema = z.object({
|
|
@@ -10,8 +11,8 @@ export const productInputSchema = z.object({
|
|
|
10
11
|
currency: z.string().default("INR"),
|
|
11
12
|
stockQuantity: z.number().int().min(0).default(0),
|
|
12
13
|
availableQuantity: z.number().int().min(0).default(0),
|
|
13
|
-
mainImage:
|
|
14
|
-
images: z.array(
|
|
14
|
+
mainImage: mediaUrlSchema,
|
|
15
|
+
images: z.array(mediaUrlSchema).max(20).default([]),
|
|
15
16
|
condition: conditionSchema.optional(),
|
|
16
17
|
tags: z.array(z.string()).max(20).default([]),
|
|
17
18
|
seoTitle: z.string().max(70).optional(),
|
|
@@ -5,7 +5,7 @@ export declare const createReviewSchema: z.ZodObject<{
|
|
|
5
5
|
rating: z.ZodNumber;
|
|
6
6
|
title: z.ZodString;
|
|
7
7
|
comment: z.ZodString;
|
|
8
|
-
images: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
8
|
+
images: z.ZodDefault<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
|
|
9
9
|
orderId: z.ZodOptional<z.ZodString>;
|
|
10
10
|
}, "strip", z.ZodTypeAny, {
|
|
11
11
|
title: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { mediaUrlSchema } from "../../../../validation/schemas";
|
|
2
3
|
import { REVIEW_BODY_MAX_LENGTH, REVIEW_BODY_MIN_LENGTH, REVIEW_TITLE_MAX_LENGTH, REVIEW_MIN_RATING, REVIEW_MAX_RATING, REVIEW_IMAGES_MAX, REVIEW_REPLY_MAX_LENGTH, } from "./config";
|
|
3
4
|
export const createReviewSchema = z.object({
|
|
4
5
|
productId: z.string().min(1),
|
|
@@ -16,7 +17,7 @@ export const createReviewSchema = z.object({
|
|
|
16
17
|
* never consulted it.
|
|
17
18
|
*/
|
|
18
19
|
comment: z.string().min(REVIEW_BODY_MIN_LENGTH).max(REVIEW_BODY_MAX_LENGTH),
|
|
19
|
-
images: z.array(
|
|
20
|
+
images: z.array(mediaUrlSchema).max(REVIEW_IMAGES_MAX).default([]),
|
|
20
21
|
orderId: z.string().optional(),
|
|
21
22
|
});
|
|
22
23
|
export const replyToReviewSchema = z.object({
|
|
@@ -14,8 +14,6 @@ export type ActionResult<T> = {
|
|
|
14
14
|
code?: string;
|
|
15
15
|
error: string;
|
|
16
16
|
issues?: unknown[];
|
|
17
|
-
/** Legacy server-action shape — kept for compat. */
|
|
18
|
-
fieldErrors?: Record<string, string[]>;
|
|
19
17
|
/** Legacy debug field — only populated in dev. */
|
|
20
18
|
debug?: {
|
|
21
19
|
stack?: string;
|
|
@@ -18,6 +18,15 @@ export interface SurfaceErrorOptions {
|
|
|
18
18
|
setFieldError?: (field: string, message: string) => void;
|
|
19
19
|
/** next-intl style translator. Falls back to the raw error message when undefined. */
|
|
20
20
|
translate?: (key: string) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Curated copy preferred over `err.message` when the error carries no
|
|
23
|
+
* mapped display message. Lets a caller keep wording better than the raw
|
|
24
|
+
* server string WITHOUT adding a second toast of its own.
|
|
25
|
+
*
|
|
26
|
+
* Never overrides field-level `issues` — those are more specific than any
|
|
27
|
+
* caller-supplied sentence.
|
|
28
|
+
*/
|
|
29
|
+
fallbackMessage?: string;
|
|
21
30
|
/** Optional reporter for the clientErrors / serverErrors collection. */
|
|
22
31
|
report?: (payload: {
|
|
23
32
|
code: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isApiError } from "./ApiError";
|
|
2
2
|
import { ApiClientError } from "../../http/ApiClient";
|
|
3
|
+
import { applyZodIssues, hasAttachableIssue } from "../../ui/forms/apply-zod-issues";
|
|
3
4
|
import { normalizeError } from "../../errors/normalize";
|
|
4
5
|
import { getErrorDisplay } from "../../errors/error-display-map";
|
|
5
6
|
/** True for `ApiError` instances and for `ApiClientError` (the lower-level
|
|
@@ -9,11 +10,36 @@ import { getErrorDisplay } from "../../errors/error-display-map";
|
|
|
9
10
|
function hasStableErrorCode(err) {
|
|
10
11
|
return isApiError(err) || (err instanceof ApiClientError && typeof err.code === "string");
|
|
11
12
|
}
|
|
13
|
+
/** Field-level issues carried by either error shape, when present. */
|
|
14
|
+
function issuesOf(err) {
|
|
15
|
+
if (isApiError(err))
|
|
16
|
+
return err.issues;
|
|
17
|
+
if (err instanceof ApiClientError)
|
|
18
|
+
return err.issues;
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
12
21
|
export function surfaceError(err, opts) {
|
|
13
|
-
const { showToast, setFieldError, translate, report } = opts;
|
|
22
|
+
const { showToast, setFieldError, translate, report, fallbackMessage } = opts;
|
|
23
|
+
// ISSUES FIRST. A validation failure names the fields it is about, and the
|
|
24
|
+
// right place for "photoURL must be …" is under the photoURL input — not a
|
|
25
|
+
// toast that says "Validation failed" and nothing else.
|
|
26
|
+
//
|
|
27
|
+
// This branch could not fire before: `errorResponse` serialised Zod issues
|
|
28
|
+
// under a `details` key that `ApiClientError` never read, so `err.issues`
|
|
29
|
+
// was always undefined and every schema rejection on every route fell
|
|
30
|
+
// through to the generic toast below.
|
|
31
|
+
//
|
|
32
|
+
// `ERROR_DISPLAY_MAP` deliberately has no `field` for VALIDATION_FAILED —
|
|
33
|
+
// a validation failure is not tied to one fixed field, which is exactly why
|
|
34
|
+
// the routing has to come from the issues themselves.
|
|
35
|
+
const issues = issuesOf(err);
|
|
36
|
+
if (setFieldError && hasAttachableIssue(issues)) {
|
|
37
|
+
applyZodIssues(issues, (name, message) => setFieldError(name, message ?? ""));
|
|
38
|
+
return; // inline errors only — no toast
|
|
39
|
+
}
|
|
14
40
|
if (hasStableErrorCode(err)) {
|
|
15
41
|
const display = getErrorDisplay(err.code);
|
|
16
|
-
const message = translate?.(display.messageKey) ?? err.message;
|
|
42
|
+
const message = translate?.(display.messageKey) ?? fallbackMessage ?? err.message;
|
|
17
43
|
if (display.field && setFieldError) {
|
|
18
44
|
setFieldError(display.field, message);
|
|
19
45
|
return;
|
|
@@ -24,7 +50,7 @@ export function surfaceError(err, opts) {
|
|
|
24
50
|
// Non-ApiError — this is a programming error or unhandled exception.
|
|
25
51
|
// Report it (workstream 13) so clientErrors picks it up.
|
|
26
52
|
const message = err instanceof Error ? err.message : "Something went wrong. Please try again.";
|
|
27
|
-
const fallback = translate?.("errors.codes.CLIENT_UNHANDLED") ?? message;
|
|
53
|
+
const fallback = fallbackMessage ?? translate?.("errors.codes.CLIENT_UNHANDLED") ?? message;
|
|
28
54
|
showToast(fallback, "error");
|
|
29
55
|
if (report) {
|
|
30
56
|
try {
|
|
@@ -16,6 +16,16 @@ export interface UseApiMutationOptions<TData, TError, TVariables, TContext> exte
|
|
|
16
16
|
/** When set, a `loading` toast is shown on mutate and finalized on settle. */
|
|
17
17
|
loadingMessage?: string;
|
|
18
18
|
successMessage?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Curated failure copy, preferred over `err.message` by the ONE surface.
|
|
21
|
+
*
|
|
22
|
+
* Exists so a caller with wording better than the raw server message
|
|
23
|
+
* ("Could not mark notification as read.") does not have to add a second
|
|
24
|
+
* toast in its own `onError` to say it — which is what produced two and
|
|
25
|
+
* three stacked banners for a single failure. A server error that carries
|
|
26
|
+
* field `issues` still wins: those route to inline field errors.
|
|
27
|
+
*/
|
|
28
|
+
errorMessage?: string;
|
|
19
29
|
/** Translation function (next-intl style); falls back to raw message. */
|
|
20
30
|
translate?: (key: string) => string;
|
|
21
31
|
/** Form field error setter (FormShell context). Optional. */
|
|
@@ -6,7 +6,7 @@ import { surfaceError } from "./surface-error";
|
|
|
6
6
|
export function useApiMutation(options) {
|
|
7
7
|
const { showToast, updateToast } = useToast();
|
|
8
8
|
const loadingIdRef = React.useRef(null);
|
|
9
|
-
const { loadingMessage, successMessage, translate, setFieldError, reportClientError, onMutate, onError, onSuccess, onSettled, ...rest } = options;
|
|
9
|
+
const { loadingMessage, successMessage, errorMessage, translate, setFieldError, reportClientError, onMutate, onError, onSuccess, onSettled, ...rest } = options;
|
|
10
10
|
return useMutation({
|
|
11
11
|
...rest,
|
|
12
12
|
onMutate: (async (variables, ctx) => {
|
|
@@ -20,6 +20,7 @@ export function useApiMutation(options) {
|
|
|
20
20
|
showToast,
|
|
21
21
|
setFieldError,
|
|
22
22
|
translate,
|
|
23
|
+
fallbackMessage: errorMessage,
|
|
23
24
|
report: reportClientError,
|
|
24
25
|
});
|
|
25
26
|
onError?.(err, variables, context, ctx);
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
export type ActionResult
|
|
2
|
-
|
|
3
|
-
data: TOutput;
|
|
4
|
-
} | {
|
|
5
|
-
ok: false;
|
|
6
|
-
error: string;
|
|
7
|
-
code?: string;
|
|
8
|
-
fieldErrors?: Record<string, string[]>;
|
|
9
|
-
debug?: {
|
|
10
|
-
stack?: string;
|
|
11
|
-
};
|
|
12
|
-
};
|
|
1
|
+
export type { ActionResult } from "../_internal/shared/types/action-result";
|
|
2
|
+
import type { ActionResult } from "../_internal/shared/types/action-result";
|
|
13
3
|
export interface ActionMiddlewareContext<TInput = unknown, TOutput = unknown> {
|
|
14
4
|
actionName: string;
|
|
15
5
|
input: TInput;
|
|
@@ -50,7 +40,7 @@ export interface ServerActionOptions<TInput, TOutput> {
|
|
|
50
40
|
name?: string;
|
|
51
41
|
/**
|
|
52
42
|
* Zod-compatible schema for input validation.
|
|
53
|
-
* If validation fails, returns `{ ok: false,
|
|
43
|
+
* If validation fails, returns `{ ok: false, issues }` before the handler runs.
|
|
54
44
|
*/
|
|
55
45
|
schema?: SafeParseSchema<TInput>;
|
|
56
46
|
/**
|
|
@@ -79,4 +69,3 @@ export interface ServerActionOptions<TInput, TOutput> {
|
|
|
79
69
|
* and can be exported directly from an `actions/` file.
|
|
80
70
|
*/
|
|
81
71
|
export declare function createServerAction<TInput = unknown, TOutput = unknown>(options: ServerActionOptions<TInput, TOutput>): (rawInput: TInput) => Promise<ActionResult<TOutput>>;
|
|
82
|
-
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { normalizeError } from "../errors/normalize";
|
|
2
|
+
import { toApiIssues } from "../errors/error-envelope";
|
|
3
|
+
import { HTTP_ERROR_CODES } from "../errors/error-mapping";
|
|
2
4
|
/**
|
|
3
5
|
* createServerAction — @mohasinac/appkit/core
|
|
4
6
|
*
|
|
@@ -65,8 +67,15 @@ export function createServerAction(options) {
|
|
|
65
67
|
if (options.schema) {
|
|
66
68
|
const parsed = options.schema.safeParse(rawInput);
|
|
67
69
|
if (!parsed.success) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
// `issues` (not the old `fieldErrors`, which had zero readers) so a
|
|
71
|
+
// server action's validation failure routes to inline field errors
|
|
72
|
+
// through the same path an HTTP route's does.
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: "Validation failed",
|
|
76
|
+
code: HTTP_ERROR_CODES.VALIDATION_FAILED,
|
|
77
|
+
issues: toApiIssues(parsed.error.issues),
|
|
78
|
+
};
|
|
70
79
|
}
|
|
71
80
|
input = parsed.data;
|
|
72
81
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE shape of an HTTP error body.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS
|
|
5
|
+
* --------------------
|
|
6
|
+
* There were four independent producers of an error response, and the client
|
|
7
|
+
* read a shape none of them fully emitted:
|
|
8
|
+
*
|
|
9
|
+
* - `next/api/api-response.ts` → `{success, error, details}` — no `code`
|
|
10
|
+
* - `next/api/routeHandler.ts` → `{ok, success, code, error, issues, requestId}`
|
|
11
|
+
* - `errors/error-handler.ts` → `{..., data: {issues}}` — issues nested
|
|
12
|
+
* - `core/server-action.ts` → `{ok, error, fieldErrors}`
|
|
13
|
+
*
|
|
14
|
+
* while `http/ApiClient.ts`'s `ApiClientError` reads exactly three keys off the
|
|
15
|
+
* body: `code`, `issues`, `requestId`.
|
|
16
|
+
*
|
|
17
|
+
* The consequence was silent and app-wide. On every route built with
|
|
18
|
+
* `createApiHandler`, a Zod rejection serialised its issues under `details`,
|
|
19
|
+
* which nothing reads — so `err.code` was `undefined`, `surfaceError`'s
|
|
20
|
+
* `hasStableErrorCode()` check failed, the `ERROR_DISPLAY_MAP` branch was
|
|
21
|
+
* skipped, and the user got a bare toast reading "Validation failed" with no
|
|
22
|
+
* field name. `applyZodIssues(err.issues, …)` was dead code everywhere.
|
|
23
|
+
*
|
|
24
|
+
* Every producer now builds its body here, so the wire shape and the reader
|
|
25
|
+
* cannot drift again. `scripts/audit-api-error-envelope.mjs` enforces it in
|
|
26
|
+
* both directions.
|
|
27
|
+
*/
|
|
28
|
+
import type { ApiIssue } from "../client/api/ApiError";
|
|
29
|
+
export interface ApiErrorEnvelope {
|
|
30
|
+
ok: false;
|
|
31
|
+
/**
|
|
32
|
+
* @deprecated Mirrors `ok` for consumers that still read `body.success`.
|
|
33
|
+
* `audit-server-action-envelope.mjs` is designed to start failing once the
|
|
34
|
+
* last reader is gone, at which point this key is removed.
|
|
35
|
+
*/
|
|
36
|
+
success: false;
|
|
37
|
+
code: string;
|
|
38
|
+
error: string;
|
|
39
|
+
issues?: ApiIssue[];
|
|
40
|
+
requestId?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Default stable code for an HTTP status, for producers that don't carry one.
|
|
44
|
+
*
|
|
45
|
+
* Mirrors the mapping already applied in `mapToHttpError`'s status branch —
|
|
46
|
+
* extracted so there is one table rather than two that can disagree about
|
|
47
|
+
* what a 409 is called.
|
|
48
|
+
*/
|
|
49
|
+
export declare function codeForStatus(status: number): string;
|
|
50
|
+
/**
|
|
51
|
+
* Narrow raw Zod issues to the wire shape.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately picks ONLY `path`, `message` and `code`. A `ZodIssue` can also
|
|
54
|
+
* carry `received` / `expected` / `unionErrors`, and `received` is the value
|
|
55
|
+
* the user actually submitted — i.e. potentially their email, phone or
|
|
56
|
+
* address. Spreading the issue wholesale would publish that in an error body
|
|
57
|
+
* and into any client-error log that captures it.
|
|
58
|
+
*
|
|
59
|
+
* The `unknown` parameter is the point: this is the sole entry point for issue
|
|
60
|
+
* arrays arriving from THREE mutually unassignable sources — zod v3
|
|
61
|
+
* `ZodIssue[]` (appkit), zod v4 `$ZodIssue[]` (the consumer app, a different
|
|
62
|
+
* major), and the `unknown[]` that `mapToHttpError` already returns. Narrowing
|
|
63
|
+
* it to any one of them makes the other two uncallable. This function exists
|
|
64
|
+
* precisely to be the place that narrows.
|
|
65
|
+
*/
|
|
66
|
+
export declare function toApiIssues(input: unknown): ApiIssue[] | undefined;
|
|
67
|
+
export declare function buildErrorEnvelope(input: {
|
|
68
|
+
message: string;
|
|
69
|
+
status?: number;
|
|
70
|
+
code?: string;
|
|
71
|
+
issues?: ApiIssue[];
|
|
72
|
+
requestId?: string;
|
|
73
|
+
}): ApiErrorEnvelope;
|