@mohasinac/appkit 4.29.1 → 4.30.1
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/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/auth/repository/token.repository.d.ts +25 -2
- package/dist/features/auth/repository/token.repository.js +26 -18
- 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 -4
- package/dist/index.js +4 -7
- 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 +48 -4
- package/dist/security/browser.d.ts +1 -1
- package/dist/security/browser.js +1 -1
- package/dist/security/index.d.ts +2 -2
- package/dist/security/index.js +2 -2
- package/dist/security/pii-encrypt.d.ts +12 -10
- package/dist/security/pii-encrypt.js +91 -29
- 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 -6
- package/dist/security/pii-schemas.js +29 -6
- package/dist/security/settings-encryption.js +31 -3
- package/dist/seed/test-utils.js +6 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- 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 +21 -6
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,96 @@
|
|
|
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 { HTTP_ERROR_CODES } from "./error-mapping";
|
|
29
|
+
/**
|
|
30
|
+
* Default stable code for an HTTP status, for producers that don't carry one.
|
|
31
|
+
*
|
|
32
|
+
* Mirrors the mapping already applied in `mapToHttpError`'s status branch —
|
|
33
|
+
* extracted so there is one table rather than two that can disagree about
|
|
34
|
+
* what a 409 is called.
|
|
35
|
+
*/
|
|
36
|
+
export function codeForStatus(status) {
|
|
37
|
+
if (status === 401)
|
|
38
|
+
return HTTP_ERROR_CODES.UNAUTHENTICATED;
|
|
39
|
+
if (status === 403)
|
|
40
|
+
return HTTP_ERROR_CODES.FORBIDDEN;
|
|
41
|
+
if (status === 404)
|
|
42
|
+
return HTTP_ERROR_CODES.NOT_FOUND;
|
|
43
|
+
if (status === 409)
|
|
44
|
+
return HTTP_ERROR_CODES.ALREADY_EXISTS;
|
|
45
|
+
if (status === 429)
|
|
46
|
+
return HTTP_ERROR_CODES.RATE_LIMITED;
|
|
47
|
+
if (status >= 500)
|
|
48
|
+
return HTTP_ERROR_CODES.INTERNAL;
|
|
49
|
+
return HTTP_ERROR_CODES.VALIDATION_FAILED;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Narrow raw Zod issues to the wire shape.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately picks ONLY `path`, `message` and `code`. A `ZodIssue` can also
|
|
55
|
+
* carry `received` / `expected` / `unionErrors`, and `received` is the value
|
|
56
|
+
* the user actually submitted — i.e. potentially their email, phone or
|
|
57
|
+
* address. Spreading the issue wholesale would publish that in an error body
|
|
58
|
+
* and into any client-error log that captures it.
|
|
59
|
+
*
|
|
60
|
+
* The `unknown` parameter is the point: this is the sole entry point for issue
|
|
61
|
+
* arrays arriving from THREE mutually unassignable sources — zod v3
|
|
62
|
+
* `ZodIssue[]` (appkit), zod v4 `$ZodIssue[]` (the consumer app, a different
|
|
63
|
+
* major), and the `unknown[]` that `mapToHttpError` already returns. Narrowing
|
|
64
|
+
* it to any one of them makes the other two uncallable. This function exists
|
|
65
|
+
* precisely to be the place that narrows.
|
|
66
|
+
*/
|
|
67
|
+
export function toApiIssues(input) {
|
|
68
|
+
if (!Array.isArray(input) || input.length === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
const mapped = [];
|
|
71
|
+
for (const raw of input) {
|
|
72
|
+
if (typeof raw !== "object" || raw === null)
|
|
73
|
+
continue;
|
|
74
|
+
const issue = raw;
|
|
75
|
+
if (typeof issue.message !== "string")
|
|
76
|
+
continue;
|
|
77
|
+
mapped.push({
|
|
78
|
+
message: issue.message,
|
|
79
|
+
...(Array.isArray(issue.path)
|
|
80
|
+
? { path: issue.path.filter((p) => typeof p === "string" || typeof p === "number") }
|
|
81
|
+
: {}),
|
|
82
|
+
...(typeof issue.code === "string" ? { code: issue.code } : {}),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return mapped.length ? mapped : undefined;
|
|
86
|
+
}
|
|
87
|
+
export function buildErrorEnvelope(input) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
success: false,
|
|
91
|
+
code: input.code ?? codeForStatus(input.status ?? 400),
|
|
92
|
+
error: input.message,
|
|
93
|
+
...(input.issues?.length ? { issues: input.issues } : {}),
|
|
94
|
+
...(input.requestId ? { requestId: input.requestId } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from "next/server.js";
|
|
2
2
|
import { AppError } from "./base-error";
|
|
3
3
|
import { mapToHttpError } from "./error-mapping";
|
|
4
|
+
import { buildErrorEnvelope, toApiIssues } from "./error-envelope";
|
|
4
5
|
/**
|
|
5
6
|
* Handle API errors with consistent response format.
|
|
6
7
|
* Use in Next.js API route catch blocks.
|
|
@@ -23,16 +24,22 @@ export function handleApiError(error) {
|
|
|
23
24
|
data: error instanceof AppError ? error.data : undefined,
|
|
24
25
|
});
|
|
25
26
|
}
|
|
27
|
+
// `issues` go at the TOP LEVEL. They used to be nested under
|
|
28
|
+
// `data.issues`, and `ApiClientError` reads `body.issues` — so this
|
|
29
|
+
// producer's field errors were as dead as `api-response.ts`'s were.
|
|
30
|
+
//
|
|
31
|
+
// `statusCode` is dropped: it only ever duplicated the HTTP status, and
|
|
32
|
+
// nothing read it off the body.
|
|
26
33
|
return NextResponse.json({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
...buildErrorEnvelope({
|
|
35
|
+
message: mapped.message,
|
|
36
|
+
status: mapped.status,
|
|
37
|
+
code: mapped.code,
|
|
38
|
+
issues: toApiIssues(mapped.issues),
|
|
39
|
+
}),
|
|
31
40
|
...(error instanceof AppError && error.data !== undefined
|
|
32
41
|
? { data: error.data }
|
|
33
|
-
:
|
|
34
|
-
? { data: { issues: mapped.issues } }
|
|
35
|
-
: {}),
|
|
42
|
+
: {}),
|
|
36
43
|
}, { status: mapped.status });
|
|
37
44
|
}
|
|
38
45
|
/**
|
|
@@ -96,7 +96,7 @@ export function NotificationBell({ limit = 10, viewAllHref, labels, icons, rende
|
|
|
96
96
|
}
|
|
97
97
|
function renderBellButton(props) {
|
|
98
98
|
const { hideOnMobile, buttonClassName, labels, unreadCount, handleToggle, isOpen } = props;
|
|
99
|
-
return (_jsxs(Button, { onClick: handleToggle, rounded: "xl", className: `${hideOnMobile ? "hidden md:flex" : "flex"} p-[0.625rem] md:p-[0.75rem] transition-colors relative text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-bg)] hover:text-[var(--appkit-color-text)] ${buttonClassName}`, "aria-label": labels.title, "aria-expanded": isOpen, children: [_jsx("svg", { className: "w-6 h-6 md:w-7 md:h-7", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" }) }), unreadCount > 0 && (_jsx(Span, { size: "xs", weight: "semibold", className: CLS_UNREAD_BADGE, children: unreadCount > 99 ? "99+" : unreadCount }))] }));
|
|
99
|
+
return (_jsxs(Button, { onClick: handleToggle, rounded: "xl", className: `${hideOnMobile ? "hidden md:flex" : "flex"} p-[0.625rem] md:p-[0.75rem] transition-colors relative text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-bg)] hover:text-[var(--appkit-color-text)] ${buttonClassName}`, "aria-label": labels.title, "aria-expanded": isOpen, children: [_jsx("svg", { className: "w-6 h-6 md:w-7 md:h-7 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" }) }), unreadCount > 0 && (_jsx(Span, { size: "xs", weight: "semibold", className: CLS_UNREAD_BADGE, children: unreadCount > 99 ? "99+" : unreadCount }))] }));
|
|
100
100
|
}
|
|
101
101
|
function renderNotificationDropdown(props) {
|
|
102
102
|
const { dropdownClassName, labels, unreadCount, isMarkingAll, handleMarkAllRead, isLoading, notifications, notificationIcons, handleMarkRead, handleMarkReadAndClose, renderActionLink, viewAllHref, setIsOpen } = props;
|