@mohasinac/appkit 4.41.0 → 4.41.2
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/features/seller/schemas/coupon-form.d.ts +37 -37
- package/dist/features/seller/schemas/coupon-form.js +36 -8
- package/dist/features/shell/SectionForm.js +44 -5
- package/dist/schemas/registry.d.ts +10 -10
- package/package.json +2 -2
- package/dist/_internal/server/features/classified/actions.d.ts +0 -12
- package/dist/_internal/server/features/classified/actions.js +0 -33
- package/dist/_internal/server/features/digital-code/actions.d.ts +0 -12
- package/dist/_internal/server/features/digital-code/actions.js +0 -33
- package/dist/_internal/server/features/homepage/data.d.ts +0 -18
- package/dist/_internal/server/features/homepage/data.js +0 -70
- package/dist/_internal/server/features/live/actions.d.ts +0 -6
- package/dist/_internal/server/features/live/actions.js +0 -27
- package/dist/_internal/server/features/messages/actions.d.ts +0 -6
- package/dist/_internal/server/features/messages/actions.js +0 -6
- package/dist/_internal/server/features/messages/data.d.ts +0 -10
- package/dist/_internal/server/features/messages/data.js +0 -24
- package/dist/_internal/server/features/messages/index.d.ts +0 -2
- package/dist/_internal/server/features/messages/index.js +0 -2
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +0 -12
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +0 -112
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +0 -2
- package/dist/_internal/shared/features/homepage/section-gate.d.ts +0 -14
- package/dist/_internal/shared/features/homepage/section-gate.js +0 -51
- package/dist/_internal/shared/features/lottery/config-form.d.ts +0 -58
- package/dist/_internal/shared/features/lottery/config-form.js +0 -71
- package/dist/client/api/useFeatureFlags.d.ts +0 -22
- package/dist/client/api/useFeatureFlags.js +0 -27
- package/dist/features/account/address-validation.d.ts +0 -20
- package/dist/features/account/address-validation.js +0 -35
- package/dist/features/account/components/ChatList.d.ts +0 -14
- package/dist/features/account/components/ChatList.js +0 -9
- package/dist/features/account/components/ChatWindow.d.ts +0 -17
- package/dist/features/account/components/ChatWindow.js +0 -6
- package/dist/features/account/components/MessagesView.d.ts +0 -16
- package/dist/features/account/components/MessagesView.js +0 -5
- package/dist/features/account/components/UserOffersView.d.ts +0 -10
- package/dist/features/account/components/UserOffersView.js +0 -5
- package/dist/features/account/constants/addresses.d.ts +0 -13
- package/dist/features/account/constants/addresses.js +0 -43
- package/dist/features/account/hooks/useAddressForm.d.ts +0 -24
- package/dist/features/account/hooks/useAddressForm.js +0 -58
- package/dist/features/admin/actions/chat-actions.d.ts +0 -29
- package/dist/features/admin/actions/chat-actions.js +0 -101
- package/dist/features/admin/components/AdminFeatureFlagsView.d.ts +0 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +0 -109
- package/dist/features/admin/components/AdminSiteView.d.ts +0 -10
- package/dist/features/admin/components/AdminSiteView.js +0 -52
- package/dist/features/admin/hooks/useChat.d.ts +0 -24
- package/dist/features/admin/hooks/useChat.js +0 -100
- package/dist/features/admin/permission-map.d.ts +0 -3
- package/dist/features/admin/permission-map.js +0 -26
- package/dist/features/admin/repository/chat.repository.d.ts +0 -50
- package/dist/features/admin/repository/chat.repository.js +0 -223
- package/dist/features/auth/actions/password-change-otp-actions.d.ts +0 -18
- package/dist/features/auth/actions/password-change-otp-actions.js +0 -88
- package/dist/features/auth/actions/realtime-token-actions.d.ts +0 -11
- package/dist/features/auth/actions/realtime-token-actions.js +0 -38
- package/dist/features/auth/password-change-otp.d.ts +0 -34
- package/dist/features/auth/password-change-otp.js +0 -56
- package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +0 -12
- package/dist/features/classified/components/ClassifiedContactSellerPanel.js +0 -49
- package/dist/features/events/api/route.d.ts +0 -12
- package/dist/features/events/api/route.js +0 -90
- package/dist/features/faq/api/route.d.ts +0 -11
- package/dist/features/faq/api/route.js +0 -52
- package/dist/features/faq/hooks/useFAQs.d.ts +0 -22
- package/dist/features/faq/hooks/useFAQs.js +0 -41
- package/dist/features/media/upload/video-poster.test.d.ts +0 -1
- package/dist/features/media/upload/video-poster.test.js +0 -42
- package/dist/features/messages/actions/index.d.ts +0 -2
- package/dist/features/messages/actions/index.js +0 -2
- package/dist/features/messages/actions/messages-actions.d.ts +0 -15
- package/dist/features/messages/actions/messages-actions.js +0 -33
- package/dist/features/messages/actions/ping-rtdb.d.ts +0 -2
- package/dist/features/messages/actions/ping-rtdb.js +0 -26
- package/dist/features/messages/hooks/useConversation.d.ts +0 -22
- package/dist/features/messages/hooks/useConversation.js +0 -155
- package/dist/features/messages/hooks/useConversations.d.ts +0 -15
- package/dist/features/messages/hooks/useConversations.js +0 -104
- package/dist/features/messages/index.d.ts +0 -6
- package/dist/features/messages/index.js +0 -4
- package/dist/features/messages/realtime.d.ts +0 -32
- package/dist/features/messages/realtime.js +0 -39
- package/dist/features/messages/repository/conversations.repository.d.ts +0 -71
- package/dist/features/messages/repository/conversations.repository.js +0 -284
- package/dist/features/messages/schemas/firestore.d.ts +0 -34
- package/dist/features/messages/schemas/firestore.js +0 -16
- package/dist/features/messages/schemas/index.d.ts +0 -177
- package/dist/features/messages/schemas/index.js +0 -35
- package/dist/features/messages/server.d.ts +0 -6
- package/dist/features/messages/server.js +0 -6
- package/dist/features/products/components/MakeOfferForm.d.ts +0 -20
- package/dist/features/products/components/MakeOfferForm.js +0 -9
- package/dist/features/reviews/api/route.d.ts +0 -21
- package/dist/features/reviews/api/route.js +0 -174
- package/dist/features/seller/api/offers/route.d.ts +0 -17
- package/dist/features/seller/api/offers/route.js +0 -63
- package/dist/features/seller/components/SellerAuctionsView.d.ts +0 -9
- package/dist/features/seller/components/SellerAuctionsView.js +0 -113
- package/dist/features/seller/components/SellerOffersPanel.d.ts +0 -21
- package/dist/features/seller/components/SellerOffersPanel.js +0 -160
- package/dist/features/seller/components/SellerPreOrdersView.d.ts +0 -6
- package/dist/features/seller/components/SellerPreOrdersView.js +0 -148
- package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +0 -6
- package/dist/features/seller/components/SellerPrizeDrawsView.js +0 -152
- package/dist/features/seller/permission-map.d.ts +0 -5
- package/dist/features/seller/permission-map.js +0 -17
- package/dist/features/shell/StepForm.d.ts +0 -80
- package/dist/features/shell/StepForm.js +0 -136
- package/dist/features/stores/api/route.d.ts +0 -12
- package/dist/features/stores/api/route.js +0 -110
- package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.js +0 -33
- package/dist/features/tester/seed-data/authored/account-auth__testing-program.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/account-auth__testing-program.js +0 -138
- package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/bids-tester-seed-data.js +0 -37
- package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/blog-tester-seed-data.js +0 -40
- package/dist/features/tester/seed-data/carts-tester-seed-data.d.ts +0 -3
- package/dist/features/tester/seed-data/carts-tester-seed-data.js +0 -70
- package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +0 -218
- package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +0 -76
- package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/events-tester-seed-data.js +0 -193
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +0 -191
- package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +0 -329
- package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/products-tester-seed-data.js +0 -815
- package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/stores-tester-seed-data.js +0 -79
- package/dist/features/wishlist/components/WishlistToggleButton.d.ts +0 -12
- package/dist/features/wishlist/components/WishlistToggleButton.js +0 -36
- package/dist/monitoring/analytics.d.ts +0 -80
- package/dist/monitoring/analytics.js +0 -176
- package/dist/next/middleware/pii-redactor.d.ts +0 -32
- package/dist/next/middleware/pii-redactor.js +0 -84
- package/dist/providers/db-firebase/realtime.d.ts +0 -42
- package/dist/providers/db-firebase/realtime.js +0 -138
- package/dist/security/rbac/Can.d.ts +0 -31
- package/dist/security/rbac/Can.js +0 -22
- package/dist/security/rbac/default-roles.d.ts +0 -2
- package/dist/security/rbac/default-roles.js +0 -83
- package/dist/security/rbac/hook.d.ts +0 -20
- package/dist/security/rbac/hook.js +0 -30
- package/dist/security/rbac/index.d.ts +0 -8
- package/dist/security/rbac/index.js +0 -6
- package/dist/security/rbac/middleware.d.ts +0 -51
- package/dist/security/rbac/middleware.js +0 -51
- package/dist/security/rbac/resolver.d.ts +0 -5
- package/dist/security/rbac/resolver.js +0 -38
- package/dist/security/rbac/server.d.ts +0 -24
- package/dist/security/rbac/server.js +0 -42
- package/dist/security/rbac/types.d.ts +0 -22
- package/dist/security/rbac/types.js +0 -2
- package/dist/seed/conversations-seed-data.d.ts +0 -2
- package/dist/seed/conversations-seed-data.js +0 -292
- package/dist/utils/filter.helper.d.ts +0 -18
- package/dist/utils/filter.helper.js +0 -21
- package/dist/utils/pagination.helper.d.ts +0 -23
- package/dist/utils/pagination.helper.js +0 -24
- package/dist/utils/search-tokens.d.ts +0 -2
- package/dist/utils/search-tokens.js +0 -21
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* WHY: Shared tester sandbox — one auto-approved test store testers browse/buy from and
|
|
3
|
-
* use as a reference when creating their own listings. Auto-expires after 7 days
|
|
4
|
-
* (testerSandboxCleanup) and is invisible to non-testers (application-layer filter).
|
|
5
|
-
* WHAT: Exports 2 StoreDocument partials tagged isTestData:true — the shared sandbox
|
|
6
|
-
* (owned by admin) and the automated runner's OWN store.
|
|
7
|
-
*
|
|
8
|
-
* 🛑 TWO STORES, TWO OWNERS, AND THE SPLIT IS LOAD-BEARING.
|
|
9
|
-
*
|
|
10
|
-
* `store-tester-sandbox` stays owned by `user-admin-letitrip`. Several authored cases
|
|
11
|
-
* name that owner explicitly — buying__bidding.ts:211 says "Sign in as admin@letitrip.in,
|
|
12
|
-
* the owner of store-tester-sandbox", and :228 warns that signing in as anyone else
|
|
13
|
-
* "gives a 403 that reads like a permissions bug and is not one". Repointing it would
|
|
14
|
-
* turn those cases into false failures.
|
|
15
|
-
*
|
|
16
|
-
* `store-claude-tester` exists because `user-claude-tester` owned NO store while its user
|
|
17
|
-
* document carried `storeId: "store-tester-qa-seller"` — a store whose `ownerId` is
|
|
18
|
-
* `user-tester-qa`. The app resolves sellers by `ownerId`, so every /api/store/* call for
|
|
19
|
-
* the runner returned 403 "No store found for this account" while the full Store Panel
|
|
20
|
-
* rendered over it. That one-way pointer blocked 36 checklist cases (measured, run
|
|
21
|
-
* 1789300124915). Root Cause #42's denormalised-mirror shape on the user<->store axis.
|
|
22
|
-
*
|
|
23
|
-
* It is deliberately EMPTY of listings. The seller cases it unblocks are create/CRUD
|
|
24
|
-
* flows (listing-lifecycle, coupons, bundles, classifieds), which want a clean store;
|
|
25
|
-
* cases that need pre-existing stock still use the shared sandbox.
|
|
26
|
-
*
|
|
27
|
-
* EXPORTS:
|
|
28
|
-
* storesTesterSeedData — Array of 2 Partial<StoreDocument> for the seed runner
|
|
29
|
-
*
|
|
30
|
-
* @tag domain:stores,tester
|
|
31
|
-
* @tag layer:seed
|
|
32
|
-
* @tag pattern:none
|
|
33
|
-
* @tag access:server-only
|
|
34
|
-
* @tag consumers:seed/index.ts,seed-cli.mjs
|
|
35
|
-
* @tag sideEffects:none
|
|
36
|
-
*/
|
|
37
|
-
import { STORE_FIELDS } from "../../../constants/field-names";
|
|
38
|
-
import { seedPhoto } from "../../../seed/_helpers/media";
|
|
39
|
-
import { testDataExpiresAt } from "./tester-ttl";
|
|
40
|
-
export const storesTesterSeedData = [
|
|
41
|
-
{
|
|
42
|
-
id: "store-tester-sandbox",
|
|
43
|
-
storeSlug: "store-tester-sandbox",
|
|
44
|
-
ownerId: "user-admin-letitrip",
|
|
45
|
-
storeName: "Tester Sandbox Store",
|
|
46
|
-
storeDescription: "Shared test store for the tester QA program. Products here are disposable and auto-expire — buy, bid, and review freely.",
|
|
47
|
-
storeCategory: "category-tester-sandbox",
|
|
48
|
-
storeLogoURL: seedPhoto("store-logo-tester-sandbox-20260101", 400, 400),
|
|
49
|
-
storeBannerURL: seedPhoto("store-banner-tester-sandbox-20260101", 1600, 400),
|
|
50
|
-
status: STORE_FIELDS.STATUS_VALUES.ACTIVE,
|
|
51
|
-
isPublic: true,
|
|
52
|
-
bio: "This store exists only for tester QA. Everything in it is disposable test data.",
|
|
53
|
-
returnPolicy: "N/A — test data.",
|
|
54
|
-
shippingPolicy: "N/A — test data.",
|
|
55
|
-
isTestData: true,
|
|
56
|
-
testDataExpiresAt: testDataExpiresAt(),
|
|
57
|
-
createdAt: new Date(),
|
|
58
|
-
updatedAt: new Date(),
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: "store-claude-tester",
|
|
62
|
-
storeSlug: "store-claude-tester",
|
|
63
|
-
ownerId: "user-claude-tester",
|
|
64
|
-
storeName: "Claude Tester Store",
|
|
65
|
-
storeDescription: "The automated checklist runner's own store. Exists so seller-side create/edit flows can be exercised against a store the runner actually owns. Disposable test data.",
|
|
66
|
-
storeCategory: "category-tester-sandbox",
|
|
67
|
-
storeLogoURL: seedPhoto("store-logo-claude-tester-20260101", 400, 400),
|
|
68
|
-
storeBannerURL: seedPhoto("store-banner-claude-tester-20260101", 1600, 400),
|
|
69
|
-
status: STORE_FIELDS.STATUS_VALUES.ACTIVE,
|
|
70
|
-
isPublic: true,
|
|
71
|
-
bio: "Automated tester's store. Everything in it is disposable test data.",
|
|
72
|
-
returnPolicy: "N/A — test data.",
|
|
73
|
-
shippingPolicy: "N/A — test data.",
|
|
74
|
-
isTestData: true,
|
|
75
|
-
testDataExpiresAt: testDataExpiresAt(),
|
|
76
|
-
createdAt: new Date(),
|
|
77
|
-
updatedAt: new Date(),
|
|
78
|
-
},
|
|
79
|
-
];
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { MouseEvent } from "react";
|
|
2
|
-
interface WishlistToggleButtonProps {
|
|
3
|
-
inWishlist: boolean;
|
|
4
|
-
isLoading?: boolean;
|
|
5
|
-
onToggle: (e: MouseEvent) => void | Promise<void>;
|
|
6
|
-
addLabel: string;
|
|
7
|
-
removeLabel: string;
|
|
8
|
-
className?: string;
|
|
9
|
-
size?: "sm" | "md" | "lg";
|
|
10
|
-
}
|
|
11
|
-
export declare function WishlistToggleButton({ inWishlist, isLoading, onToggle, addLabel, removeLabel, className, size, }: WishlistToggleButtonProps): import("react").JSX.Element;
|
|
12
|
-
export {};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { Button, LoginRequiredModal } from "../../../ui";
|
|
5
|
-
import { isAuthError } from "../../../utils/auth-error";
|
|
6
|
-
import { normalizeError } from "../../../errors/normalize";
|
|
7
|
-
const sizeClasses = {
|
|
8
|
-
sm: "w-7 h-7",
|
|
9
|
-
md: "w-9 h-9",
|
|
10
|
-
lg: "w-11 h-11",
|
|
11
|
-
};
|
|
12
|
-
export function WishlistToggleButton({ inWishlist, isLoading = false, onToggle, addLabel, removeLabel, className = "", size = "md", }) {
|
|
13
|
-
const label = inWishlist ? removeLabel : addLabel;
|
|
14
|
-
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
15
|
-
async function handleClick(e) {
|
|
16
|
-
try {
|
|
17
|
-
await onToggle(e);
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
void normalizeError(err);
|
|
21
|
-
if (isAuthError(err)) {
|
|
22
|
-
setShowLoginModal(true);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to save items to your wishlist. Please log in or create an account." }), _jsx(Button, { type: "button", onClick: handleClick, disabled: isLoading, "aria-label": label, title: label, rounded: "full", className: `
|
|
27
|
-
flex items-[center] justify-[center]
|
|
28
|
-
transition-all duration-150
|
|
29
|
-
${inWishlist
|
|
30
|
-
? "bg-error-surface text-error hover:bg-error-surface"
|
|
31
|
-
: "bg-[rgba(255,255,255,0.8)] text-[var(--appkit-color-text-faint)] hover:text-error"}
|
|
32
|
-
${sizeClasses[size]}
|
|
33
|
-
${isLoading ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
|
|
34
|
-
${className}
|
|
35
|
-
`, children: _jsx("svg", { className: "w-4/6 h-4/6", viewBox: "0 0 24 24", fill: inWishlist ? "currentColor" : "none", stroke: "currentColor", strokeWidth: 2, children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" }) }) })] }));
|
|
36
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import type { JsonValue } from "@mohasinac/appkit";
|
|
2
|
-
export declare const ANALYTICS_EVENTS: {
|
|
3
|
-
readonly PAGE_VIEW: "page_view";
|
|
4
|
-
readonly LOGIN: "login";
|
|
5
|
-
readonly SIGN_UP: "sign_up";
|
|
6
|
-
readonly LOGOUT: "logout";
|
|
7
|
-
readonly VIEW_ITEM: "view_item";
|
|
8
|
-
readonly ADD_TO_CART: "add_to_cart";
|
|
9
|
-
readonly PURCHASE: "purchase";
|
|
10
|
-
readonly PLACE_BID: "place_bid";
|
|
11
|
-
readonly WIN_AUCTION: "win_auction";
|
|
12
|
-
readonly SEARCH: "search";
|
|
13
|
-
readonly VIEW_FAQ: "view_faq";
|
|
14
|
-
};
|
|
15
|
-
export type AnalyticsEvent = (typeof ANALYTICS_EVENTS)[keyof typeof ANALYTICS_EVENTS];
|
|
16
|
-
export declare function initializeAnalytics(params: {
|
|
17
|
-
app: JsonValue;
|
|
18
|
-
enabled?: boolean;
|
|
19
|
-
currency?: string;
|
|
20
|
-
}): Promise<void>;
|
|
21
|
-
export declare const trackEvent: (eventName: string, eventParams?: Record<string, JsonValue>) => void;
|
|
22
|
-
export declare const setAnalyticsUserId: (userId: string | null) => void;
|
|
23
|
-
export declare const setAnalyticsUserProperties: (properties: Record<string, string | number | boolean>) => void;
|
|
24
|
-
export declare const trackPageView: (page_path: string, page_title: string) => void;
|
|
25
|
-
export declare const trackAuth: {
|
|
26
|
-
login: (method: "email" | "google" | "apple") => void;
|
|
27
|
-
register: (method: "email" | "google" | "apple") => void;
|
|
28
|
-
logout: () => void;
|
|
29
|
-
};
|
|
30
|
-
export declare const trackEcommerce: {
|
|
31
|
-
viewProduct: (product: {
|
|
32
|
-
id: string;
|
|
33
|
-
name: string;
|
|
34
|
-
category: string;
|
|
35
|
-
price: number;
|
|
36
|
-
currency?: string;
|
|
37
|
-
}) => void;
|
|
38
|
-
addToCart: (product: {
|
|
39
|
-
id: string;
|
|
40
|
-
name: string;
|
|
41
|
-
category: string;
|
|
42
|
-
price: number;
|
|
43
|
-
quantity: number;
|
|
44
|
-
currency?: string;
|
|
45
|
-
}) => void;
|
|
46
|
-
purchase: (order: {
|
|
47
|
-
id: string;
|
|
48
|
-
total: number;
|
|
49
|
-
tax?: number;
|
|
50
|
-
shipping?: number;
|
|
51
|
-
currency?: string;
|
|
52
|
-
items: Array<{
|
|
53
|
-
id: string;
|
|
54
|
-
name: string;
|
|
55
|
-
category: string;
|
|
56
|
-
price: number;
|
|
57
|
-
quantity: number;
|
|
58
|
-
}>;
|
|
59
|
-
}) => void;
|
|
60
|
-
placeBid: (product: {
|
|
61
|
-
id: string;
|
|
62
|
-
name: string;
|
|
63
|
-
bidAmount: number;
|
|
64
|
-
}) => void;
|
|
65
|
-
winAuction: (product: {
|
|
66
|
-
id: string;
|
|
67
|
-
name: string;
|
|
68
|
-
finalBid: number;
|
|
69
|
-
}) => void;
|
|
70
|
-
};
|
|
71
|
-
export declare const trackContent: {
|
|
72
|
-
search: (searchTerm: string, resultsCount: number) => void;
|
|
73
|
-
viewFaq: (faqId: string, question: string) => void;
|
|
74
|
-
};
|
|
75
|
-
export declare const trackForm: {
|
|
76
|
-
submit: (formName: string) => void;
|
|
77
|
-
};
|
|
78
|
-
export declare const trackAdmin: {
|
|
79
|
-
action: (action: string, target: string) => void;
|
|
80
|
-
};
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { normalizeError } from "../errors/normalize";
|
|
2
|
-
import { getAnalytics, isSupported, logEvent, setUserId, setUserProperties, } from "firebase/analytics";
|
|
3
|
-
import { logger } from "../core/Logger";
|
|
4
|
-
let analytics = null;
|
|
5
|
-
let analyticsEnabled = false;
|
|
6
|
-
let ecommerceCurrency = "USD";
|
|
7
|
-
export const ANALYTICS_EVENTS = {
|
|
8
|
-
PAGE_VIEW: "page_view",
|
|
9
|
-
LOGIN: "login",
|
|
10
|
-
SIGN_UP: "sign_up",
|
|
11
|
-
LOGOUT: "logout",
|
|
12
|
-
VIEW_ITEM: "view_item",
|
|
13
|
-
ADD_TO_CART: "add_to_cart",
|
|
14
|
-
PURCHASE: "purchase",
|
|
15
|
-
PLACE_BID: "place_bid",
|
|
16
|
-
WIN_AUCTION: "win_auction",
|
|
17
|
-
SEARCH: "search",
|
|
18
|
-
VIEW_FAQ: "view_faq",
|
|
19
|
-
};
|
|
20
|
-
export async function initializeAnalytics(params) {
|
|
21
|
-
analyticsEnabled = params.enabled ?? true;
|
|
22
|
-
if (params.currency) {
|
|
23
|
-
ecommerceCurrency = params.currency;
|
|
24
|
-
}
|
|
25
|
-
if (!analyticsEnabled)
|
|
26
|
-
return;
|
|
27
|
-
try {
|
|
28
|
-
const supported = await isSupported();
|
|
29
|
-
if (!supported)
|
|
30
|
-
return;
|
|
31
|
-
analytics = getAnalytics(params.app);
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
void normalizeError(error);
|
|
35
|
-
analytics = null;
|
|
36
|
-
logger.warn("Firebase Analytics disabled", { error });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
export const trackEvent = (eventName, eventParams) => {
|
|
40
|
-
if (!analytics || !analyticsEnabled)
|
|
41
|
-
return;
|
|
42
|
-
try {
|
|
43
|
-
logEvent(analytics, eventName, eventParams);
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
void normalizeError(error);
|
|
47
|
-
logger.error(`Failed to log event ${eventName}`, { error });
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
export const setAnalyticsUserId = (userId) => {
|
|
51
|
-
if (!analytics || !analyticsEnabled)
|
|
52
|
-
return;
|
|
53
|
-
try {
|
|
54
|
-
setUserId(analytics, userId);
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
void normalizeError(error);
|
|
58
|
-
logger.error("Failed to set user ID", { error });
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
export const setAnalyticsUserProperties = (properties) => {
|
|
62
|
-
if (!analytics || !analyticsEnabled)
|
|
63
|
-
return;
|
|
64
|
-
try {
|
|
65
|
-
setUserProperties(analytics, properties);
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
void normalizeError(error);
|
|
69
|
-
logger.error("Failed to set user properties", { error });
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
export const trackPageView = (page_path, page_title) => {
|
|
73
|
-
trackEvent(ANALYTICS_EVENTS.PAGE_VIEW, {
|
|
74
|
-
page_path,
|
|
75
|
-
page_title,
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
export const trackAuth = {
|
|
79
|
-
login: (method) => {
|
|
80
|
-
trackEvent(ANALYTICS_EVENTS.LOGIN, { method });
|
|
81
|
-
},
|
|
82
|
-
register: (method) => {
|
|
83
|
-
trackEvent(ANALYTICS_EVENTS.SIGN_UP, { method });
|
|
84
|
-
},
|
|
85
|
-
logout: () => {
|
|
86
|
-
trackEvent(ANALYTICS_EVENTS.LOGOUT);
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
export const trackEcommerce = {
|
|
90
|
-
viewProduct: (product) => {
|
|
91
|
-
const currency = product.currency ?? ecommerceCurrency;
|
|
92
|
-
trackEvent(ANALYTICS_EVENTS.VIEW_ITEM, {
|
|
93
|
-
currency,
|
|
94
|
-
value: product.price,
|
|
95
|
-
items: [
|
|
96
|
-
{
|
|
97
|
-
item_id: product.id,
|
|
98
|
-
item_name: product.name,
|
|
99
|
-
item_category: product.category,
|
|
100
|
-
price: product.price,
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
});
|
|
104
|
-
},
|
|
105
|
-
addToCart: (product) => {
|
|
106
|
-
const currency = product.currency ?? ecommerceCurrency;
|
|
107
|
-
trackEvent(ANALYTICS_EVENTS.ADD_TO_CART, {
|
|
108
|
-
currency,
|
|
109
|
-
value: product.price * product.quantity,
|
|
110
|
-
items: [
|
|
111
|
-
{
|
|
112
|
-
item_id: product.id,
|
|
113
|
-
item_name: product.name,
|
|
114
|
-
item_category: product.category,
|
|
115
|
-
price: product.price,
|
|
116
|
-
quantity: product.quantity,
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
});
|
|
120
|
-
},
|
|
121
|
-
purchase: (order) => {
|
|
122
|
-
const currency = order.currency ?? ecommerceCurrency;
|
|
123
|
-
trackEvent(ANALYTICS_EVENTS.PURCHASE, {
|
|
124
|
-
transaction_id: order.id,
|
|
125
|
-
currency,
|
|
126
|
-
value: order.total,
|
|
127
|
-
tax: order.tax || 0,
|
|
128
|
-
shipping: order.shipping || 0,
|
|
129
|
-
items: order.items.map((item) => ({
|
|
130
|
-
item_id: item.id,
|
|
131
|
-
item_name: item.name,
|
|
132
|
-
item_category: item.category,
|
|
133
|
-
price: item.price,
|
|
134
|
-
quantity: item.quantity,
|
|
135
|
-
})),
|
|
136
|
-
});
|
|
137
|
-
},
|
|
138
|
-
placeBid: (product) => {
|
|
139
|
-
trackEvent(ANALYTICS_EVENTS.PLACE_BID, {
|
|
140
|
-
product_id: product.id,
|
|
141
|
-
product_name: product.name,
|
|
142
|
-
bid_amount: product.bidAmount,
|
|
143
|
-
});
|
|
144
|
-
},
|
|
145
|
-
winAuction: (product) => {
|
|
146
|
-
trackEvent(ANALYTICS_EVENTS.WIN_AUCTION, {
|
|
147
|
-
product_id: product.id,
|
|
148
|
-
product_name: product.name,
|
|
149
|
-
final_bid: product.finalBid,
|
|
150
|
-
});
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
export const trackContent = {
|
|
154
|
-
search: (searchTerm, resultsCount) => {
|
|
155
|
-
trackEvent(ANALYTICS_EVENTS.SEARCH, {
|
|
156
|
-
search_term: searchTerm,
|
|
157
|
-
results_count: resultsCount,
|
|
158
|
-
});
|
|
159
|
-
},
|
|
160
|
-
viewFaq: (faqId, question) => {
|
|
161
|
-
trackEvent(ANALYTICS_EVENTS.VIEW_FAQ, {
|
|
162
|
-
faq_id: faqId,
|
|
163
|
-
question,
|
|
164
|
-
});
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
export const trackForm = {
|
|
168
|
-
submit: (formName) => {
|
|
169
|
-
trackEvent("form_submit", { form_name: formName });
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
export const trackAdmin = {
|
|
173
|
-
action: (action, target) => {
|
|
174
|
-
trackEvent("admin_action", { action, target });
|
|
175
|
-
},
|
|
176
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { AuthRequestContext, Middleware } from "./types";
|
|
2
|
-
export interface PiiRedactionRule {
|
|
3
|
-
/** Fields to redact from the top-level object or items[] */
|
|
4
|
-
fields: string[];
|
|
5
|
-
/**
|
|
6
|
-
* If `true` (default), replace the value with a masked string like `a***@domain.com`.
|
|
7
|
-
* If `false`, remove the field entirely.
|
|
8
|
-
*/
|
|
9
|
-
mask?: boolean;
|
|
10
|
-
/** Only redact if the caller does NOT have this permission string */
|
|
11
|
-
unless?: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* createPiiRedactorMiddleware — field-level PII redaction for non-owners.
|
|
15
|
-
*
|
|
16
|
-
* Removes or masks declared PII fields from the response body unless the
|
|
17
|
-
* calling user holds the specified permission.
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```ts
|
|
21
|
-
* const userListRedactor = createPiiRedactorMiddleware([
|
|
22
|
-
* { fields: ["email", "phone"], mask: true, unless: "admin:users:pii" },
|
|
23
|
-
* { fields: ["bankAccount"], mask: false, unless: "admin:payouts" },
|
|
24
|
-
* ]);
|
|
25
|
-
*
|
|
26
|
-
* export const GET = createApiMiddleware({
|
|
27
|
-
* permission: "admin:users",
|
|
28
|
-
* middlewares: [userListRedactor],
|
|
29
|
-
* })(async (input, ctx) => { ... });
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
export declare function createPiiRedactorMiddleware(rules: PiiRedactionRule[]): Middleware<AuthRequestContext>;
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { normalizeError } from "../../errors/normalize";
|
|
2
|
-
import { NextResponse } from "next/server";
|
|
3
|
-
function maskPii(value) {
|
|
4
|
-
if (value.includes("@")) {
|
|
5
|
-
const [local, domain] = value.split("@");
|
|
6
|
-
return `${local?.[0] ?? ""}***@${domain}`;
|
|
7
|
-
}
|
|
8
|
-
if (/^\+?\d{7,}$/.test(value)) {
|
|
9
|
-
return value.slice(0, 3) + "***" + value.slice(-2);
|
|
10
|
-
}
|
|
11
|
-
return "***";
|
|
12
|
-
}
|
|
13
|
-
function hasPermission(permissions, required) {
|
|
14
|
-
return permissions.has("*") || permissions.has(required);
|
|
15
|
-
}
|
|
16
|
-
function redactObject(obj, fields, mask) {
|
|
17
|
-
return fields.reduce((acc, field) => {
|
|
18
|
-
if (!(field in acc))
|
|
19
|
-
return acc;
|
|
20
|
-
const current = acc[field];
|
|
21
|
-
const redacted = { ...acc };
|
|
22
|
-
if (mask && typeof current === "string") {
|
|
23
|
-
redacted[field] = maskPii(current);
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
delete redacted[field];
|
|
27
|
-
}
|
|
28
|
-
return redacted;
|
|
29
|
-
}, obj);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* createPiiRedactorMiddleware — field-level PII redaction for non-owners.
|
|
33
|
-
*
|
|
34
|
-
* Removes or masks declared PII fields from the response body unless the
|
|
35
|
-
* calling user holds the specified permission.
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* ```ts
|
|
39
|
-
* const userListRedactor = createPiiRedactorMiddleware([
|
|
40
|
-
* { fields: ["email", "phone"], mask: true, unless: "admin:users:pii" },
|
|
41
|
-
* { fields: ["bankAccount"], mask: false, unless: "admin:payouts" },
|
|
42
|
-
* ]);
|
|
43
|
-
*
|
|
44
|
-
* export const GET = createApiMiddleware({
|
|
45
|
-
* permission: "admin:users",
|
|
46
|
-
* middlewares: [userListRedactor],
|
|
47
|
-
* })(async (input, ctx) => { ... });
|
|
48
|
-
* ```
|
|
49
|
-
*/
|
|
50
|
-
export function createPiiRedactorMiddleware(rules) {
|
|
51
|
-
return async (_req, ctx, next) => {
|
|
52
|
-
const response = await next();
|
|
53
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
54
|
-
if (!contentType.includes("application/json"))
|
|
55
|
-
return response;
|
|
56
|
-
let body;
|
|
57
|
-
try {
|
|
58
|
-
body = (await response.json());
|
|
59
|
-
}
|
|
60
|
-
catch (_err) {
|
|
61
|
-
void normalizeError(_err);
|
|
62
|
-
return response;
|
|
63
|
-
}
|
|
64
|
-
for (const { fields, mask = true, unless } of rules) {
|
|
65
|
-
// Skip redaction if caller has the authorising permission
|
|
66
|
-
if (unless && hasPermission(ctx.user.permissions, unless))
|
|
67
|
-
continue;
|
|
68
|
-
// Handle both paginated { data: [] } and single-object responses
|
|
69
|
-
if (Array.isArray(body.data)) {
|
|
70
|
-
body = {
|
|
71
|
-
...body,
|
|
72
|
-
data: body.data.map((item) => redactObject(item, fields, mask)),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
body = redactObject(body, fields, mask);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return NextResponse.json(body, {
|
|
80
|
-
status: response.status,
|
|
81
|
-
headers: response.headers,
|
|
82
|
-
});
|
|
83
|
-
};
|
|
84
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FirebaseRealtimeRepository<T>
|
|
3
|
-
*
|
|
4
|
-
* Abstract base implementing `IRealtimeRepository<T>` from `@mohasinac/contracts`
|
|
5
|
-
* against Firebase Realtime Database (Admin SDK).
|
|
6
|
-
*
|
|
7
|
-
* Realtime DB does not support Firestore-style queries; `findAll()` loads the
|
|
8
|
-
* entire collection node. Use this only for small, high-churn datasets (e.g.
|
|
9
|
-
* presence, chat, live-bid prices).
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* ```ts
|
|
13
|
-
* class PresenceRepository extends FirebaseRealtimeRepository<PresenceEntry> {
|
|
14
|
-
* constructor() { super("presence"); }
|
|
15
|
-
* }
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
import type { IRealtimeRepository, PagedResult, SieveQuery, WhereOp } from "../../contracts";
|
|
19
|
-
import type { DocumentData } from "firebase-admin/firestore";
|
|
20
|
-
export declare abstract class FirebaseRealtimeRepository<T extends DocumentData> implements IRealtimeRepository<T> {
|
|
21
|
-
protected readonly path: string;
|
|
22
|
-
constructor(rtdbPath: string);
|
|
23
|
-
protected get rtdb(): import("firebase-admin/database").Database;
|
|
24
|
-
protected nodeRef(id?: string): import("@firebase/database-types").Reference;
|
|
25
|
-
findById(id: string): Promise<T | null>;
|
|
26
|
-
findAll(query?: SieveQuery): Promise<PagedResult<T>>;
|
|
27
|
-
findWhere(field: keyof T, _op: WhereOp, value: unknown): Promise<T[]>;
|
|
28
|
-
create(data: Omit<T, "id" | "createdAt" | "updatedAt">): Promise<T>;
|
|
29
|
-
update(id: string, data: Partial<T>): Promise<T>;
|
|
30
|
-
delete(id: string): Promise<void>;
|
|
31
|
-
batchCreate(items: Array<Omit<T, "id" | "createdAt" | "updatedAt">>): Promise<T[]>;
|
|
32
|
-
batchDelete(ids: string[]): Promise<void>;
|
|
33
|
-
/**
|
|
34
|
-
* Subscribe to a single RTDB node.
|
|
35
|
-
* Returns an unsubscribe function.
|
|
36
|
-
*
|
|
37
|
-
* NOTE: The Admin SDK `onValue` callback runs in-process but is not browser-side.
|
|
38
|
-
* For browser real-time, use the client SDK directly.
|
|
39
|
-
*/
|
|
40
|
-
subscribe(id: string, cb: (data: T | null) => void): () => void;
|
|
41
|
-
subscribeWhere(field: keyof T, value: unknown, cb: (items: T[]) => void): () => void;
|
|
42
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FirebaseRealtimeRepository<T>
|
|
3
|
-
*
|
|
4
|
-
* Abstract base implementing `IRealtimeRepository<T>` from `@mohasinac/contracts`
|
|
5
|
-
* against Firebase Realtime Database (Admin SDK).
|
|
6
|
-
*
|
|
7
|
-
* Realtime DB does not support Firestore-style queries; `findAll()` loads the
|
|
8
|
-
* entire collection node. Use this only for small, high-churn datasets (e.g.
|
|
9
|
-
* presence, chat, live-bid prices).
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* ```ts
|
|
13
|
-
* class PresenceRepository extends FirebaseRealtimeRepository<PresenceEntry> {
|
|
14
|
-
* constructor() { super("presence"); }
|
|
15
|
-
* }
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
import { getAdminRealtimeDb } from "./admin";
|
|
19
|
-
export class FirebaseRealtimeRepository {
|
|
20
|
-
constructor(rtdbPath) {
|
|
21
|
-
this.path = rtdbPath;
|
|
22
|
-
}
|
|
23
|
-
get rtdb() {
|
|
24
|
-
return getAdminRealtimeDb();
|
|
25
|
-
}
|
|
26
|
-
nodeRef(id) {
|
|
27
|
-
return id ? this.rtdb.ref(`${this.path}/${id}`) : this.rtdb.ref(this.path);
|
|
28
|
-
}
|
|
29
|
-
// -- IReadRepository --------------------------------------------------------
|
|
30
|
-
async findById(id) {
|
|
31
|
-
const snap = await this.nodeRef(id).get();
|
|
32
|
-
if (!snap.exists())
|
|
33
|
-
return null;
|
|
34
|
-
return { id, ...snap.val() };
|
|
35
|
-
}
|
|
36
|
-
async findAll(query) {
|
|
37
|
-
const snap = await this.nodeRef().get();
|
|
38
|
-
if (!snap.exists()) {
|
|
39
|
-
return {
|
|
40
|
-
data: [],
|
|
41
|
-
total: 0,
|
|
42
|
-
page: 1,
|
|
43
|
-
perPage: query?.perPage ?? 20,
|
|
44
|
-
totalPages: 0,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const raw = snap.val();
|
|
48
|
-
let data = Object.entries(raw).map(([id, val]) => ({ id, ...val }));
|
|
49
|
-
// Basic server-side sort (RTDB has no native multi-field sort)
|
|
50
|
-
if (query?.sort) {
|
|
51
|
-
const key = query.sort;
|
|
52
|
-
const dir = query.order === "desc" ? -1 : 1;
|
|
53
|
-
data = data.slice().sort((a, b) => {
|
|
54
|
-
const av = a[key];
|
|
55
|
-
const bv = b[key];
|
|
56
|
-
if (av === bv)
|
|
57
|
-
return 0;
|
|
58
|
-
return (av < bv ? -1 : 1) * dir;
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
const total = data.length;
|
|
62
|
-
const perPage = Math.max(1, query?.perPage ?? 20);
|
|
63
|
-
const page = Math.max(1, query?.page ?? 1);
|
|
64
|
-
const offset = (page - 1) * perPage;
|
|
65
|
-
const totalPages = total === 0 ? 0 : Math.max(1, Math.ceil(total / perPage));
|
|
66
|
-
return {
|
|
67
|
-
data: data.slice(offset, offset + perPage),
|
|
68
|
-
total,
|
|
69
|
-
page,
|
|
70
|
-
perPage,
|
|
71
|
-
totalPages,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
async findWhere(field, _op, value) {
|
|
75
|
-
// RTDB Admin SDK supports orderByChild + equalTo for == only
|
|
76
|
-
const snap = await this.nodeRef()
|
|
77
|
-
.orderByChild(field)
|
|
78
|
-
.equalTo(value)
|
|
79
|
-
.get();
|
|
80
|
-
if (!snap.exists())
|
|
81
|
-
return [];
|
|
82
|
-
const raw = snap.val();
|
|
83
|
-
return Object.entries(raw).map(([id, val]) => ({ id, ...val }));
|
|
84
|
-
}
|
|
85
|
-
// -- IWriteRepository -------------------------------------------------------
|
|
86
|
-
async create(data) {
|
|
87
|
-
const now = Date.now();
|
|
88
|
-
const payload = { ...data, createdAt: now, updatedAt: now };
|
|
89
|
-
const ref = await this.nodeRef().push(payload);
|
|
90
|
-
return { id: ref.key, ...payload };
|
|
91
|
-
}
|
|
92
|
-
async update(id, data) {
|
|
93
|
-
const payload = { ...data, updatedAt: Date.now() };
|
|
94
|
-
await this.nodeRef(id).update(payload);
|
|
95
|
-
const snap = await this.nodeRef(id).get();
|
|
96
|
-
return { id, ...snap.val() };
|
|
97
|
-
}
|
|
98
|
-
async delete(id) {
|
|
99
|
-
await this.nodeRef(id).remove();
|
|
100
|
-
}
|
|
101
|
-
async batchCreate(items) {
|
|
102
|
-
return Promise.all(items.map((item) => this.create(item)));
|
|
103
|
-
}
|
|
104
|
-
async batchDelete(ids) {
|
|
105
|
-
await Promise.all(ids.map((id) => this.delete(id)));
|
|
106
|
-
}
|
|
107
|
-
// -- IRealtimeRepository ---------------------------------------------------
|
|
108
|
-
/**
|
|
109
|
-
* Subscribe to a single RTDB node.
|
|
110
|
-
* Returns an unsubscribe function.
|
|
111
|
-
*
|
|
112
|
-
* NOTE: The Admin SDK `onValue` callback runs in-process but is not browser-side.
|
|
113
|
-
* For browser real-time, use the client SDK directly.
|
|
114
|
-
*/
|
|
115
|
-
subscribe(id, cb) {
|
|
116
|
-
const ref = this.nodeRef(id);
|
|
117
|
-
const handler = (snap) => {
|
|
118
|
-
cb(snap.exists() ? { id, ...snap.val() } : null);
|
|
119
|
-
};
|
|
120
|
-
ref.on("value", handler);
|
|
121
|
-
return () => ref.off("value", handler);
|
|
122
|
-
}
|
|
123
|
-
subscribeWhere(field, value, cb) {
|
|
124
|
-
const ref = this.nodeRef()
|
|
125
|
-
.orderByChild(field)
|
|
126
|
-
.equalTo(value);
|
|
127
|
-
const handler = (snap) => {
|
|
128
|
-
if (!snap.exists()) {
|
|
129
|
-
cb([]);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
const raw = snap.val();
|
|
133
|
-
cb(Object.entries(raw).map(([id, v]) => ({ id, ...v })));
|
|
134
|
-
};
|
|
135
|
-
ref.on("value", handler);
|
|
136
|
-
return () => ref.off("value", handler);
|
|
137
|
-
}
|
|
138
|
-
}
|