opticedge-cloud-utils 1.1.30 → 1.1.32
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/db/mongo.d.ts +1 -0
- package/dist/db/mongo.js +9 -0
- package/dist/db/mongo2.d.ts +1 -0
- package/dist/db/mongo2.js +8 -0
- package/dist/db/mongo3.d.ts +2 -0
- package/dist/db/mongo3.js +15 -0
- package/dist/pricecharting.d.ts +1 -0
- package/dist/pricecharting.js +82 -0
- package/package.json +46 -46
- package/src/db/mongo.ts +12 -2
- package/src/db/mongo2.ts +10 -1
- package/src/db/mongo3.ts +18 -0
- package/src/pricecharting.ts +101 -0
- package/tests/db/mongo.test.ts +32 -12
- package/tests/db/mongo2.test.ts +30 -15
- package/tests/db/mongo3.test.ts +37 -16
- package/tests/pricecharting.test.ts +139 -2
package/dist/db/mongo.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ import { Db, Collection, Document } from 'mongodb';
|
|
|
2
2
|
export declare function connectToMongo(uri: string, dbName: string): Promise<void>;
|
|
3
3
|
export declare function getDb(): Db;
|
|
4
4
|
export declare function getCollection<T extends Document = Document>(name: string): Collection<T>;
|
|
5
|
+
export declare function closeMongo(): Promise<void>;
|
package/dist/db/mongo.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.connectToMongo = connectToMongo;
|
|
4
4
|
exports.getDb = getDb;
|
|
5
5
|
exports.getCollection = getCollection;
|
|
6
|
+
exports.closeMongo = closeMongo;
|
|
6
7
|
const mongodb_1 = require("mongodb");
|
|
7
8
|
let client;
|
|
8
9
|
let db;
|
|
@@ -29,3 +30,11 @@ function getCollection(name) {
|
|
|
29
30
|
throw new Error('Mongo not initialized');
|
|
30
31
|
return db.collection(name);
|
|
31
32
|
}
|
|
33
|
+
async function closeMongo() {
|
|
34
|
+
if (!client)
|
|
35
|
+
return;
|
|
36
|
+
await client.close();
|
|
37
|
+
client = undefined;
|
|
38
|
+
db = undefined;
|
|
39
|
+
connectPromise = undefined;
|
|
40
|
+
}
|
package/dist/db/mongo2.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ import { Db, Collection, Document } from 'mongodb';
|
|
|
2
2
|
export declare function connectToMongo2(uri: string): Promise<void>;
|
|
3
3
|
export declare function getDb2(dbName: string): Db;
|
|
4
4
|
export declare function getCollection2<T extends Document = Document>(dbName: string, collectionName: string): Collection<T>;
|
|
5
|
+
export declare function closeMongo2(): Promise<void>;
|
package/dist/db/mongo2.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.connectToMongo2 = connectToMongo2;
|
|
4
4
|
exports.getDb2 = getDb2;
|
|
5
5
|
exports.getCollection2 = getCollection2;
|
|
6
|
+
exports.closeMongo2 = closeMongo2;
|
|
6
7
|
// Multi DB safe util
|
|
7
8
|
const mongodb_1 = require("mongodb");
|
|
8
9
|
let client;
|
|
@@ -26,3 +27,10 @@ function getDb2(dbName) {
|
|
|
26
27
|
function getCollection2(dbName, collectionName) {
|
|
27
28
|
return getDb2(dbName).collection(collectionName);
|
|
28
29
|
}
|
|
30
|
+
async function closeMongo2() {
|
|
31
|
+
if (!client)
|
|
32
|
+
return;
|
|
33
|
+
await client.close();
|
|
34
|
+
client = undefined;
|
|
35
|
+
connectPromise = undefined;
|
|
36
|
+
}
|
package/dist/db/mongo3.d.ts
CHANGED
|
@@ -2,5 +2,7 @@ import { MongoClient, Db, Collection, Document } from 'mongodb';
|
|
|
2
2
|
export declare function connectToMongo3(uri: string): Promise<MongoClient>;
|
|
3
3
|
export declare function getDb3(uri: string, dbName: string): Db;
|
|
4
4
|
export declare function getCollection3<T extends Document = Document>(uri: string, dbName: string, collectionName: string): Collection<T>;
|
|
5
|
+
export declare function closeMongo3(uri: string): Promise<void>;
|
|
6
|
+
export declare function closeAllMongo3(): Promise<void>;
|
|
5
7
|
export declare const __mongoClients: Map<string, MongoClient>;
|
|
6
8
|
export declare const __connectPromises: Map<string, Promise<void>>;
|
package/dist/db/mongo3.js
CHANGED
|
@@ -4,6 +4,8 @@ exports.__connectPromises = exports.__mongoClients = void 0;
|
|
|
4
4
|
exports.connectToMongo3 = connectToMongo3;
|
|
5
5
|
exports.getDb3 = getDb3;
|
|
6
6
|
exports.getCollection3 = getCollection3;
|
|
7
|
+
exports.closeMongo3 = closeMongo3;
|
|
8
|
+
exports.closeAllMongo3 = closeAllMongo3;
|
|
7
9
|
// Multi cluster safe util
|
|
8
10
|
const mongodb_1 = require("mongodb");
|
|
9
11
|
const clients = new Map();
|
|
@@ -30,5 +32,18 @@ function getDb3(uri, dbName) {
|
|
|
30
32
|
function getCollection3(uri, dbName, collectionName) {
|
|
31
33
|
return getDb3(uri, dbName).collection(collectionName);
|
|
32
34
|
}
|
|
35
|
+
async function closeMongo3(uri) {
|
|
36
|
+
const client = clients.get(uri);
|
|
37
|
+
if (client) {
|
|
38
|
+
await client.close();
|
|
39
|
+
}
|
|
40
|
+
clients.delete(uri);
|
|
41
|
+
connectPromises.delete(uri);
|
|
42
|
+
}
|
|
43
|
+
async function closeAllMongo3() {
|
|
44
|
+
await Promise.all(Array.from(clients.values()).map(client => client.close()));
|
|
45
|
+
clients.clear();
|
|
46
|
+
connectPromises.clear();
|
|
47
|
+
}
|
|
33
48
|
exports.__mongoClients = clients;
|
|
34
49
|
exports.__connectPromises = connectPromises;
|
package/dist/pricecharting.d.ts
CHANGED
|
@@ -15,4 +15,5 @@ type CardParts = {
|
|
|
15
15
|
* 3. If no valid number found — returns `{ name: original, number: null }`.
|
|
16
16
|
*/
|
|
17
17
|
export declare function splitNameAndNumber(input: string, realNumber?: string | null): CardParts;
|
|
18
|
+
export declare function productExists(productId: number, token: string, fetchTimeoutMs: number): Promise<boolean | null>;
|
|
18
19
|
export {};
|
package/dist/pricecharting.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// src/pricecharting.ts
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
4
|
exports.splitNameAndNumber = splitNameAndNumber;
|
|
5
|
+
exports.productExists = productExists;
|
|
6
|
+
const logger_1 = require("./logger");
|
|
4
7
|
const regex_1 = require("./regex");
|
|
8
|
+
const PRODUCT_NOT_FOUND_MESSAGE = 'no such product';
|
|
9
|
+
const PRODUCT_API_URL = 'https://www.pricecharting.com/api/product';
|
|
5
10
|
/**
|
|
6
11
|
* Split a card label into `{ name, number }`.
|
|
7
12
|
*
|
|
@@ -48,3 +53,80 @@ function splitNameAndNumber(input, realNumber) {
|
|
|
48
53
|
return { name: s, number: null };
|
|
49
54
|
return { name: namePart, number: numberPart };
|
|
50
55
|
}
|
|
56
|
+
async function productExists(productId, token, fetchTimeoutMs) {
|
|
57
|
+
if (!Number.isInteger(productId) || productId <= 0) {
|
|
58
|
+
(0, logger_1.log)(logger_1.LogLevel.WARNING, '[productExists] invalid productId', { productId });
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (!token?.trim()) {
|
|
62
|
+
(0, logger_1.log)(logger_1.LogLevel.WARNING, '[productExists] missing token', { productId });
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (!Number.isFinite(fetchTimeoutMs) || fetchTimeoutMs < 1000) {
|
|
66
|
+
(0, logger_1.log)(logger_1.LogLevel.WARNING, '[productExists] invalid fetch timeout', {
|
|
67
|
+
productId,
|
|
68
|
+
fetchTimeoutMs
|
|
69
|
+
});
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const params = new URLSearchParams({
|
|
73
|
+
t: token,
|
|
74
|
+
id: String(productId)
|
|
75
|
+
});
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timeout = setTimeout(() => controller.abort(), fetchTimeoutMs);
|
|
78
|
+
try {
|
|
79
|
+
const resp = await fetch(`${PRODUCT_API_URL}?${params.toString()}`, {
|
|
80
|
+
method: 'GET',
|
|
81
|
+
signal: controller.signal
|
|
82
|
+
});
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
if (resp.status === 404)
|
|
85
|
+
return false;
|
|
86
|
+
if (resp.status !== 200) {
|
|
87
|
+
(0, logger_1.log)(logger_1.LogLevel.WARNING, '[productExists] unexpected status code', {
|
|
88
|
+
productId,
|
|
89
|
+
statusCode: resp.status
|
|
90
|
+
});
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
let data;
|
|
94
|
+
try {
|
|
95
|
+
data = await resp.json();
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
(0, logger_1.log)(logger_1.LogLevel.WARNING, '[productExists] failed to parse JSON', {
|
|
99
|
+
productId,
|
|
100
|
+
traceback: err instanceof Error ? err.stack : String(err)
|
|
101
|
+
});
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (typeof data !== 'object' || data === null) {
|
|
105
|
+
(0, logger_1.log)(logger_1.LogLevel.WARNING, '[productExists] unexpected response body', {
|
|
106
|
+
productId,
|
|
107
|
+
bodyType: typeof data
|
|
108
|
+
});
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const status = (data.status || '').toLowerCase();
|
|
112
|
+
const errText = ((data.error || '') + ' ' + (data['error-message'] || '')).toLowerCase();
|
|
113
|
+
if (status === 'error') {
|
|
114
|
+
if (errText.includes(PRODUCT_NOT_FOUND_MESSAGE.toLowerCase()))
|
|
115
|
+
return false;
|
|
116
|
+
(0, logger_1.log)(logger_1.LogLevel.WARNING, '[productExists] API returned error', { productId, error: errText });
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
catch (unknownErr) {
|
|
122
|
+
const err = unknownErr instanceof Error ? unknownErr : new Error(String(unknownErr));
|
|
123
|
+
(0, logger_1.log)(logger_1.LogLevel.WARNING, '[productExists] request failed', {
|
|
124
|
+
productId,
|
|
125
|
+
traceback: err.stack ?? err.message
|
|
126
|
+
});
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
}
|
|
132
|
+
}
|
package/package.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "opticedge-cloud-utils",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Common utilities for cloud functions",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"clean": "rimraf dist coverage *.tsbuildinfo",
|
|
9
|
-
"build": "npm run clean && tsc",
|
|
10
|
-
"prepare": "npm run build",
|
|
11
|
-
"test": "jest --coverage",
|
|
12
|
-
"lint": "eslint . --ext .ts --fix",
|
|
13
|
-
"format": "prettier --write .",
|
|
14
|
-
"fix": "npm run lint && npm run format"
|
|
15
|
-
},
|
|
16
|
-
"author": "Evans Musonda",
|
|
17
|
-
"license": "MIT",
|
|
18
|
-
"dependencies": {
|
|
19
|
-
"@google-cloud/pubsub": "^5.2.0",
|
|
20
|
-
"@google-cloud/secret-manager": "^6.0.1",
|
|
21
|
-
"@google-cloud/tasks": "^6.1.0",
|
|
22
|
-
"axios": "^1.10.0"
|
|
23
|
-
},
|
|
24
|
-
"devDependencies": {
|
|
25
|
-
"@google-cloud/functions-framework": "*",
|
|
26
|
-
"@types/jest": "^29.5.14",
|
|
27
|
-
"@types/node": "^22.15.23",
|
|
28
|
-
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
|
29
|
-
"@typescript-eslint/parser": "^8.33.0",
|
|
30
|
-
"eslint": "^8.57.1",
|
|
31
|
-
"eslint-config-prettier": "^10.1.5",
|
|
32
|
-
"eslint-plugin-jest": "^28.11.1",
|
|
33
|
-
"eslint-plugin-prettier": "^5.4.0",
|
|
34
|
-
"google-auth-library": "*",
|
|
35
|
-
"jest": "^29.7.0",
|
|
36
|
-
"mongodb": "*",
|
|
37
|
-
"prettier": "^3.5.3",
|
|
38
|
-
"ts-jest": "^29.3.4",
|
|
39
|
-
"typescript": "^5.8.3"
|
|
40
|
-
},
|
|
41
|
-
"peerDependencies": {
|
|
42
|
-
"@google-cloud/functions-framework": "^5.0.2",
|
|
43
|
-
"google-auth-library": "^10.6.2",
|
|
44
|
-
"mongodb": "^7.1.1"
|
|
45
|
-
}
|
|
46
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "opticedge-cloud-utils",
|
|
3
|
+
"version": "1.1.32",
|
|
4
|
+
"description": "Common utilities for cloud functions",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"clean": "rimraf dist coverage *.tsbuildinfo",
|
|
9
|
+
"build": "npm run clean && tsc",
|
|
10
|
+
"prepare": "npm run build",
|
|
11
|
+
"test": "jest --coverage",
|
|
12
|
+
"lint": "eslint . --ext .ts --fix",
|
|
13
|
+
"format": "prettier --write .",
|
|
14
|
+
"fix": "npm run lint && npm run format"
|
|
15
|
+
},
|
|
16
|
+
"author": "Evans Musonda",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@google-cloud/pubsub": "^5.2.0",
|
|
20
|
+
"@google-cloud/secret-manager": "^6.0.1",
|
|
21
|
+
"@google-cloud/tasks": "^6.1.0",
|
|
22
|
+
"axios": "^1.10.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@google-cloud/functions-framework": "*",
|
|
26
|
+
"@types/jest": "^29.5.14",
|
|
27
|
+
"@types/node": "^22.15.23",
|
|
28
|
+
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
|
29
|
+
"@typescript-eslint/parser": "^8.33.0",
|
|
30
|
+
"eslint": "^8.57.1",
|
|
31
|
+
"eslint-config-prettier": "^10.1.5",
|
|
32
|
+
"eslint-plugin-jest": "^28.11.1",
|
|
33
|
+
"eslint-plugin-prettier": "^5.4.0",
|
|
34
|
+
"google-auth-library": "*",
|
|
35
|
+
"jest": "^29.7.0",
|
|
36
|
+
"mongodb": "*",
|
|
37
|
+
"prettier": "^3.5.3",
|
|
38
|
+
"ts-jest": "^29.3.4",
|
|
39
|
+
"typescript": "^5.8.3"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@google-cloud/functions-framework": "^5.0.2",
|
|
43
|
+
"google-auth-library": "^10.6.2",
|
|
44
|
+
"mongodb": "^7.1.1"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/db/mongo.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MongoClient, Db, Collection, Document } from 'mongodb'
|
|
2
2
|
|
|
3
|
-
let client: MongoClient
|
|
4
|
-
let db: Db
|
|
3
|
+
let client: MongoClient | undefined
|
|
4
|
+
let db: Db | undefined
|
|
5
5
|
let connectPromise: Promise<void> | undefined
|
|
6
6
|
|
|
7
7
|
export async function connectToMongo(uri: string, dbName: string) {
|
|
@@ -27,3 +27,13 @@ export function getCollection<T extends Document = Document>(name: string): Coll
|
|
|
27
27
|
if (!db) throw new Error('Mongo not initialized')
|
|
28
28
|
return db.collection<T>(name)
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
export async function closeMongo(): Promise<void> {
|
|
32
|
+
if (!client) return
|
|
33
|
+
|
|
34
|
+
await client.close()
|
|
35
|
+
|
|
36
|
+
client = undefined
|
|
37
|
+
db = undefined
|
|
38
|
+
connectPromise = undefined
|
|
39
|
+
}
|
package/src/db/mongo2.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Multi DB safe util
|
|
2
2
|
import { MongoClient, Db, Collection, Document } from 'mongodb'
|
|
3
3
|
|
|
4
|
-
let client: MongoClient
|
|
4
|
+
let client: MongoClient | undefined
|
|
5
5
|
let connectPromise: Promise<void> | undefined
|
|
6
6
|
|
|
7
7
|
export async function connectToMongo2(uri: string) {
|
|
@@ -28,3 +28,12 @@ export function getCollection2<T extends Document = Document>(
|
|
|
28
28
|
): Collection<T> {
|
|
29
29
|
return getDb2(dbName).collection<T>(collectionName)
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
export async function closeMongo2(): Promise<void> {
|
|
33
|
+
if (!client) return
|
|
34
|
+
|
|
35
|
+
await client.close()
|
|
36
|
+
|
|
37
|
+
client = undefined
|
|
38
|
+
connectPromise = undefined
|
|
39
|
+
}
|
package/src/db/mongo3.ts
CHANGED
|
@@ -37,5 +37,23 @@ export function getCollection3<T extends Document = Document>(
|
|
|
37
37
|
return getDb3(uri, dbName).collection<T>(collectionName)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export async function closeMongo3(uri: string): Promise<void> {
|
|
41
|
+
const client = clients.get(uri)
|
|
42
|
+
|
|
43
|
+
if (client) {
|
|
44
|
+
await client.close()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clients.delete(uri)
|
|
48
|
+
connectPromises.delete(uri)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function closeAllMongo3(): Promise<void> {
|
|
52
|
+
await Promise.all(Array.from(clients.values()).map(client => client.close()))
|
|
53
|
+
|
|
54
|
+
clients.clear()
|
|
55
|
+
connectPromises.clear()
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
export const __mongoClients = clients
|
|
41
59
|
export const __connectPromises = connectPromises
|
package/src/pricecharting.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
// src/pricecharting.ts
|
|
2
|
+
|
|
3
|
+
import { log, LogLevel } from './logger'
|
|
1
4
|
import { escapeForRegex } from './regex'
|
|
2
5
|
|
|
3
6
|
type CardParts = { name: string; number: string | null }
|
|
4
7
|
|
|
8
|
+
interface ProductExistsResponse {
|
|
9
|
+
status?: string
|
|
10
|
+
error?: string
|
|
11
|
+
'error-message'?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PRODUCT_NOT_FOUND_MESSAGE = 'no such product'
|
|
15
|
+
const PRODUCT_API_URL = 'https://www.pricecharting.com/api/product'
|
|
16
|
+
|
|
5
17
|
/**
|
|
6
18
|
* Split a card label into `{ name, number }`.
|
|
7
19
|
*
|
|
@@ -49,3 +61,92 @@ export function splitNameAndNumber(input: string, realNumber?: string | null): C
|
|
|
49
61
|
|
|
50
62
|
return { name: namePart, number: numberPart }
|
|
51
63
|
}
|
|
64
|
+
|
|
65
|
+
export async function productExists(
|
|
66
|
+
productId: number,
|
|
67
|
+
token: string,
|
|
68
|
+
fetchTimeoutMs: number
|
|
69
|
+
): Promise<boolean | null> {
|
|
70
|
+
if (!Number.isInteger(productId) || productId <= 0) {
|
|
71
|
+
log(LogLevel.WARNING, '[productExists] invalid productId', { productId })
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!token?.trim()) {
|
|
76
|
+
log(LogLevel.WARNING, '[productExists] missing token', { productId })
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!Number.isFinite(fetchTimeoutMs) || fetchTimeoutMs < 1000) {
|
|
81
|
+
log(LogLevel.WARNING, '[productExists] invalid fetch timeout', {
|
|
82
|
+
productId,
|
|
83
|
+
fetchTimeoutMs
|
|
84
|
+
})
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const params = new URLSearchParams({
|
|
89
|
+
t: token,
|
|
90
|
+
id: String(productId)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const controller = new AbortController()
|
|
94
|
+
const timeout = setTimeout(() => controller.abort(), fetchTimeoutMs)
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const resp = await fetch(`${PRODUCT_API_URL}?${params.toString()}`, {
|
|
98
|
+
method: 'GET',
|
|
99
|
+
signal: controller.signal
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
clearTimeout(timeout)
|
|
103
|
+
|
|
104
|
+
if (resp.status === 404) return false
|
|
105
|
+
if (resp.status !== 200) {
|
|
106
|
+
log(LogLevel.WARNING, '[productExists] unexpected status code', {
|
|
107
|
+
productId,
|
|
108
|
+
statusCode: resp.status
|
|
109
|
+
})
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let data: ProductExistsResponse
|
|
114
|
+
try {
|
|
115
|
+
data = await resp.json()
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log(LogLevel.WARNING, '[productExists] failed to parse JSON', {
|
|
118
|
+
productId,
|
|
119
|
+
traceback: err instanceof Error ? err.stack : String(err)
|
|
120
|
+
})
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof data !== 'object' || data === null) {
|
|
125
|
+
log(LogLevel.WARNING, '[productExists] unexpected response body', {
|
|
126
|
+
productId,
|
|
127
|
+
bodyType: typeof data
|
|
128
|
+
})
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const status = (data.status || '').toLowerCase()
|
|
133
|
+
const errText = ((data.error || '') + ' ' + (data['error-message'] || '')).toLowerCase()
|
|
134
|
+
|
|
135
|
+
if (status === 'error') {
|
|
136
|
+
if (errText.includes(PRODUCT_NOT_FOUND_MESSAGE.toLowerCase())) return false
|
|
137
|
+
log(LogLevel.WARNING, '[productExists] API returned error', { productId, error: errText })
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return true
|
|
142
|
+
} catch (unknownErr: unknown) {
|
|
143
|
+
const err = unknownErr instanceof Error ? unknownErr : new Error(String(unknownErr))
|
|
144
|
+
log(LogLevel.WARNING, '[productExists] request failed', {
|
|
145
|
+
productId,
|
|
146
|
+
traceback: err.stack ?? err.message
|
|
147
|
+
})
|
|
148
|
+
return null
|
|
149
|
+
} finally {
|
|
150
|
+
clearTimeout(timeout)
|
|
151
|
+
}
|
|
152
|
+
}
|
package/tests/db/mongo.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { connectToMongo, getCollection, getDb } from '../../src/db/mongo'
|
|
1
|
+
import { connectToMongo, getCollection, getDb, closeMongo } from '../../src/db/mongo'
|
|
2
2
|
import { MongoClient, Db } from 'mongodb'
|
|
3
3
|
|
|
4
4
|
jest.mock('mongodb')
|
|
@@ -8,12 +8,17 @@ const mockDb = {
|
|
|
8
8
|
} as unknown as Db
|
|
9
9
|
|
|
10
10
|
const mockConnect = jest.fn()
|
|
11
|
+
const mockClose = jest.fn()
|
|
12
|
+
|
|
11
13
|
const mockClient = {
|
|
12
14
|
connect: mockConnect,
|
|
15
|
+
close: mockClose,
|
|
13
16
|
db: jest.fn().mockReturnValue(mockDb)
|
|
14
17
|
} as unknown as MongoClient
|
|
15
18
|
|
|
16
|
-
beforeEach(() => {
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await closeMongo()
|
|
21
|
+
|
|
17
22
|
jest.clearAllMocks()
|
|
18
23
|
;(MongoClient as unknown as jest.Mock).mockImplementation(() => mockClient)
|
|
19
24
|
})
|
|
@@ -22,10 +27,19 @@ describe('Mongo Utils', () => {
|
|
|
22
27
|
it('should connect to MongoDB and store client/db', async () => {
|
|
23
28
|
await connectToMongo('mongo-uri', 'test-db')
|
|
24
29
|
|
|
30
|
+
expect(MongoClient).toHaveBeenCalledWith('mongo-uri')
|
|
25
31
|
expect(mockConnect).toHaveBeenCalled()
|
|
26
32
|
expect(mockClient.db).toHaveBeenCalledWith('test-db')
|
|
27
33
|
})
|
|
28
34
|
|
|
35
|
+
it('should not reconnect if db is already initialized', async () => {
|
|
36
|
+
await connectToMongo('mongo-uri', 'test-db')
|
|
37
|
+
await connectToMongo('mongo-uri', 'test-db')
|
|
38
|
+
|
|
39
|
+
expect(MongoClient).toHaveBeenCalledTimes(1)
|
|
40
|
+
expect(mockConnect).toHaveBeenCalledTimes(1)
|
|
41
|
+
})
|
|
42
|
+
|
|
29
43
|
it('should return a collection when db is initialized', async () => {
|
|
30
44
|
await connectToMongo('mongo-uri', 'test-db')
|
|
31
45
|
getCollection('users')
|
|
@@ -35,17 +49,23 @@ describe('Mongo Utils', () => {
|
|
|
35
49
|
|
|
36
50
|
it('getDb should return the initialized Db instance', async () => {
|
|
37
51
|
await connectToMongo('mongo-uri', 'test-db')
|
|
38
|
-
|
|
39
|
-
expect(
|
|
52
|
+
|
|
53
|
+
expect(getDb()).toBe(mockDb)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should throw error if db is not initialized', async () => {
|
|
57
|
+
await closeMongo()
|
|
58
|
+
|
|
59
|
+
expect(() => getCollection('users')).toThrow(/Mongo not initialized/)
|
|
60
|
+
expect(() => getDb()).toThrow(/Mongo not initialized/)
|
|
40
61
|
})
|
|
41
62
|
|
|
42
|
-
it('should
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
63
|
+
it('should close Mongo and reset state', async () => {
|
|
64
|
+
await connectToMongo('mongo-uri', 'test-db')
|
|
65
|
+
await closeMongo()
|
|
66
|
+
|
|
67
|
+
expect(mockClose).toHaveBeenCalled()
|
|
68
|
+
|
|
69
|
+
expect(() => getDb()).toThrow(/Mongo not initialized/)
|
|
50
70
|
})
|
|
51
71
|
})
|
package/tests/db/mongo2.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { connectToMongo2, getDb2, getCollection2 } from '../../src/db/mongo2'
|
|
1
|
+
import { connectToMongo2, getDb2, getCollection2, closeMongo2 } from '../../src/db/mongo2'
|
|
2
2
|
import { MongoClient, Db } from 'mongodb'
|
|
3
3
|
|
|
4
4
|
jest.mock('mongodb')
|
|
@@ -8,12 +8,17 @@ const mockDb = {
|
|
|
8
8
|
} as unknown as Db
|
|
9
9
|
|
|
10
10
|
const mockConnect = jest.fn()
|
|
11
|
+
const mockClose = jest.fn()
|
|
12
|
+
|
|
11
13
|
const mockClient = {
|
|
12
14
|
connect: mockConnect,
|
|
15
|
+
close: mockClose,
|
|
13
16
|
db: jest.fn().mockReturnValue(mockDb)
|
|
14
17
|
} as unknown as MongoClient
|
|
15
18
|
|
|
16
|
-
beforeEach(() => {
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await closeMongo2()
|
|
21
|
+
|
|
17
22
|
jest.clearAllMocks()
|
|
18
23
|
;(MongoClient as unknown as jest.Mock).mockImplementation(() => mockClient)
|
|
19
24
|
})
|
|
@@ -22,13 +27,23 @@ describe('Mongo Utils', () => {
|
|
|
22
27
|
it('should connect to MongoDB and store client', async () => {
|
|
23
28
|
await connectToMongo2('mongo-uri')
|
|
24
29
|
|
|
25
|
-
expect(
|
|
30
|
+
expect(MongoClient).toHaveBeenCalledWith('mongo-uri')
|
|
31
|
+
expect(mockConnect).toHaveBeenCalledTimes(1)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should not reconnect if client is already initialized', async () => {
|
|
35
|
+
await connectToMongo2('mongo-uri')
|
|
36
|
+
await connectToMongo2('mongo-uri')
|
|
37
|
+
|
|
38
|
+
expect(MongoClient).toHaveBeenCalledTimes(1)
|
|
39
|
+
expect(mockConnect).toHaveBeenCalledTimes(1)
|
|
26
40
|
})
|
|
27
41
|
|
|
28
42
|
it('should return a Db instance when client is initialized', async () => {
|
|
29
43
|
await connectToMongo2('mongo-uri')
|
|
30
44
|
|
|
31
45
|
const db = getDb2('test-db')
|
|
46
|
+
|
|
32
47
|
expect(mockClient.db).toHaveBeenCalledWith('test-db')
|
|
33
48
|
expect(db).toBe(mockDb)
|
|
34
49
|
})
|
|
@@ -37,23 +52,23 @@ describe('Mongo Utils', () => {
|
|
|
37
52
|
await connectToMongo2('mongo-uri')
|
|
38
53
|
|
|
39
54
|
getCollection2('test-db', 'users')
|
|
55
|
+
|
|
40
56
|
expect(mockClient.db).toHaveBeenCalledWith('test-db')
|
|
41
57
|
expect(mockDb.collection).toHaveBeenCalledWith('users')
|
|
42
58
|
})
|
|
43
59
|
|
|
44
|
-
it('should throw error if client is not initialized
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
60
|
+
it('should throw error if client is not initialized', async () => {
|
|
61
|
+
await closeMongo2()
|
|
62
|
+
|
|
63
|
+
expect(() => getDb2('test-db')).toThrow('Mongo not initialized')
|
|
64
|
+
expect(() => getCollection2('test-db', 'users')).toThrow('Mongo not initialized')
|
|
50
65
|
})
|
|
51
66
|
|
|
52
|
-
it('should
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
it('should close Mongo and reset state', async () => {
|
|
68
|
+
await connectToMongo2('mongo-uri')
|
|
69
|
+
await closeMongo2()
|
|
70
|
+
|
|
71
|
+
expect(mockClose).toHaveBeenCalledTimes(1)
|
|
72
|
+
expect(() => getDb2('test-db')).toThrow('Mongo not initialized')
|
|
58
73
|
})
|
|
59
74
|
})
|
package/tests/db/mongo3.test.ts
CHANGED
|
@@ -2,6 +2,8 @@ import {
|
|
|
2
2
|
connectToMongo3,
|
|
3
3
|
getDb3,
|
|
4
4
|
getCollection3,
|
|
5
|
+
closeMongo3,
|
|
6
|
+
closeAllMongo3,
|
|
5
7
|
__mongoClients,
|
|
6
8
|
__connectPromises
|
|
7
9
|
} from '../../src/db/mongo3'
|
|
@@ -14,12 +16,17 @@ const mockDb = {
|
|
|
14
16
|
} as unknown as Db
|
|
15
17
|
|
|
16
18
|
const mockConnect = jest.fn()
|
|
19
|
+
const mockClose = jest.fn()
|
|
20
|
+
|
|
17
21
|
const mockClient = {
|
|
18
22
|
connect: mockConnect,
|
|
23
|
+
close: mockClose,
|
|
19
24
|
db: jest.fn().mockReturnValue(mockDb)
|
|
20
25
|
} as unknown as MongoClient
|
|
21
26
|
|
|
22
|
-
beforeEach(() => {
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
await closeAllMongo3()
|
|
29
|
+
|
|
23
30
|
jest.clearAllMocks()
|
|
24
31
|
__mongoClients.clear()
|
|
25
32
|
__connectPromises.clear()
|
|
@@ -33,9 +40,11 @@ describe('MongoClientManager (multi-cluster)', () => {
|
|
|
33
40
|
it('should connect and cache client for a single cluster', async () => {
|
|
34
41
|
await connectToMongo3(clusterAUri)
|
|
35
42
|
|
|
36
|
-
expect(
|
|
43
|
+
expect(MongoClient).toHaveBeenCalledWith(clusterAUri)
|
|
44
|
+
expect(mockConnect).toHaveBeenCalledTimes(1)
|
|
37
45
|
|
|
38
46
|
const db = getDb3(clusterAUri, 'test-db')
|
|
47
|
+
|
|
39
48
|
expect(mockClient.db).toHaveBeenCalledWith('test-db')
|
|
40
49
|
expect(db).toBe(mockDb)
|
|
41
50
|
})
|
|
@@ -44,6 +53,7 @@ describe('MongoClientManager (multi-cluster)', () => {
|
|
|
44
53
|
await connectToMongo3(clusterAUri)
|
|
45
54
|
await connectToMongo3(clusterAUri)
|
|
46
55
|
|
|
56
|
+
expect(MongoClient).toHaveBeenCalledTimes(1)
|
|
47
57
|
expect(mockConnect).toHaveBeenCalledTimes(1)
|
|
48
58
|
})
|
|
49
59
|
|
|
@@ -51,6 +61,7 @@ describe('MongoClientManager (multi-cluster)', () => {
|
|
|
51
61
|
await connectToMongo3(clusterAUri)
|
|
52
62
|
await connectToMongo3(clusterBUri)
|
|
53
63
|
|
|
64
|
+
expect(MongoClient).toHaveBeenCalledTimes(2)
|
|
54
65
|
expect(mockConnect).toHaveBeenCalledTimes(2)
|
|
55
66
|
})
|
|
56
67
|
|
|
@@ -63,21 +74,31 @@ describe('MongoClientManager (multi-cluster)', () => {
|
|
|
63
74
|
expect(mockDb.collection).toHaveBeenCalledWith('users')
|
|
64
75
|
})
|
|
65
76
|
|
|
66
|
-
it('should throw error if client not initialized
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
it('should throw error if client not initialized', () => {
|
|
78
|
+
expect(() => getDb3('unknown-uri', 'some-db')).toThrow('Mongo client not initialized')
|
|
79
|
+
expect(() => getCollection3('unknown-uri', 'some-db', 'users')).toThrow(
|
|
80
|
+
'Mongo client not initialized'
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should close one Mongo client and reset only that uri', async () => {
|
|
85
|
+
await connectToMongo3(clusterAUri)
|
|
86
|
+
await closeMongo3(clusterAUri)
|
|
87
|
+
|
|
88
|
+
expect(mockClose).toHaveBeenCalledTimes(1)
|
|
89
|
+
expect(__mongoClients.has(clusterAUri)).toBe(false)
|
|
90
|
+
expect(__connectPromises.has(clusterAUri)).toBe(false)
|
|
91
|
+
expect(() => getDb3(clusterAUri, 'test-db')).toThrow('Mongo client not initialized')
|
|
72
92
|
})
|
|
73
93
|
|
|
74
|
-
it('should
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
it('should close all Mongo clients and reset state', async () => {
|
|
95
|
+
await connectToMongo3(clusterAUri)
|
|
96
|
+
await connectToMongo3(clusterBUri)
|
|
97
|
+
|
|
98
|
+
await closeAllMongo3()
|
|
99
|
+
|
|
100
|
+
expect(mockClose).toHaveBeenCalledTimes(2)
|
|
101
|
+
expect(__mongoClients.size).toBe(0)
|
|
102
|
+
expect(__connectPromises.size).toBe(0)
|
|
82
103
|
})
|
|
83
104
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/pricecharting.test.ts
|
|
2
2
|
|
|
3
|
-
import { splitNameAndNumber } from '../src/pricecharting'
|
|
3
|
+
import { productExists, splitNameAndNumber } from '../src/pricecharting'
|
|
4
4
|
|
|
5
5
|
describe('splitNameAndNumber (legacy fallback and new realNumber param)', () => {
|
|
6
6
|
const legacyCases: Array<[string, { name: string; number: string | null }]> = [
|
|
@@ -73,3 +73,140 @@ describe('splitNameAndNumber (legacy fallback and new realNumber param)', () =>
|
|
|
73
73
|
expect(splitNameAndNumber(input, '123')).toEqual({ name: 'Example', number: 'X123' })
|
|
74
74
|
})
|
|
75
75
|
})
|
|
76
|
+
|
|
77
|
+
describe('productExists', () => {
|
|
78
|
+
const originalFetch = global.fetch
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
jest.clearAllMocks()
|
|
82
|
+
jest.useRealTimers()
|
|
83
|
+
global.fetch = jest.fn()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
global.fetch = originalFetch
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
function mockFetchResponse(status: number, body: unknown) {
|
|
91
|
+
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
92
|
+
status,
|
|
93
|
+
json: jest.fn().mockResolvedValue(body)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test('returns null for invalid productId', async () => {
|
|
98
|
+
const result = await productExists(0, 'token', 30_000)
|
|
99
|
+
|
|
100
|
+
expect(result).toBeNull()
|
|
101
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('returns null for missing token', async () => {
|
|
105
|
+
const result = await productExists(123, '', 30_000)
|
|
106
|
+
|
|
107
|
+
expect(result).toBeNull()
|
|
108
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('returns null for invalid timeout', async () => {
|
|
112
|
+
const result = await productExists(123, 'token', 30)
|
|
113
|
+
|
|
114
|
+
expect(result).toBeNull()
|
|
115
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('returns false when API responds with HTTP 404', async () => {
|
|
119
|
+
mockFetchResponse(404, {
|
|
120
|
+
error: 'No such product',
|
|
121
|
+
'error-message': 'No such product',
|
|
122
|
+
status: 'error'
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const result = await productExists(123, 'token', 30_000)
|
|
126
|
+
|
|
127
|
+
expect(result).toBe(false)
|
|
128
|
+
expect(global.fetch).toHaveBeenCalledTimes(1)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('returns true when API responds with 200 and non-error body', async () => {
|
|
132
|
+
mockFetchResponse(200, {
|
|
133
|
+
status: 'success',
|
|
134
|
+
product: {
|
|
135
|
+
id: 123
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const result = await productExists(123, 'token', 30_000)
|
|
140
|
+
|
|
141
|
+
expect(result).toBe(true)
|
|
142
|
+
expect(global.fetch).toHaveBeenCalledTimes(1)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('returns false when API responds with status error and no such product message', async () => {
|
|
146
|
+
mockFetchResponse(200, {
|
|
147
|
+
status: 'error',
|
|
148
|
+
error: 'No such product',
|
|
149
|
+
'error-message': 'No such product'
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const result = await productExists(123, 'token', 30_000)
|
|
153
|
+
|
|
154
|
+
expect(result).toBe(false)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('returns null when API responds with non-200 non-404 status', async () => {
|
|
158
|
+
mockFetchResponse(500, {
|
|
159
|
+
status: 'error',
|
|
160
|
+
error: 'server error'
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const result = await productExists(123, 'token', 30_000)
|
|
164
|
+
|
|
165
|
+
expect(result).toBeNull()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('returns null when JSON parsing fails', async () => {
|
|
169
|
+
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
170
|
+
status: 200,
|
|
171
|
+
json: jest.fn().mockRejectedValue(new Error('invalid json'))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const result = await productExists(123, 'token', 30_000)
|
|
175
|
+
|
|
176
|
+
expect(result).toBeNull()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('returns null when response body is not an object', async () => {
|
|
180
|
+
mockFetchResponse(200, null)
|
|
181
|
+
|
|
182
|
+
const result = await productExists(123, 'token', 30_000)
|
|
183
|
+
|
|
184
|
+
expect(result).toBeNull()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('returns null when fetch fails', async () => {
|
|
188
|
+
;(global.fetch as jest.Mock).mockRejectedValue(new Error('network error'))
|
|
189
|
+
|
|
190
|
+
const result = await productExists(123, 'token', 30_000)
|
|
191
|
+
|
|
192
|
+
expect(result).toBeNull()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('calls PriceCharting API with token and product id', async () => {
|
|
196
|
+
mockFetchResponse(200, {
|
|
197
|
+
status: 'success'
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
await productExists(456, 'test-token', 30_000)
|
|
201
|
+
|
|
202
|
+
expect(global.fetch).toHaveBeenCalledTimes(1)
|
|
203
|
+
|
|
204
|
+
const [url, options] = (global.fetch as jest.Mock).mock.calls[0]
|
|
205
|
+
|
|
206
|
+
expect(url).toContain('https://www.pricecharting.com/api/product')
|
|
207
|
+
expect(url).toContain('t=test-token')
|
|
208
|
+
expect(url).toContain('id=456')
|
|
209
|
+
expect(options.method).toBe('GET')
|
|
210
|
+
expect(options.signal).toBeDefined()
|
|
211
|
+
})
|
|
212
|
+
})
|