u
This commit is contained in:
177
src/api/api-tester.mjs
Normal file
177
src/api/api-tester.mjs
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* API Tester CLI — reuses src/api/index.js directly.
|
||||
*
|
||||
* Usage:
|
||||
* node src/api/api-tester.mjs # interactive REPL
|
||||
* node src/api/api-tester.mjs <method> [args...] # one-shot call
|
||||
*
|
||||
* Auth:
|
||||
* 1. Copy your session cookie from the browser DevTools (name: "token", value: the JWT)
|
||||
* 2. Paste it into ~/.dashfront-token (or pass --token-file <path>)
|
||||
* 3. Or pass --token <jwt-string> on the command line
|
||||
*
|
||||
* Examples:
|
||||
* node src/api/api-tester.mjs posterminal.list
|
||||
* node src/api/api-tester.mjs category.getTree
|
||||
* node src/api/api-tester.mjs posterminal.create '{"name":"Kassa 3"}'
|
||||
* node src/api/api-tester.mjs auth.me
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { resolve } from 'node:path';
|
||||
import { createInterface } from 'node:readline';
|
||||
|
||||
// ── CLI args ─────────────────────────────────────────────────────────────────
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function flag(name) {
|
||||
const idx = args.indexOf(name);
|
||||
if (idx === -1) return null;
|
||||
const val = args[idx + 1];
|
||||
args.splice(idx, 2);
|
||||
return val;
|
||||
}
|
||||
|
||||
const BASE_URL = flag('--base') || 'http://localhost:4208';
|
||||
const tokenFileArg = flag('--token-file');
|
||||
const tokenArg = flag('--token');
|
||||
|
||||
function loadToken() {
|
||||
if (tokenArg) return tokenArg;
|
||||
|
||||
const paths = [
|
||||
tokenFileArg,
|
||||
resolve(homedir(), '.dashfront-token'),
|
||||
'./.dashfront-token',
|
||||
].filter(Boolean);
|
||||
|
||||
for (const p of paths) {
|
||||
try {
|
||||
const t = readFileSync(p, 'utf-8').trim();
|
||||
if (t) return t;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
console.error('No token found. Options:');
|
||||
console.error(' 1. Place JWT in ~/.dashfront-token');
|
||||
console.error(' 2. Pass --token <jwt>');
|
||||
console.error(' 3. Pass --token-file <path>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const TOKEN = loadToken();
|
||||
|
||||
// ── Patch globals so src/api/index.js works in Node ──────────────────────────
|
||||
|
||||
// Fake browser globals so src/api/index.js works in Node
|
||||
globalThis.window = {
|
||||
location: { origin: BASE_URL },
|
||||
};
|
||||
globalThis.document = {
|
||||
get cookie() { return ''; },
|
||||
set cookie(_v) { /* no-op */ },
|
||||
};
|
||||
|
||||
// Patch fetch to send the session cookie
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const url = typeof input === 'string' ? input : input?.url ?? String(input);
|
||||
const fullUrl = url.startsWith('http') ? url : `${BASE_URL}${url}`;
|
||||
|
||||
const headers = new Headers(init?.headers);
|
||||
headers.set('Cookie', `token=${TOKEN}`);
|
||||
|
||||
const res = await originalFetch(fullUrl, { ...init, headers });
|
||||
return res;
|
||||
};
|
||||
|
||||
// ── Load the API module ──────────────────────────────────────────────────────
|
||||
|
||||
const api = (await import('./index.js')).default;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const flatten = (obj, prefix = '') => {
|
||||
const entries = [];
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
if (typeof val === 'function') {
|
||||
entries.push([path, val]);
|
||||
} else if (typeof val === 'object' && val !== null) {
|
||||
entries.push(...flatten(val, path));
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
};
|
||||
|
||||
const methods = new Map(flatten(api));
|
||||
|
||||
function parseArg(raw) {
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === 'undefined') return undefined;
|
||||
if (trimmed === 'null') return null;
|
||||
if (trimmed === 'true') return true;
|
||||
if (trimmed === 'false') return false;
|
||||
try { return JSON.parse(trimmed); } catch { return trimmed; }
|
||||
}
|
||||
|
||||
function invoke(methodPath, argStrings = []) {
|
||||
const fn = methods.get(methodPath);
|
||||
if (!fn) {
|
||||
console.error(`Unknown method: ${methodPath}`);
|
||||
console.error(`Available: ${[...methods.keys()].join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const args = argStrings.map(parseArg);
|
||||
|
||||
return fn(...args)
|
||||
.then((result) => {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`[ERROR] ${err.message}`);
|
||||
if (err.status) console.error(` status: ${err.status}`);
|
||||
if (err.data) console.error(` data:`, err.data);
|
||||
});
|
||||
}
|
||||
|
||||
// ── One-shot mode ────────────────────────────────────────────────────────────
|
||||
|
||||
if (args.length > 0) {
|
||||
const [method, ...rest] = args;
|
||||
await invoke(method, rest);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ── Interactive REPL ─────────────────────────────────────────────────────────
|
||||
|
||||
console.log(`API Tester — ${BASE_URL}`);
|
||||
console.log(`Token: ${TOKEN.slice(0, 20)}…`);
|
||||
console.log(`Methods: ${[...methods.keys()].join(', ')}`);
|
||||
console.log('Type a method call, or "quit" to exit.\n');
|
||||
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
function prompt() {
|
||||
rl.question('api> ', async (line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed === 'quit' || trimmed === 'exit') {
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Parse: method arg1 arg2 ...
|
||||
// Arguments are space-separated; wrap objects in quotes: method '{"key":"val"}'
|
||||
const parts = trimmed.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
||||
const [method, ...rest] = parts.map((p) => p.replace(/^"|"$/g, ''));
|
||||
|
||||
await invoke(method, rest);
|
||||
prompt();
|
||||
});
|
||||
}
|
||||
|
||||
prompt();
|
||||
@@ -371,6 +371,20 @@ export const category = {
|
||||
body: { parentId },
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* PATCH /api/category/:id
|
||||
* Body: { pos: boolean } — toggles the POS (Point of Sale) flag.
|
||||
* → 200 { category: { id, pos } }
|
||||
*
|
||||
* PROPOSED — backend needs to implement this field.
|
||||
*/
|
||||
updatePos(categoryId, pos) {
|
||||
return http(`/api/category/${categoryId}`, {
|
||||
method: 'PATCH',
|
||||
body: { pos },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -437,6 +451,83 @@ export const company = {
|
||||
},
|
||||
};
|
||||
|
||||
export const posterminal = {
|
||||
/**
|
||||
* GET /api/posterminal → { posterminals: [{ id, name, token, pairingpin }] }
|
||||
* List all terminals for the selected company.
|
||||
*/
|
||||
list() {
|
||||
return http('/api/posterminal');
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/posterminal
|
||||
* Body: { name: string }
|
||||
* Creates a new terminal (auto-generates 6-digit pin).
|
||||
* → 200 { posterminal: { id, name, token, pairingpin } }
|
||||
*/
|
||||
create({ name }) {
|
||||
return http('/api/posterminal', {
|
||||
method: 'PUT',
|
||||
body: { name },
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE /api/posterminal/:id → 204
|
||||
* Deletes a terminal.
|
||||
*/
|
||||
delete(terminalId) {
|
||||
return http(`/api/posterminal/${terminalId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/posterminal/:id/category/:categoryId
|
||||
* Enable a category on a terminal.
|
||||
* → 200 { id: number, categoryIds: number[] }
|
||||
*/
|
||||
enableCategory(terminalId, categoryId) {
|
||||
return http(`/api/posterminal/${terminalId}/category/${categoryId}`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE /api/posterminal/:id/category/:categoryId
|
||||
* Disable a category on a terminal.
|
||||
* → 200 { id: number, categoryIds: number[] }
|
||||
*/
|
||||
disableCategory(terminalId, categoryId) {
|
||||
return http(`/api/posterminal/${terminalId}/category/${categoryId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/posterminal/:id/all
|
||||
* Enable all categories on a terminal.
|
||||
* → 200 { id: number, categoryIds: number[] }
|
||||
*/
|
||||
enableAllCategories(terminalId) {
|
||||
return http(`/api/posterminal/${terminalId}/all`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* DELETE /api/posterminal/:id/all
|
||||
* Disable all categories on a terminal.
|
||||
* → 200 { id: number, categoryIds: number[] }
|
||||
*/
|
||||
disableAllCategories(terminalId) {
|
||||
return http(`/api/posterminal/${terminalId}/all`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const auth = {
|
||||
async me() {
|
||||
try {
|
||||
@@ -488,4 +579,4 @@ export const auth = {
|
||||
},
|
||||
};
|
||||
|
||||
export default { auth, article, brand, category, company, thumb, unit };
|
||||
export default { auth, article, brand, category, company, posterminal, thumb, unit };
|
||||
|
||||
Reference in New Issue
Block a user