mirror of
https://github.com/tiennm99/store-scraper-bot.git
synced 2026-10-11 12:29:01 +00:00
Phase 7 cleanup of the Vercel + Upstash consolidation plan:
- delete wrangler.toml, Dockerfile, docker-compose{,.dev}.yml,
scripts/migrate-atlas-to-upstash.js (one-shot migration done)
- drop wrangler + mongodb devDeps and migrate* npm scripts;
regenerate package-lock.json (-70 packages)
- prune CF/Wrangler/Atlas-export entries from .gitignore + .vercelignore
- drop MONGODB_URI from .env.deploy.example
- rewrite README for Vercel + Upstash architecture
- refresh stale Cloudflare comments in src/{logger,models,repository}
91 lines
3.7 KiB
JavaScript
91 lines
3.7 KiB
JavaScript
// Upstash Redis adapter.
|
|
//
|
|
// Logical key namespace:
|
|
// admin singleton
|
|
// group:{chatId} per-group state
|
|
// apple:{appId} cached Apple response (with TTL)
|
|
// google:{appId} cached Google response (with TTL)
|
|
//
|
|
// Multi-tenancy: every physical Redis key carries a configurable prefix
|
|
// (env.KEY_PREFIX, default 'store-scraper-bot:') so this bot can safely share
|
|
// an Upstash database with other Vercel projects without collision. Repository
|
|
// callers pass logical keys; the adapter applies the prefix transparently.
|
|
//
|
|
// 60s minimum TTL clamp is preserved from the KV days for parity safety,
|
|
// even though Redis would accept lower values.
|
|
|
|
import { Redis } from '@upstash/redis';
|
|
|
|
const MIN_TTL_SECONDS = 60;
|
|
const DEFAULT_KEY_PREFIX = 'store-scraper-bot:';
|
|
|
|
export class UpstashUnavailable extends Error {
|
|
constructor(missing) {
|
|
super(`Upstash env var missing: ${missing}`);
|
|
this.name = 'UpstashUnavailable';
|
|
}
|
|
}
|
|
|
|
// Build a handle bundling the Redis client and the key prefix together.
|
|
// The handle is what callers pass into getJson/putJson/del/scan — it stays
|
|
// opaque so repositories never need to know about prefixing themselves.
|
|
//
|
|
// Accepts both env var naming conventions:
|
|
// - UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN (vanilla Upstash signup)
|
|
// - KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Marketplace integration)
|
|
// so the operator doesn't have to duplicate vars when the bot shares an
|
|
// Upstash DB provisioned via Vercel.
|
|
export function createUpstashClient(env) {
|
|
const url = env?.UPSTASH_REDIS_REST_URL ?? env?.KV_REST_API_URL;
|
|
const token = env?.UPSTASH_REDIS_REST_TOKEN ?? env?.KV_REST_API_TOKEN;
|
|
if (!url) throw new UpstashUnavailable('UPSTASH_REDIS_REST_URL or KV_REST_API_URL');
|
|
if (!token) throw new UpstashUnavailable('UPSTASH_REDIS_REST_TOKEN or KV_REST_API_TOKEN');
|
|
const client = new Redis({ url, token });
|
|
const prefix = env.KEY_PREFIX ?? DEFAULT_KEY_PREFIX;
|
|
return { client, prefix };
|
|
}
|
|
|
|
function physicalKey(handle, key) {
|
|
return `${handle.prefix}${key}`;
|
|
}
|
|
|
|
// Upstash auto-deserializes values that look like JSON. We always store via
|
|
// JSON.stringify, so reads can return the parsed object directly. Returns null
|
|
// on missing key, matching the prior KV semantics.
|
|
export async function getJson(handle, key) {
|
|
const value = await handle.client.get(physicalKey(handle, key));
|
|
if (value == null) return null;
|
|
// Some SDK versions return strings, others return parsed objects depending
|
|
// on content. Normalize: if string, parse; if object, pass through.
|
|
return typeof value === 'string' ? JSON.parse(value) : value;
|
|
}
|
|
|
|
export async function putJson(handle, key, value, opts = {}) {
|
|
const ex =
|
|
opts.expirationTtl != null ? Math.max(MIN_TTL_SECONDS, opts.expirationTtl) : null;
|
|
const setOpts = ex != null ? { ex } : undefined;
|
|
await handle.client.set(physicalKey(handle, key), JSON.stringify(value), setOpts);
|
|
}
|
|
|
|
export async function del(handle, key) {
|
|
await handle.client.del(physicalKey(handle, key));
|
|
}
|
|
|
|
// Suffix-based scan. Caller passes a logical match like 'group:*'; adapter
|
|
// prepends the key prefix so only this bot's keys are returned.
|
|
// Returns the list of *logical* keys (prefix stripped) so callers stay
|
|
// prefix-unaware.
|
|
export async function scan(handle, matchSuffix) {
|
|
const match = `${handle.prefix}${matchSuffix}`;
|
|
const out = [];
|
|
let cursor = '0';
|
|
do {
|
|
const [next, batch] = await handle.client.scan(cursor, { match, count: 100 });
|
|
cursor = next;
|
|
for (const physical of batch) {
|
|
out.push(physical.startsWith(handle.prefix) ? physical.slice(handle.prefix.length) : physical);
|
|
}
|
|
} while (cursor !== '0');
|
|
return out;
|
|
}
|