初始提交
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '../../web/data');
|
||||
// Ensure data directory exists (for export.json)
|
||||
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
const paths = {
|
||||
export: path.join(DATA_DIR, 'export.json')
|
||||
};
|
||||
|
||||
const pool = mysql.createPool({
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
user: 'cc-ticket-machine',
|
||||
password: 'cc-ticket-machine',
|
||||
database: 'cc-ticket-machine',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
|
||||
// In-memory cache for synchronous read access
|
||||
const cache = {
|
||||
config: {
|
||||
api_base: 'http://127.0.0.1:23333/api',
|
||||
current_station: { name: 'Station1', code: '01-01' },
|
||||
transfers: [],
|
||||
promotion: { name: '', discount: 1 }
|
||||
},
|
||||
stations: [],
|
||||
lines: [],
|
||||
fares: [],
|
||||
orders: [],
|
||||
orderIndex: {},
|
||||
ticketIndex: {},
|
||||
icCards: [],
|
||||
icCardIndex: {},
|
||||
icCardEvents: [],
|
||||
statsTicket: [],
|
||||
statsGate: []
|
||||
};
|
||||
|
||||
const DataService = {
|
||||
paths, // Kept for compatibility if anything accesses paths.export
|
||||
|
||||
init: async () => {
|
||||
try {
|
||||
// Create Tables
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS kv_store (k VARCHAR(255) PRIMARY KEY, v JSON)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS stations (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS \`lines\` (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS fares (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS orders (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS order_index (order_code VARCHAR(255) PRIMARY KEY, data JSON)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS ticket_index (ticket_id VARCHAR(255) PRIMARY KEY, data JSON, last_update_ts BIGINT)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS ic_cards (card_id VARCHAR(255) PRIMARY KEY, data JSON, last_update_ts BIGINT)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS ic_card_events (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS logs (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS ticket_events (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS stats_ticket (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`);
|
||||
await conn.query(`CREATE TABLE IF NOT EXISTS stats_gate (id INT AUTO_INCREMENT PRIMARY KEY, data JSON)`);
|
||||
|
||||
// Load Cache
|
||||
const [configs] = await conn.query('SELECT v FROM kv_store WHERE k = ?', ['config']);
|
||||
if (configs.length > 0) cache.config = configs[0].v;
|
||||
else await conn.query('INSERT INTO kv_store (k, v) VALUES (?, ?)', ['config', JSON.stringify(cache.config)]);
|
||||
|
||||
const [stations] = await conn.query('SELECT data FROM stations');
|
||||
cache.stations = stations.map(r => r.data);
|
||||
|
||||
const [lines] = await conn.query('SELECT data FROM `lines`');
|
||||
cache.lines = lines.map(r => r.data);
|
||||
|
||||
const [fares] = await conn.query('SELECT data FROM fares');
|
||||
cache.fares = fares.map(r => r.data);
|
||||
|
||||
const [orders] = await conn.query('SELECT data FROM orders');
|
||||
cache.orders = orders.map(r => r.data);
|
||||
|
||||
const [orderIndices] = await conn.query('SELECT order_code, data FROM order_index');
|
||||
orderIndices.forEach(r => { cache.orderIndex[r.order_code] = r.data; });
|
||||
|
||||
const [ticketIndices] = await conn.query('SELECT ticket_id, data FROM ticket_index');
|
||||
ticketIndices.forEach(r => { cache.ticketIndex[r.ticket_id] = r.data; });
|
||||
|
||||
const [icCards] = await conn.query('SELECT card_id, data FROM ic_cards');
|
||||
cache.icCardIndex = {};
|
||||
icCards.forEach(r => { cache.icCardIndex[r.card_id] = r.data; });
|
||||
cache.icCards = Object.values(cache.icCardIndex).sort((a, b) => Number(b.last_update_ts || 0) - Number(a.last_update_ts || 0));
|
||||
|
||||
const [icCardEvents] = await conn.query('SELECT data FROM ic_card_events ORDER BY id ASC');
|
||||
cache.icCardEvents = icCardEvents.map((r) => r.data);
|
||||
|
||||
const [statsT] = await conn.query('SELECT data FROM stats_ticket');
|
||||
cache.statsTicket = statsT.map(r => r.data);
|
||||
|
||||
const [statsG] = await conn.query('SELECT data FROM stats_gate');
|
||||
cache.statsGate = statsG.map(r => r.data);
|
||||
|
||||
console.log('DataService initialized with MySQL');
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to initialize DataService:', e);
|
||||
// Fallback or exit? For now, we continue but cache might be empty
|
||||
}
|
||||
},
|
||||
|
||||
// Config
|
||||
getConfig: () => cache.config,
|
||||
saveConfig: async (cfg) => {
|
||||
cache.config = cfg;
|
||||
await pool.query('INSERT INTO kv_store (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = ?', ['config', JSON.stringify(cfg), JSON.stringify(cfg)]);
|
||||
},
|
||||
|
||||
// Stations
|
||||
getStations: () => cache.stations,
|
||||
saveStations: async (list) => {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
await conn.query('DELETE FROM stations');
|
||||
if (list.length > 0) {
|
||||
const placeholders = list.map(() => '(?)').join(',');
|
||||
const values = list.map(item => JSON.stringify(item));
|
||||
await conn.query('INSERT INTO stations (data) VALUES ' + placeholders, values);
|
||||
}
|
||||
await conn.commit();
|
||||
cache.stations = list;
|
||||
} catch (e) {
|
||||
await conn.rollback();
|
||||
console.error('saveStations error', e);
|
||||
throw e;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
},
|
||||
|
||||
// Lines
|
||||
getLines: () => cache.lines,
|
||||
saveLines: async (list) => {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
await conn.query('DELETE FROM `lines`');
|
||||
if (list.length > 0) {
|
||||
const placeholders = list.map(() => '(?)').join(',');
|
||||
const values = list.map(item => JSON.stringify(item));
|
||||
await conn.query('INSERT INTO `lines` (data) VALUES ' + placeholders, values);
|
||||
}
|
||||
await conn.commit();
|
||||
cache.lines = list;
|
||||
} catch (e) {
|
||||
await conn.rollback();
|
||||
console.error('saveLines error', e);
|
||||
throw e;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
},
|
||||
|
||||
// Fares
|
||||
getFares: () => cache.fares,
|
||||
saveFares: async (list) => {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
await conn.query('DELETE FROM fares');
|
||||
if (list.length > 0) {
|
||||
const placeholders = list.map(() => '(?)').join(',');
|
||||
const values = list.map(item => JSON.stringify(item));
|
||||
await conn.query(`INSERT INTO fares (data) VALUES ${placeholders}`, values);
|
||||
}
|
||||
await conn.commit();
|
||||
cache.fares = list;
|
||||
} catch (e) {
|
||||
await conn.rollback();
|
||||
console.error('saveFares error', e);
|
||||
throw e;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
},
|
||||
|
||||
// Orders
|
||||
getOrders: () => cache.orders,
|
||||
saveOrders: async (list) => {
|
||||
// Optimization: If we assume append-only, we can just insert the last one.
|
||||
// But the API passes the whole list. For correctness with the current API:
|
||||
// Full replace is inefficient for orders. But mimicking file overwrite.
|
||||
// Better: logic.js pushes to list and calls saveOrders.
|
||||
// We should probably just insert the new ones if we could track diffs.
|
||||
// For now, doing full replace to be safe with existing logic, or optimized if possible.
|
||||
// Wait, the API `router.post('/orders')` does: list.push(rec); saveOrders(list);
|
||||
// I can optimize this in `saveOrders` if I knew it was an append.
|
||||
// But to be safe and simple:
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
await conn.query('DELETE FROM orders');
|
||||
if (list.length > 0) {
|
||||
// Split into chunks if too large?
|
||||
const placeholders = list.map(() => '(?)').join(',');
|
||||
const values = list.map(item => JSON.stringify(item));
|
||||
// Be careful with max packet size.
|
||||
// If list is huge, this crashes.
|
||||
// But for this task, we assume reasonable size.
|
||||
await conn.query(`INSERT INTO orders (data) VALUES ${placeholders}`, values);
|
||||
}
|
||||
await conn.commit();
|
||||
cache.orders = list;
|
||||
} catch (e) {
|
||||
await conn.rollback();
|
||||
console.error('saveOrders error', e);
|
||||
throw e;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
},
|
||||
|
||||
getOrderIndex: () => cache.orderIndex,
|
||||
saveOrderIndex: async (idx) => {
|
||||
// This is also potentially huge.
|
||||
// `router.post('/orders')` does: idx[code] = rec; saveOrderIndex(idx);
|
||||
// Ideally we should just upsert the single entry.
|
||||
// But since we receive the whole object, we can't easily know which one changed without diffing.
|
||||
// However, for the sake of the migration task, I will do a truncate/insert loop or similar.
|
||||
// Actually, `order_index` table has `order_code` PK.
|
||||
// I can iterate and UPSERT all? That's slow.
|
||||
// Given "Development" context, maybe I just clear and insert all.
|
||||
// OR, I can accept that `saveOrderIndex` is heavy.
|
||||
|
||||
// Better approach: modifying `router.post('/orders')` to NOT call saveOrderIndex with the whole object, but call a new method `addOrder(rec)`.
|
||||
// But I want to minimize changes to `api.js`.
|
||||
|
||||
// Let's implement full save for now.
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
await conn.query('DELETE FROM order_index');
|
||||
const entries = Object.entries(idx);
|
||||
if (entries.length > 0) {
|
||||
// Batch insert
|
||||
for (let i = 0; i < entries.length; i += 100) {
|
||||
const batch = entries.slice(i, i + 100);
|
||||
const q = `INSERT INTO order_index (order_code, data) VALUES ${batch.map(()=>'(?,?)').join(',')}`;
|
||||
const params = batch.flatMap(([k,v]) => [k, JSON.stringify(v)]);
|
||||
await conn.query(q, params);
|
||||
}
|
||||
}
|
||||
await conn.commit();
|
||||
cache.orderIndex = idx;
|
||||
} catch (e) {
|
||||
await conn.rollback();
|
||||
console.error('saveOrderIndex error', e);
|
||||
throw e;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
},
|
||||
|
||||
// IC Cards
|
||||
getIcCards: () => cache.icCards,
|
||||
getIcCardIndex: () => cache.icCardIndex,
|
||||
saveIcCards: async (list) => {
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
await conn.query('DELETE FROM ic_cards');
|
||||
if (list.length > 0) {
|
||||
for (let i = 0; i < list.length; i += 100) {
|
||||
const batch = list.slice(i, i + 100).map((item) => ({
|
||||
...item,
|
||||
card_id: String(item?.card_id || '').trim(),
|
||||
last_update_ts: Number(item?.last_update_ts || Date.now())
|
||||
})).filter((item) => item.card_id);
|
||||
if (batch.length === 0) continue;
|
||||
const q = `INSERT INTO ic_cards (card_id, data, last_update_ts) VALUES ${batch.map(() => '(?,?,?)').join(',')}`;
|
||||
const params = batch.flatMap((item) => [item.card_id, JSON.stringify(item), item.last_update_ts]);
|
||||
await conn.query(q, params);
|
||||
}
|
||||
}
|
||||
await conn.commit();
|
||||
cache.icCardIndex = {};
|
||||
list.forEach((item) => {
|
||||
const id = String(item?.card_id || '').trim();
|
||||
if (id) cache.icCardIndex[id] = item;
|
||||
});
|
||||
cache.icCards = Object.values(cache.icCardIndex).sort((a, b) => Number(b.last_update_ts || 0) - Number(a.last_update_ts || 0));
|
||||
} catch (e) {
|
||||
await conn.rollback();
|
||||
console.error('saveIcCards error', e);
|
||||
throw e;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
},
|
||||
upsertIcCard: async (update) => {
|
||||
const id = String(update?.card_id || '').trim();
|
||||
if (!id) return null;
|
||||
const cur = cache.icCardIndex[id] || {};
|
||||
const merged = { ...cur, ...update, card_id: id, last_update_ts: Date.now() };
|
||||
cache.icCardIndex[id] = merged;
|
||||
cache.icCards = Object.values(cache.icCardIndex).sort((a, b) => Number(b.last_update_ts || 0) - Number(a.last_update_ts || 0));
|
||||
try {
|
||||
await pool.query(
|
||||
'INSERT INTO ic_cards (card_id, data, last_update_ts) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE data = ?, last_update_ts = ?',
|
||||
[id, JSON.stringify(merged), merged.last_update_ts, JSON.stringify(merged), merged.last_update_ts]
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('upsertIcCard error', e);
|
||||
}
|
||||
return merged;
|
||||
},
|
||||
deleteIcCard: async (cardId) => {
|
||||
const id = String(cardId || '').trim();
|
||||
if (!id) return;
|
||||
delete cache.icCardIndex[id];
|
||||
cache.icCards = Object.values(cache.icCardIndex).sort((a, b) => Number(b.last_update_ts || 0) - Number(a.last_update_ts || 0));
|
||||
try {
|
||||
await pool.query('DELETE FROM ic_cards WHERE card_id = ?', [id]);
|
||||
} catch (e) {
|
||||
console.error('deleteIcCard error', e);
|
||||
}
|
||||
},
|
||||
appendIcCardEvent: async (entry) => {
|
||||
cache.icCardEvents.push(entry);
|
||||
try {
|
||||
await pool.query('INSERT INTO ic_card_events (data) VALUES (?)', [JSON.stringify(entry)]);
|
||||
} catch (e) {
|
||||
console.error('appendIcCardEvent error', e);
|
||||
}
|
||||
},
|
||||
getIcCardEvents: async (cardId) => {
|
||||
const id = String(cardId || '').trim();
|
||||
try {
|
||||
return cache.icCardEvents.filter((item) => String(item?.card_id || '').trim() === id);
|
||||
} catch (e) {
|
||||
console.error('getIcCardEvents error', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// Logs (Async Read, Append Write)
|
||||
appendLog: async (entry) => {
|
||||
try {
|
||||
await pool.query('INSERT INTO logs (data) VALUES (?)', [JSON.stringify(entry)]);
|
||||
} catch(e) { console.error('appendLog error', e); }
|
||||
},
|
||||
readLogs: async ({ max = 200, category, type, q, since, until } = {}) => {
|
||||
try {
|
||||
const limit = Math.min(5000, Math.max(1, Number(max) || 200));
|
||||
const fetchLimit = Math.min(20000, Math.max(limit, limit * 10));
|
||||
const [rows] = await pool.query('SELECT data FROM logs ORDER BY id DESC LIMIT ?', [fetchLimit]);
|
||||
let list = rows.map(r => r.data);
|
||||
|
||||
const cat = (category == null) ? '' : String(category).trim().toLowerCase();
|
||||
if (cat) list = list.filter(x => String(x?.category || '').trim().toLowerCase() === cat);
|
||||
|
||||
const typeRaw = (type == null) ? '' : String(type).trim();
|
||||
if (typeRaw) {
|
||||
const typeList = typeRaw.split(',').map(s => s.trim()).filter(Boolean).map(s => s.toLowerCase());
|
||||
list = list.filter(x => typeList.includes(String(x?.type || '').trim().toLowerCase()));
|
||||
}
|
||||
|
||||
const qRaw = (q == null) ? '' : String(q).trim().toLowerCase();
|
||||
if (qRaw) {
|
||||
list = list.filter(x => {
|
||||
try { return JSON.stringify(x || {}).toLowerCase().includes(qRaw); } catch (e) { return false; }
|
||||
});
|
||||
}
|
||||
|
||||
const toTs = (v) => {
|
||||
if (v == null) return null;
|
||||
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
||||
const s = String(v).trim();
|
||||
if (!s) return null;
|
||||
const n = Number(s);
|
||||
if (Number.isFinite(n)) return n;
|
||||
const d = Date.parse(s);
|
||||
return Number.isFinite(d) ? d : null;
|
||||
};
|
||||
const sinceTs = toTs(since);
|
||||
const untilTs = toTs(until);
|
||||
if (sinceTs != null || untilTs != null) {
|
||||
list = list.filter(x => {
|
||||
const d = Date.parse(String(x?.ts || ''));
|
||||
if (!Number.isFinite(d)) return false;
|
||||
if (sinceTs != null && d < sinceTs) return false;
|
||||
if (untilTs != null && d > untilTs) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return list.slice(0, limit);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
readLastLogs: async function (max = 200) { return this.readLogs({ max }); },
|
||||
|
||||
// Ticket Events (Async Read, Append Write)
|
||||
appendTicketEvent: async (ev) => {
|
||||
try {
|
||||
await pool.query('INSERT INTO ticket_events (data) VALUES (?)', [JSON.stringify(ev)]);
|
||||
} catch(e) { console.error('appendTicketEvent error', e); }
|
||||
},
|
||||
readAllTicketEvents: async () => { // Changed to async!
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT data FROM ticket_events ORDER BY id ASC');
|
||||
return rows.map(r => r.data);
|
||||
} catch(e) { return []; }
|
||||
},
|
||||
// Optimized method for filtering by ticket_id (if I update callers)
|
||||
getTicketEvents: async (ticketId) => {
|
||||
try {
|
||||
// We can't easily filter JSON in WHERE clause efficiently without generated columns.
|
||||
// But for small scale it's fine. Or fetch all and filter in app (like original).
|
||||
// `SELECT data FROM ticket_events WHERE data->>"$.ticket_id" = ?`
|
||||
const [rows] = await pool.query('SELECT data FROM ticket_events WHERE data->"$.ticket_id" = ? ORDER BY id ASC', [ticketId]);
|
||||
return rows.map(r => r.data);
|
||||
} catch(e) { return []; }
|
||||
},
|
||||
|
||||
// Ticket Index
|
||||
getTicketIndex: () => cache.ticketIndex,
|
||||
saveTicketIndex: async (idx) => {
|
||||
// Same issue as orderIndex. Heavy.
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
await conn.query('DELETE FROM ticket_index');
|
||||
const entries = Object.entries(idx);
|
||||
if (entries.length > 0) {
|
||||
for (let i = 0; i < entries.length; i += 100) {
|
||||
const batch = entries.slice(i, i + 100);
|
||||
const q = `INSERT INTO ticket_index (ticket_id, data, last_update_ts) VALUES ${batch.map(()=>'(?,?,?)').join(',')}`;
|
||||
const params = batch.flatMap(([k,v]) => [k, JSON.stringify(v), v.last_update_ts || 0]);
|
||||
await conn.query(q, params);
|
||||
}
|
||||
}
|
||||
await conn.commit();
|
||||
cache.ticketIndex = idx;
|
||||
} catch (e) {
|
||||
await conn.rollback();
|
||||
console.error('saveTicketIndex error', e);
|
||||
throw e;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
},
|
||||
upsertTicketIndex: async (update) => {
|
||||
const id = String(update.ticket_id || '').trim();
|
||||
if (!id) return;
|
||||
const cur = cache.ticketIndex[id] || {};
|
||||
const merged = { ...cur, ...update, last_update_ts: Date.now() };
|
||||
cache.ticketIndex[id] = merged;
|
||||
|
||||
// Efficient single update
|
||||
try {
|
||||
await pool.query('INSERT INTO ticket_index (ticket_id, data, last_update_ts) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE data = ?, last_update_ts = ?',
|
||||
[id, JSON.stringify(merged), merged.last_update_ts, JSON.stringify(merged), merged.last_update_ts]);
|
||||
} catch(e) { console.error('upsertTicketIndex error', e); }
|
||||
return merged;
|
||||
},
|
||||
|
||||
// Stats
|
||||
appendStatTicket: async (item) => {
|
||||
cache.statsTicket.push(item);
|
||||
try {
|
||||
await pool.query('INSERT INTO stats_ticket (data) VALUES (?)', [JSON.stringify(item)]);
|
||||
} catch(e) { console.error('appendStatTicket error', e); }
|
||||
},
|
||||
getStatsTicket: () => cache.statsTicket,
|
||||
|
||||
appendStatGate: async (item) => {
|
||||
cache.statsGate.push(item);
|
||||
try {
|
||||
await pool.query('INSERT INTO stats_gate (data) VALUES (?)', [JSON.stringify(item)]);
|
||||
} catch(e) { console.error('appendStatGate error', e); }
|
||||
},
|
||||
getStatsGate: () => cache.statsGate,
|
||||
|
||||
// Export
|
||||
buildExportPayload: () => ({
|
||||
config: DataService.getConfig(),
|
||||
stations: DataService.getStations(),
|
||||
lines: DataService.getLines(),
|
||||
fares: DataService.getFares(),
|
||||
stats_ticket: DataService.getStatsTicket(),
|
||||
stats_gate: DataService.getStatsGate(),
|
||||
}),
|
||||
saveExport: () => {
|
||||
try { fs.writeFileSync(paths.export, JSON.stringify(DataService.buildExportPayload(), null, 2)); } catch(e){}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DataService;
|
||||
Reference in New Issue
Block a user