@mohasinac/appkit 3.2.9 → 3.2.11
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/filters/filter-load-options.js +2 -1
- package/dist/_internal/server/features/auth/firebase-identity.d.ts +2 -1
- package/dist/_internal/server/jobs/core/cleanupRtdbEvents.js +4 -1
- package/dist/_internal/shared/listing-types/action-tracker.js +2 -1
- package/dist/features/blog/api/[slug]/route.js +2 -1
- package/dist/features/homepage/lib/live-stats.js +7 -5
- package/dist/features/scams/actions/scam-actions.js +2 -1
- package/dist/providers/storage-firebase/provider.js +109 -55
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* <AsyncFacetSection loadOptions={load} ... />
|
|
11
11
|
*/
|
|
12
12
|
import { apiClient } from "../../../../http/ApiClient";
|
|
13
|
+
import { normalizeError } from "../../../../errors/normalize";
|
|
13
14
|
const PAGE_SIZE = 25;
|
|
14
15
|
function buildUrl(base, q, page, extra) {
|
|
15
16
|
const params = new URLSearchParams({
|
|
@@ -29,7 +30,7 @@ async function fetchPage(url, mapItem) {
|
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
catch (_err) {
|
|
32
|
-
// PaginatedSelect degrades to
|
|
33
|
+
void normalizeError(_err); // PaginatedSelect degrades to empty page; caller can retry by re-typing
|
|
33
34
|
return { items: [], hasMore: false };
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { JsonObject } from "../../../../schemas/types";
|
|
1
2
|
type IdentityOperation = "signInWithPassword" | "signInWithCustomToken";
|
|
2
3
|
/**
|
|
3
4
|
* Calls the Firebase Identity Toolkit REST API with one retry for transient
|
|
@@ -8,5 +9,5 @@ type IdentityOperation = "signInWithPassword" | "signInWithCustomToken";
|
|
|
8
9
|
* into a session cookie. Centralised here so the URL, Content-Type, and retry
|
|
9
10
|
* config are in one place.
|
|
10
11
|
*/
|
|
11
|
-
export declare function callFirebaseIdentityToolkit<T extends
|
|
12
|
+
export declare function callFirebaseIdentityToolkit<T extends JsonObject>(operation: IdentityOperation, body: JsonObject, apiKey: string): Promise<T>;
|
|
12
13
|
export {};
|
|
@@ -17,7 +17,10 @@ export async function runCleanupRtdbEvents(ctx) {
|
|
|
17
17
|
if (staleAuthIds.length > 0) {
|
|
18
18
|
await Promise.all(staleAuthIds.flatMap((id) => [
|
|
19
19
|
rtdb.ref(`auth_events/${id}`).remove(),
|
|
20
|
-
auth.deleteUser(`auth_event_${id}`).catch(
|
|
20
|
+
auth.deleteUser(`auth_event_${id}`).catch((err) => {
|
|
21
|
+
void normalizeError(err);
|
|
22
|
+
ctx.logger.warn("cleanupRtdbEvents: deleteUser skipped — may already be absent", { userId: `auth_event_${id}`, error: err instanceof Error ? err.message : String(err) });
|
|
23
|
+
}),
|
|
21
24
|
]));
|
|
22
25
|
ctx.logger.info("Auth events removed", { count: staleAuthIds.length });
|
|
23
26
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
1
2
|
function defaultSink(event) {
|
|
2
3
|
if (typeof window === "undefined") {
|
|
3
4
|
// SSR / Node — best-effort console log; production servers can swap
|
|
@@ -32,7 +33,7 @@ export const actionTracker = {
|
|
|
32
33
|
const result = currentSink(event);
|
|
33
34
|
// Swallow promise rejections so the caller path is never affected.
|
|
34
35
|
if (result && typeof result.catch === "function") {
|
|
35
|
-
result.catch(
|
|
36
|
+
result.catch((err) => { void normalizeError(err); });
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
catch {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { NextResponse } from "next/server.js";
|
|
12
12
|
import { blogRepository } from "../../repository/blog.repository";
|
|
13
13
|
import { normalizeError } from "../../../../errors/normalize";
|
|
14
|
+
import { safeFireAndForget } from "../../../../utils/safe-fire-forget";
|
|
14
15
|
function toBlogPost(doc) {
|
|
15
16
|
return {
|
|
16
17
|
...doc,
|
|
@@ -28,7 +29,7 @@ export async function GET(_request, context) {
|
|
|
28
29
|
return NextResponse.json({ success: false, error: "Blog post not found" }, { status: 404 });
|
|
29
30
|
}
|
|
30
31
|
// Increment view count fire-and-forget — must not block response
|
|
31
|
-
blogRepository.incrementViews(post.id)
|
|
32
|
+
safeFireAndForget(blogRepository.incrementViews(post.id), "blog: incrementViews");
|
|
32
33
|
// Related posts: same category, latest 3, excluding current
|
|
33
34
|
const related = (await blogRepository.findRelated(post.category, post.id, 3)).map(toBlogPost);
|
|
34
35
|
const body = { post: toBlogPost(post), related };
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { ALLOWED_LIVE_COLLECTIONS } from "../schemas/firestore";
|
|
9
9
|
import { productRepository, storeRepository, userRepository, reviewRepository, orderRepository, } from "../../../repositories";
|
|
10
10
|
import { getAdminDb, getFirestoreCount } from "../../../providers/db-firebase";
|
|
11
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
12
|
+
import { serverLogger } from "../../../monitoring/server-logger";
|
|
11
13
|
/**
|
|
12
14
|
* Fetch live stats for all requested metrics.
|
|
13
15
|
* Preset metrics are deduped so each Firestore query runs at most once.
|
|
@@ -27,22 +29,22 @@ export async function fetchLiveStats(requests) {
|
|
|
27
29
|
if (neededPresets.has("total_listings")) {
|
|
28
30
|
tasks.push(productRepository.count()
|
|
29
31
|
.then((n) => { resolvedPresets.total_listings = String(n); })
|
|
30
|
-
.catch(
|
|
32
|
+
.catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: total_listings query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
|
|
31
33
|
}
|
|
32
34
|
if (neededPresets.has("verified_sellers")) {
|
|
33
35
|
tasks.push(storeRepository.count()
|
|
34
36
|
.then((n) => { resolvedPresets.verified_sellers = String(n); })
|
|
35
|
-
.catch(
|
|
37
|
+
.catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: verified_sellers query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
|
|
36
38
|
}
|
|
37
39
|
if (neededPresets.has("total_buyers")) {
|
|
38
40
|
tasks.push(userRepository.countByRole("user")
|
|
39
41
|
.then((n) => { resolvedPresets.total_buyers = String(n); })
|
|
40
|
-
.catch(
|
|
42
|
+
.catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: total_buyers query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
|
|
41
43
|
}
|
|
42
44
|
if (neededPresets.has("total_orders")) {
|
|
43
45
|
tasks.push(orderRepository.count()
|
|
44
46
|
.then((n) => { resolvedPresets.total_orders = String(n); })
|
|
45
|
-
.catch(
|
|
47
|
+
.catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: total_orders query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
|
|
46
48
|
}
|
|
47
49
|
if (neededPresets.has("total_reviews") || neededPresets.has("platform_rating")) {
|
|
48
50
|
tasks.push(reviewRepository.findAll()
|
|
@@ -57,7 +59,7 @@ export async function fetchLiveStats(requests) {
|
|
|
57
59
|
resolvedPresets.platform_rating = avg.toFixed(1) + "★";
|
|
58
60
|
}
|
|
59
61
|
})
|
|
60
|
-
.catch(
|
|
62
|
+
.catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: reviews/rating query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
|
|
61
63
|
}
|
|
62
64
|
// --- Collection-query metrics ------------------------------------------------
|
|
63
65
|
const queryRequests = requests.filter((r) => r.source === "live-collection" && r.collectionQuery !== undefined);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
|
|
3
3
|
import { scammerRepository } from "../repository/scammer.repository";
|
|
4
|
+
import { safeFireAndForget } from "../../../utils/safe-fire-forget";
|
|
4
5
|
/**
|
|
5
6
|
* List verified scammer profiles for the public registry page.
|
|
6
7
|
* Accepts sieve-style searchParams from Next.js page props.
|
|
@@ -43,7 +44,7 @@ export async function getPublicScammerById(id) {
|
|
|
43
44
|
if (!doc || doc.status !== "verified")
|
|
44
45
|
return null;
|
|
45
46
|
// Increment views — non-blocking.
|
|
46
|
-
scammerRepository.incrementViews(doc.id)
|
|
47
|
+
safeFireAndForget(scammerRepository.incrementViews(doc.id), "scam: incrementViews");
|
|
47
48
|
return doc;
|
|
48
49
|
}
|
|
49
50
|
/**
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
import { getAdminStorageLite } from "../db-firebase/admin-storage-lite";
|
|
23
|
+
import { withRetry } from "../../http/retry";
|
|
24
|
+
import { normalizeError } from "../../errors/normalize";
|
|
25
|
+
import { serverLogger } from "../../monitoring/server-logger";
|
|
23
26
|
function getBucket() {
|
|
24
27
|
const bucketName = process.env.FIREBASE_ADMIN_STORAGE_BUCKET?.trim() ??
|
|
25
28
|
process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET?.trim();
|
|
@@ -29,75 +32,126 @@ function getBucket() {
|
|
|
29
32
|
}
|
|
30
33
|
export const firebaseStorageProvider = {
|
|
31
34
|
async upload(file, storagePath, options) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
35
|
+
try {
|
|
36
|
+
const bucket = getBucket();
|
|
37
|
+
const fileRef = bucket.file(storagePath);
|
|
38
|
+
await withRetry(() => fileRef.save(file, {
|
|
39
|
+
metadata: {
|
|
40
|
+
contentType: options?.contentType ?? "application/octet-stream",
|
|
41
|
+
cacheControl: options?.cacheControl ?? "public, max-age=31536000",
|
|
42
|
+
metadata: options?.metadata ?? {},
|
|
43
|
+
},
|
|
44
|
+
public: options?.isPublic !== false,
|
|
45
|
+
}), 2, 300);
|
|
46
|
+
const [metadata] = await withRetry(() => fileRef.getMetadata(), 2, 300);
|
|
47
|
+
const url = options?.isPublic !== false
|
|
48
|
+
? `https://storage.googleapis.com/${bucket.name}/${storagePath}`
|
|
49
|
+
: await withRetry(() => fileRef
|
|
50
|
+
.getSignedUrl({
|
|
51
|
+
action: "read",
|
|
52
|
+
expires: Date.now() + 60 * 60 * 1000,
|
|
53
|
+
})
|
|
54
|
+
.then(([u]) => u), 2, 300);
|
|
55
|
+
return {
|
|
56
|
+
path: storagePath,
|
|
57
|
+
url,
|
|
58
|
+
contentType: metadata.contentType,
|
|
59
|
+
size: Number(metadata.size),
|
|
60
|
+
updatedAt: metadata.updated,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
void normalizeError(err);
|
|
65
|
+
serverLogger.error("storage-provider: upload failed", {
|
|
66
|
+
storagePath,
|
|
67
|
+
error: err instanceof Error ? err.message : String(err),
|
|
68
|
+
});
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
58
71
|
},
|
|
59
72
|
async delete(storagePath) {
|
|
60
|
-
|
|
73
|
+
try {
|
|
74
|
+
await withRetry(() => getBucket().file(storagePath).delete({ ignoreNotFound: true }), 2, 300);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
void normalizeError(err);
|
|
78
|
+
serverLogger.error("storage-provider: delete failed", {
|
|
79
|
+
storagePath,
|
|
80
|
+
error: err instanceof Error ? err.message : String(err),
|
|
81
|
+
});
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
61
84
|
},
|
|
62
85
|
getPublicUrl(storagePath) {
|
|
63
86
|
const bucket = getBucket();
|
|
64
87
|
return `https://storage.googleapis.com/${bucket.name}/${storagePath}`;
|
|
65
88
|
},
|
|
66
89
|
async getSignedUrl(storagePath, expiresInSeconds = 3600) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
try {
|
|
91
|
+
const [url] = await withRetry(() => getBucket()
|
|
92
|
+
.file(storagePath)
|
|
93
|
+
.getSignedUrl({
|
|
94
|
+
action: "read",
|
|
95
|
+
expires: Date.now() + expiresInSeconds * 1000,
|
|
96
|
+
}), 2, 300);
|
|
97
|
+
return url;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
void normalizeError(err);
|
|
101
|
+
serverLogger.error("storage-provider: getSignedUrl failed", {
|
|
102
|
+
storagePath,
|
|
103
|
+
error: err instanceof Error ? err.message : String(err),
|
|
104
|
+
});
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
74
107
|
},
|
|
75
108
|
async copy(from, to) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
path: to,
|
|
83
|
-
url: `https://storage.googleapis.com/${bucket.name}/${to}`,
|
|
84
|
-
contentType: metadata.contentType,
|
|
85
|
-
size: Number(metadata.size),
|
|
86
|
-
updatedAt: metadata.updated,
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
|
-
async list(prefix) {
|
|
90
|
-
const bucket = getBucket();
|
|
91
|
-
const [files] = await bucket.getFiles({ prefix });
|
|
92
|
-
return Promise.all(files.map(async (f) => {
|
|
93
|
-
const [metadata] = await f.getMetadata();
|
|
109
|
+
try {
|
|
110
|
+
const bucket = getBucket();
|
|
111
|
+
const src = bucket.file(from);
|
|
112
|
+
const dest = bucket.file(to);
|
|
113
|
+
await withRetry(() => src.copy(dest), 2, 300);
|
|
114
|
+
const [metadata] = await withRetry(() => dest.getMetadata(), 2, 300);
|
|
94
115
|
return {
|
|
95
|
-
path:
|
|
96
|
-
url: `https://storage.googleapis.com/${bucket.name}/${
|
|
116
|
+
path: to,
|
|
117
|
+
url: `https://storage.googleapis.com/${bucket.name}/${to}`,
|
|
97
118
|
contentType: metadata.contentType,
|
|
98
119
|
size: Number(metadata.size),
|
|
99
120
|
updatedAt: metadata.updated,
|
|
100
121
|
};
|
|
101
|
-
}
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
void normalizeError(err);
|
|
125
|
+
serverLogger.error("storage-provider: copy failed", {
|
|
126
|
+
from,
|
|
127
|
+
to,
|
|
128
|
+
error: err instanceof Error ? err.message : String(err),
|
|
129
|
+
});
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
async list(prefix) {
|
|
134
|
+
try {
|
|
135
|
+
const bucket = getBucket();
|
|
136
|
+
const [files] = await withRetry(() => bucket.getFiles({ prefix }), 2, 300);
|
|
137
|
+
return Promise.all(files.map(async (f) => {
|
|
138
|
+
const [metadata] = await withRetry(() => f.getMetadata(), 2, 300);
|
|
139
|
+
return {
|
|
140
|
+
path: f.name,
|
|
141
|
+
url: `https://storage.googleapis.com/${bucket.name}/${f.name}`,
|
|
142
|
+
contentType: metadata.contentType,
|
|
143
|
+
size: Number(metadata.size),
|
|
144
|
+
updatedAt: metadata.updated,
|
|
145
|
+
};
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
void normalizeError(err);
|
|
150
|
+
serverLogger.error("storage-provider: list failed", {
|
|
151
|
+
prefix,
|
|
152
|
+
error: err instanceof Error ? err.message : String(err),
|
|
153
|
+
});
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
102
156
|
},
|
|
103
157
|
};
|