| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181 |
- import crypto from 'node:crypto';
- export const parseUrl = process.env.PARSE_URL || 'http://127.0.0.1:3000/parse';
- export const apiUrl = process.env.API_URL || 'http://127.0.0.1:3000/api/amazon';
- export const appId = process.env.PARSE_APP_ID;
- export const masterKey = process.env.PARSE_MASTER_KEY;
- export const runId = process.env.SP_API_RUN_ID || `spapi-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}`;
- if (!appId || !masterKey) throw new Error('Missing PARSE_APP_ID or PARSE_MASTER_KEY');
- export const parseHeaders = {
- 'Content-Type': 'application/json',
- 'X-Parse-Application-Id': appId,
- 'X-Parse-Master-Key': masterKey,
- };
- export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
- export const shopPointer = objectId => ({ __type: 'Pointer', className: 'Shop', objectId });
- export const orderPointer = objectId => ({ __type: 'Pointer', className: 'Order', objectId });
- export const dateValue = value => {
- if (!value) return undefined;
- const date = new Date(value);
- return Number.isNaN(date.getTime()) ? undefined : { __type: 'Date', iso: date.toISOString() };
- };
- export function cleanObject(value) {
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
- }
- export function stableStringify(value) {
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
- if (value && typeof value === 'object') {
- return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
- }
- return JSON.stringify(value);
- }
- export function hash(value) {
- return crypto.createHash('sha256').update(typeof value === 'string' ? value : stableStringify(value)).digest('hex');
- }
- export function log(event, data = {}) {
- console.log(JSON.stringify({ at: new Date().toISOString(), runId, event, ...data }));
- }
- export async function parseRequest(path, method = 'GET', body) {
- const response = await fetch(`${parseUrl}${path}`, {
- method,
- headers: parseHeaders,
- body: body === undefined ? undefined : JSON.stringify(body),
- });
- const text = await response.text();
- let result = {};
- try { result = JSON.parse(text); } catch {}
- if (!response.ok || result.error) {
- throw new Error(`${method} ${path}: ${result.error?.message || result.error || text.slice(0, 400) || response.status}`);
- }
- return result;
- }
- export async function allRows(className, keys = '', where = undefined) {
- const rows = [];
- let skip = 0;
- const whereParam = where === undefined ? '' : `&where=${encodeURIComponent(JSON.stringify(where))}`;
- while (true) {
- const page = await parseRequest(`/classes/${className}?limit=1000&skip=${skip}${keys ? `&keys=${keys}` : ''}${whereParam}`);
- rows.push(...(page.results || []));
- const length = page.results?.length || 0;
- if (length < 1000) return rows;
- skip += length;
- }
- }
- export async function forward(shopId, path, method = 'GET', body = undefined, maxAttempts = 5) {
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
- try {
- const response = await fetch(`${apiUrl}/forward`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'shop-objectid': shopId },
- body: JSON.stringify({ path, method, ...(body === undefined ? {} : { body }) }),
- signal: AbortSignal.timeout(180000),
- });
- const text = await response.text();
- let result = {};
- try { result = JSON.parse(text); } catch {}
- if (!response.ok || result.success === false) {
- const error = new Error(result.message || result.errors?.[0]?.message || text.slice(0, 400) || `HTTP ${response.status}`);
- error.status = Number(result.status || response.status);
- throw error;
- }
- return result.data || result;
- } catch (error) {
- if (attempt === maxAttempts || [400, 401, 403, 404, 409, 422].includes(error.status)) throw error;
- const delay = Math.min(attempt * 15000, 60000);
- log('forward_retry', { shopId, path: path.split('?')[0], attempt, delay, error: error.message });
- await sleep(delay);
- }
- }
- }
- const classCaches = new Map();
- async function classCache(className) {
- if (!classCaches.has(className)) {
- const rows = await allRows(className, 'objectId,recordKey');
- classCaches.set(className, new Map(rows.filter(row => row.recordKey).map(row => [row.recordKey, row.objectId])));
- }
- return classCaches.get(className);
- }
- export async function upsertMany(className, rows) {
- if (!rows.length) return { inserted: 0, updated: 0 };
- rows = [...new Map(rows.map(row => [row.recordKey, row])).values()];
- const cache = await classCache(className);
- let inserted = 0;
- let updated = 0;
- for (let start = 0; start < rows.length; start += 25) {
- const page = rows.slice(start, start + 25);
- const requests = page.map(row => {
- if (!row.recordKey) throw new Error(`${className} row is missing recordKey`);
- const objectId = cache.get(row.recordKey);
- return {
- method: objectId ? 'PUT' : 'POST',
- path: objectId ? `/parse/classes/${className}/${objectId}` : `/parse/classes/${className}`,
- body: row,
- };
- });
- const result = await parseRequest('/batch', 'POST', { requests });
- for (let index = 0; index < page.length; index++) {
- const response = result[index];
- if (response?.error) throw new Error(`${className}: ${response.error.error || response.error.code}`);
- if (cache.has(page[index].recordKey)) {
- updated++;
- } else {
- cache.set(page[index].recordKey, response?.success?.objectId);
- inserted++;
- }
- }
- }
- return { inserted, updated };
- }
- export async function updateObject(className, objectId, body) {
- return parseRequest(`/classes/${className}/${objectId}`, 'PUT', body);
- }
- export async function writeAudit({ dataType, endpoint, status, shop, region = '', marketplaceId = '', records = 0, pages = 0, error = '', startedAt, details = {} }) {
- const finishedAt = new Date();
- return parseRequest('/classes/SpApiSyncAudit', 'POST', {
- runId,
- dataType,
- endpoint,
- status,
- region,
- marketplaceId,
- records,
- pages,
- error: String(error || '').slice(0, 2000),
- startedAt: dateValue(startedAt || finishedAt),
- finishedAt: dateValue(finishedAt),
- details,
- ...(shop?.objectId ? { shop: shopPointer(shop.objectId) } : {}),
- });
- }
- export const regionByMarketplace = {
- ATVPDKIKX0DER: 'NA', A2EUQ1WTGCTBG2: 'NA', A1AM78C64UM0Y8: 'NA', A2Q3Y263D00KWC: 'NA',
- A1F83G8C2ARO7P: 'EU', A1PA6795UKMFR9: 'EU', A13V1IB3VIYZZH: 'EU', A1RKKUPIHCS9HS: 'EU',
- APJ6JRA9NG5V4: 'EU', A1805IZSGTT6HS: 'EU', A2NODRKZP88ZB9: 'EU', A1C3SOZRARQ6R3: 'EU',
- AMEN7PMS3EDWL: 'EU', A28R8C7NBKEWEA: 'EU', A2VIGQ35RCS4UG: 'EU', A17E79C6D8DWNP: 'EU',
- A39IBJ37TRP1C6: 'FE', A19VAU5U5O7RUS: 'FE',
- };
- export async function activeAmazonShops() {
- const shops = await allRows('Shop');
- return shops.filter(shop => shop.platform === 'amazon'
- && shop.status === 'active'
- && shop.disable !== true
- && shop.deleted !== true
- && shop.config?.SpApiConfig);
- }
|