252 lines
8.9 KiB
JavaScript
252 lines
8.9 KiB
JavaScript
function enc(value) {
|
|
return encodeURIComponent(String(value));
|
|
}
|
|
|
|
function buildUrl(baseUrl, path) {
|
|
return new URL(path, baseUrl || window.location.origin).href;
|
|
}
|
|
|
|
function buildPath(path, query) {
|
|
if (!query) return path;
|
|
const params = new URLSearchParams();
|
|
for (const [key, value] of Object.entries(query)) {
|
|
if (value == null) continue;
|
|
params.set(key, String(value));
|
|
}
|
|
const qs = params.toString();
|
|
return qs ? `${path}?${qs}` : path;
|
|
}
|
|
|
|
/**
|
|
* Thin HTTP client for the dashback API (cookie auth, JSON / blob / empty).
|
|
*/
|
|
export class ApiClient {
|
|
#baseUrl;
|
|
|
|
constructor(baseUrl = window.location.origin) {
|
|
this.#baseUrl = baseUrl;
|
|
|
|
this.auth = {
|
|
/** POST /api/auth/login */
|
|
login: (body) => this.#json('POST', '/api/auth/login', { body }),
|
|
/** POST /api/auth/logout */
|
|
logout: () => this.#json('POST', '/api/auth/logout'),
|
|
/** GET /api/auth/me */
|
|
me: () => this.#json('GET', '/api/auth/me'),
|
|
/** PUT /api/auth/selected-company */
|
|
selectedCompany: (body) => this.#json('PUT', '/api/auth/selected-company', { body }),
|
|
};
|
|
|
|
this.health = {
|
|
/** GET /api/health */
|
|
get: () => this.#json('GET', '/api/health'),
|
|
};
|
|
|
|
this.company = {
|
|
/** GET /api/company */
|
|
list: () => this.#json('GET', '/api/company'),
|
|
/** POST /api/company */
|
|
create: (body) => this.#json('POST', '/api/company', { body }),
|
|
/** PUT /api/company/{id} */
|
|
update: (id, body) => this.#json('PUT', `/api/company/${enc(id)}`, { body }),
|
|
/** DELETE /api/company/{id} */
|
|
delete: (id) => this.#empty('DELETE', `/api/company/${enc(id)}`),
|
|
};
|
|
|
|
this.unit = {
|
|
/** GET /api/unit */
|
|
list: () => this.#json('GET', '/api/unit'),
|
|
/** PUT /api/unit */
|
|
upsert: (body) => this.#json('PUT', '/api/unit', { body }),
|
|
/** DELETE /api/unit?si=… */
|
|
delete: (si) => this.#empty('DELETE', '/api/unit', { query: { si } }),
|
|
};
|
|
|
|
this.brand = {
|
|
/** GET /api/brand */
|
|
list: () => this.#json('GET', '/api/brand'),
|
|
/** PUT /api/brand */
|
|
upsert: (body) => this.#json('PUT', '/api/brand', { body }),
|
|
/** DELETE /api/brand/{id} */
|
|
delete: (id) => this.#empty('DELETE', `/api/brand/${enc(id)}`),
|
|
};
|
|
|
|
this.category = {
|
|
/** GET /api/category/tree */
|
|
tree: () => this.#json('GET', '/api/category/tree'),
|
|
/** GET /api/category/export.zip */
|
|
exportZip: () => this.#blob('GET', '/api/category/export.zip'),
|
|
/** POST /api/category */
|
|
create: (body) => this.#json('POST', '/api/category', { body }),
|
|
/** GET /api/category/{id}/articles */
|
|
articles: (id) => this.#json('GET', `/api/category/${enc(id)}/articles`),
|
|
/** PATCH /api/category/{id} */
|
|
update: (id, body) => this.#json('PATCH', `/api/category/${enc(id)}`, { body }),
|
|
/** DELETE /api/category/{id} */
|
|
delete: (id) => this.#empty('DELETE', `/api/category/${enc(id)}`),
|
|
/** PUT /api/category/order */
|
|
order: (body) => this.#json('PUT', '/api/category/order', { body }),
|
|
/** POST /api/category/{id}/image — raw AVIF body */
|
|
addImage: (id, avifBytes) =>
|
|
this.#json('POST', `/api/category/${enc(id)}/image`, {
|
|
body: avifBytes,
|
|
contentType: 'image/avif',
|
|
raw: true,
|
|
}),
|
|
/** DELETE /api/category/{id}/image */
|
|
deleteImage: (id) => this.#json('DELETE', `/api/category/${enc(id)}/image`),
|
|
};
|
|
|
|
this.article = {
|
|
/** GET /api/articleIndex */
|
|
index: () => this.#json('GET', '/api/articleIndex'),
|
|
/** POST /api/article */
|
|
create: (body) => this.#json('POST', '/api/article', { body }),
|
|
/** GET /api/article/export.zip */
|
|
exportZip: (query = {}) => this.#blob('GET', '/api/article/export.zip', { query }),
|
|
/** GET /api/article/{sku} */
|
|
get: (sku) => this.#json('GET', `/api/article/${enc(sku)}`),
|
|
/** PATCH /api/article/{sku} */
|
|
update: (sku, body) => this.#json('PATCH', `/api/article/${enc(sku)}`, { body }),
|
|
/** DELETE /api/article/{sku} */
|
|
delete: (sku) => this.#empty('DELETE', `/api/article/${enc(sku)}`),
|
|
/** PUT /api/article/{sku}/prices */
|
|
setPrices: (sku, body) => this.#json('PUT', `/api/article/${enc(sku)}/prices`, { body }),
|
|
/** POST /api/article/{sku}/image — raw AVIF body */
|
|
addImage: (sku, avifBytes) =>
|
|
this.#json('POST', `/api/article/${enc(sku)}/image`, {
|
|
body: avifBytes,
|
|
contentType: 'image/avif',
|
|
raw: true,
|
|
}),
|
|
/** DELETE /api/article/{sku}/image/{fullHash} */
|
|
deleteImage: (sku, fullHash) =>
|
|
this.#empty('DELETE', `/api/article/${enc(sku)}/image/${enc(fullHash)}`),
|
|
/** PUT /api/article/{sku}/images/order */
|
|
orderImages: (sku, body) =>
|
|
this.#json('PUT', `/api/article/${enc(sku)}/images/order`, { body }),
|
|
/** PUT /api/article/{sku}/eans */
|
|
setEans: (sku, body) => this.#json('PUT', `/api/article/${enc(sku)}/eans`, { body }),
|
|
/** PUT /api/article/{sku}/brand */
|
|
setBrand: (sku, body) => this.#json('PUT', `/api/article/${enc(sku)}/brand`, { body }),
|
|
/** PUT /api/article/{sku}/weight */
|
|
setWeight: (sku, body) => this.#json('PUT', `/api/article/${enc(sku)}/weight`, { body }),
|
|
/** PUT /api/article/{sku}/categories */
|
|
setCategories: (sku, body) =>
|
|
this.#json('PUT', `/api/article/${enc(sku)}/categories`, { body }),
|
|
};
|
|
|
|
this.posterminal = {
|
|
/** GET /api/posterminal */
|
|
list: () => this.#json('GET', '/api/posterminal'),
|
|
/** PUT /api/posterminal */
|
|
upsert: (body) => this.#json('PUT', '/api/posterminal', { body }),
|
|
/** DELETE /api/posterminal/{id} */
|
|
delete: (id) => this.#empty('DELETE', `/api/posterminal/${enc(id)}`),
|
|
/** PUT /api/posterminal/{id}/category/{categoryId} */
|
|
setOn: (id, categoryId) =>
|
|
this.#json('PUT', `/api/posterminal/${enc(id)}/category/${enc(categoryId)}`),
|
|
/** DELETE /api/posterminal/{id}/category/{categoryId} */
|
|
setOff: (id, categoryId) =>
|
|
this.#json('DELETE', `/api/posterminal/${enc(id)}/category/${enc(categoryId)}`),
|
|
/** PUT /api/posterminal/{id}/all */
|
|
setOnAll: (id) => this.#json('PUT', `/api/posterminal/${enc(id)}/all`),
|
|
/** DELETE /api/posterminal/{id}/all */
|
|
setOffAll: (id) => this.#json('DELETE', `/api/posterminal/${enc(id)}/all`),
|
|
};
|
|
|
|
this.image = {
|
|
/** GET /api/image/{fullHash} → AVIF blob */
|
|
get: (fullHash) => this.#blob('GET', `/api/image/${enc(fullHash)}`),
|
|
/** Absolute URL for <img src> (same-origin / cookie auth). */
|
|
url: (fullHash) => buildUrl(this.#baseUrl, `/api/image/${enc(fullHash)}`),
|
|
};
|
|
|
|
this.thumb = {
|
|
/** GET /api/thumb/{fullHash} → AVIF blob */
|
|
get: (fullHash) => this.#blob('GET', `/api/thumb/${enc(fullHash)}`),
|
|
/** Absolute URL for <img src> (same-origin / cookie auth). */
|
|
url: (fullHash) => buildUrl(this.#baseUrl, `/api/thumb/${enc(fullHash)}`),
|
|
};
|
|
}
|
|
|
|
/** Absolute URL for a same-origin API path (e.g. SSE). */
|
|
urlFor(path) {
|
|
return buildUrl(this.#baseUrl, path);
|
|
}
|
|
|
|
async #json(method, path, { body, query, contentType, raw } = {}) {
|
|
const url = buildPath(path, query);
|
|
const headers = {};
|
|
let payload;
|
|
if (body !== undefined) {
|
|
if (raw) {
|
|
headers['Content-Type'] = contentType;
|
|
payload = body;
|
|
} else {
|
|
headers['Content-Type'] = contentType || 'application/json';
|
|
payload = JSON.stringify(body);
|
|
}
|
|
}
|
|
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers,
|
|
credentials: 'include',
|
|
body: payload,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
let message = res.statusText;
|
|
let data;
|
|
try {
|
|
data = await res.json();
|
|
message = data.error || data.message || message;
|
|
} catch {
|
|
message = (await res.text().catch(() => message)) || message;
|
|
}
|
|
const err = new Error(message || `Request failed (${res.status})`);
|
|
err.status = res.status;
|
|
err.path = url;
|
|
err.method = method;
|
|
err.data = data;
|
|
throw err;
|
|
}
|
|
if (res.status === 204) return null;
|
|
return res.json();
|
|
}
|
|
|
|
async #empty(method, path, { query } = {}) {
|
|
await this.#json(method, path, { query });
|
|
return null;
|
|
}
|
|
|
|
async #blob(method, path, { query } = {}) {
|
|
const url = buildPath(path, query);
|
|
const res = await fetch(url, {
|
|
method,
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!res.ok) {
|
|
let message = res.statusText;
|
|
let data;
|
|
try {
|
|
data = await res.json();
|
|
message = data.error || data.message || message;
|
|
} catch {
|
|
message = (await res.text().catch(() => message)) || message;
|
|
}
|
|
const err = new Error(message || `Request failed (${res.status})`);
|
|
err.status = res.status;
|
|
err.path = url;
|
|
err.method = method;
|
|
err.data = data;
|
|
throw err;
|
|
}
|
|
return res.blob();
|
|
}
|
|
}
|
|
|
|
export default ApiClient;
|