@tecsom/apipos-js 0.0.0-stage → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +398 -2
- package/dist/apipos.d.ts +67 -0
- package/dist/apipos.js +291 -0
- package/dist/http.d.ts +36 -0
- package/dist/http.js +113 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/printer.d.ts +61 -0
- package/dist/printer.js +189 -0
- package/dist/strip-accents.d.ts +21 -0
- package/dist/strip-accents.js +57 -0
- package/dist/ticket-preview.d.ts +33 -0
- package/dist/ticket-preview.js +115 -0
- package/dist/types.d.ts +506 -0
- package/dist/types.js +14 -0
- package/dist/version-check.d.ts +22 -0
- package/dist/version-check.js +72 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/package.json +56 -4
package/dist/apipos.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Cliente de Apipos, el agente local de impresión que corre en la caja
|
|
3
|
+
* (por defecto en http://127.0.0.1:50432). Envuelve los endpoints de
|
|
4
|
+
* APIPOS_API.md y crea las impresoras (`ApiPosPrinter`) que mandan los trabajos.
|
|
5
|
+
*
|
|
6
|
+
* La librería no sabe nada del POS que la usa: lo que cada app quiera hacerle al
|
|
7
|
+
* texto antes de imprimir (limpieza, quitar acentos) entra por el hook
|
|
8
|
+
* `transformContent`.
|
|
9
|
+
*
|
|
10
|
+
* Handshake de versión: cada respuesta trae `X-Apipos-Version`; si no coincide
|
|
11
|
+
* con la versión de la librería, avisa UNA vez por versión de Apipos: por
|
|
12
|
+
* defecto con `console.warn`, una vez por página; con `onVersionMismatch`, una
|
|
13
|
+
* vez por cliente.
|
|
14
|
+
*/
|
|
15
|
+
import { base64ToBlob, isRecord, isUnknownArray, request as sendRequest, unexpectedResponse } from './http.js';
|
|
16
|
+
import { ApiPosPrinter } from './printer.js';
|
|
17
|
+
import { VERSION } from './version.js';
|
|
18
|
+
import { describeVersions } from './version-check.js';
|
|
19
|
+
const DEFAULT_BASE_URL = 'http://127.0.0.1:50432';
|
|
20
|
+
// El `fetch` global se busca en CADA petición, como en v1 (que llamaba a `fetch`
|
|
21
|
+
// directo): si alguien lo reemplaza después de crear el cliente (dobles de
|
|
22
|
+
// prueba, instrumentación), se usa el vigente. Se invoca como método de
|
|
23
|
+
// `globalThis`, así el navegador no lanza "Illegal invocation".
|
|
24
|
+
const globalFetch = (url, init) => globalThis.fetch(url, init);
|
|
25
|
+
// Default de `transformContent`: copia superficial. Quien imprime puede reusar su
|
|
26
|
+
// arreglo (otra copia, el reintento sin logo) y la librería nunca lo muta.
|
|
27
|
+
const copyContent = content => content.slice();
|
|
28
|
+
// Default de `onVersionMismatch`: el aviso a la consola del navegador. Busca
|
|
29
|
+
// `console.warn` al avisar (no al crear el cliente), así se puede interceptar.
|
|
30
|
+
const warnInConsole = (info) => {
|
|
31
|
+
console.warn(info.message);
|
|
32
|
+
};
|
|
33
|
+
// Versiones de Apipos (`String(info.backend)`; `'null'` = no la reporta) ya
|
|
34
|
+
// avisadas con el `console.warn` por defecto. Vive en el MÓDULO, no en cada
|
|
35
|
+
// cliente: una app puede crear un `new Apipos()` por impresión y el aviso debe
|
|
36
|
+
// salir una vez por página, no en cada ticket.
|
|
37
|
+
const warnedInConsole = new Set();
|
|
38
|
+
function isVersionStatus(value) {
|
|
39
|
+
return value === 'equal' || value === 'client_older' || value === 'client_newer' || value === 'unknown';
|
|
40
|
+
}
|
|
41
|
+
function isStringOrNull(value) {
|
|
42
|
+
return value === null || typeof value === 'string';
|
|
43
|
+
}
|
|
44
|
+
function isServerClientStatus(value) {
|
|
45
|
+
return (isRecord(value) &&
|
|
46
|
+
isStringOrNull(value.name) &&
|
|
47
|
+
isStringOrNull(value.version) &&
|
|
48
|
+
typeof value.server === 'string' &&
|
|
49
|
+
isVersionStatus(value.status) &&
|
|
50
|
+
typeof value.message === 'string');
|
|
51
|
+
}
|
|
52
|
+
// `version` y `client` no existen en Apipos anteriores a 1.0.6: su ausencia no
|
|
53
|
+
// es un error (justo con esos backends importa que checkHealth siga funcionando).
|
|
54
|
+
function isHealthData(value) {
|
|
55
|
+
return (isRecord(value) &&
|
|
56
|
+
typeof value.status === 'string' &&
|
|
57
|
+
typeof value.os === 'string' &&
|
|
58
|
+
typeof value.platform === 'string' &&
|
|
59
|
+
typeof value.backend === 'string' &&
|
|
60
|
+
typeof value.port === 'number' &&
|
|
61
|
+
(value.version === undefined || typeof value.version === 'string') &&
|
|
62
|
+
(value.client === undefined || value.client === null || isServerClientStatus(value.client)));
|
|
63
|
+
}
|
|
64
|
+
function isStringArray(value) {
|
|
65
|
+
return isUnknownArray(value) && value.every(item => typeof item === 'string');
|
|
66
|
+
}
|
|
67
|
+
function isFontFormat(value) {
|
|
68
|
+
return value === 'ttf' || value === 'otf' || value === 'ttc' || value === 'otc';
|
|
69
|
+
}
|
|
70
|
+
function isFontFace(value) {
|
|
71
|
+
return (isRecord(value) &&
|
|
72
|
+
typeof value.style === 'string' &&
|
|
73
|
+
typeof value.bold === 'boolean' &&
|
|
74
|
+
typeof value.italic === 'boolean' &&
|
|
75
|
+
typeof value.weight === 'number' &&
|
|
76
|
+
typeof value.full_name === 'string' &&
|
|
77
|
+
isStringOrNull(value.postscript_name) &&
|
|
78
|
+
typeof value.path === 'string' &&
|
|
79
|
+
typeof value.index === 'number' &&
|
|
80
|
+
isFontFormat(value.format));
|
|
81
|
+
}
|
|
82
|
+
function isFontFamily(value) {
|
|
83
|
+
return (isRecord(value) &&
|
|
84
|
+
typeof value.family === 'string' &&
|
|
85
|
+
typeof value.monospace === 'boolean' &&
|
|
86
|
+
isUnknownArray(value.faces) &&
|
|
87
|
+
value.faces.every(isFontFace));
|
|
88
|
+
}
|
|
89
|
+
/** Forma de `data` de GET /fonts (APIPOS_API.md, "Fuentes del sistema"). */
|
|
90
|
+
function isFontCatalogData(value) {
|
|
91
|
+
return (isRecord(value) &&
|
|
92
|
+
isUnknownArray(value.families) &&
|
|
93
|
+
value.families.every(isFontFamily) &&
|
|
94
|
+
typeof value.count === 'number' &&
|
|
95
|
+
isStringArray(value.builtin) &&
|
|
96
|
+
isStringArray(value.dirs) &&
|
|
97
|
+
typeof value.scanned_at === 'string');
|
|
98
|
+
}
|
|
99
|
+
function isRenderFormat(value) {
|
|
100
|
+
return value === 'pdf' || value === 'png';
|
|
101
|
+
}
|
|
102
|
+
function isFontSource(value) {
|
|
103
|
+
return value === 'system' || value === 'builtin' || value === 'fallback';
|
|
104
|
+
}
|
|
105
|
+
function isRenderedFont(value) {
|
|
106
|
+
return (isRecord(value) &&
|
|
107
|
+
typeof value.family === 'string' &&
|
|
108
|
+
typeof value.resolved === 'string' &&
|
|
109
|
+
isFontSource(value.source) &&
|
|
110
|
+
typeof value.bold === 'boolean' &&
|
|
111
|
+
typeof value.italic === 'boolean');
|
|
112
|
+
}
|
|
113
|
+
function isPageSizeMm(value) {
|
|
114
|
+
return isRecord(value) && typeof value.width === 'number' && typeof value.height === 'number';
|
|
115
|
+
}
|
|
116
|
+
/** Forma de `data` de POST /render (APIPOS_API.md, "Vista previa"). `dpi` es `null` con `pdf`. */
|
|
117
|
+
function isRenderData(value) {
|
|
118
|
+
return (isRecord(value) &&
|
|
119
|
+
isRenderFormat(value.format) &&
|
|
120
|
+
(value.dpi === null || typeof value.dpi === 'number') &&
|
|
121
|
+
typeof value.base64 === 'string' &&
|
|
122
|
+
isPageSizeMm(value.page_mm) &&
|
|
123
|
+
typeof value.pages === 'number' &&
|
|
124
|
+
isUnknownArray(value.fonts) &&
|
|
125
|
+
value.fonts.every(isRenderedFont) &&
|
|
126
|
+
isStringArray(value.warnings));
|
|
127
|
+
}
|
|
128
|
+
// Mime del `blob` de una vista previa según el formato que devolvió Apipos.
|
|
129
|
+
const RENDER_MIME = { pdf: 'application/pdf', png: 'image/png' };
|
|
130
|
+
export class Apipos {
|
|
131
|
+
#baseUrl;
|
|
132
|
+
#fetch;
|
|
133
|
+
#transformContent;
|
|
134
|
+
#onVersionMismatch;
|
|
135
|
+
// Con `onVersionMismatch` inyectado, las versiones ya avisadas son de ESTE
|
|
136
|
+
// cliente: quien inyecta el hook controla su propia política.
|
|
137
|
+
#warnedBackends = new Set();
|
|
138
|
+
constructor(options = {}) {
|
|
139
|
+
this.#baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
140
|
+
this.#fetch = options.fetch ?? globalFetch;
|
|
141
|
+
this.#transformContent = options.transformContent ?? copyContent;
|
|
142
|
+
this.#onVersionMismatch = options.onVersionMismatch ?? null;
|
|
143
|
+
}
|
|
144
|
+
/** URL base de Apipos (default `http://127.0.0.1:50432`). */
|
|
145
|
+
get baseUrl() {
|
|
146
|
+
return this.#baseUrl;
|
|
147
|
+
}
|
|
148
|
+
/** Alias de `baseUrl` (compatibilidad con v1). */
|
|
149
|
+
get apiUrl() {
|
|
150
|
+
return this.#baseUrl;
|
|
151
|
+
}
|
|
152
|
+
/** Compatibilidad con v1: cambia la URL base para las siguientes peticiones. */
|
|
153
|
+
set apiUrl(url) {
|
|
154
|
+
this.#baseUrl = url;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Verifica que el servicio de impresión esté disponible.
|
|
158
|
+
* GET /health → { data: { status, version, os, platform, backend, port, client }, message, status }
|
|
159
|
+
*
|
|
160
|
+
* Devuelve `{ ok, data, version }`: `ok` es `true` si el servicio responde
|
|
161
|
+
* "ok" y `version` = `describeVersions(VERSION, data.version)`
|
|
162
|
+
* (`{ client, backend, status, message }`).
|
|
163
|
+
*
|
|
164
|
+
* `data.version` pasa por el MISMO aviso deduplicado que el header: si el
|
|
165
|
+
* header ya avisó esa versión no se repite, y si la respuesta no trajo
|
|
166
|
+
* `headers` (dobles de `fetch`) el aviso sale de aquí.
|
|
167
|
+
*
|
|
168
|
+
* Nunca lanza: ante cualquier error devuelve `{ ok: false, data: null, version: null }`.
|
|
169
|
+
*/
|
|
170
|
+
async checkHealth() {
|
|
171
|
+
try {
|
|
172
|
+
const data = await this.request('GET', '/health');
|
|
173
|
+
if (!isHealthData(data))
|
|
174
|
+
throw unexpectedResponse('/health');
|
|
175
|
+
const version = describeVersions(VERSION, data.version ?? null);
|
|
176
|
+
this.#notifyVersion(version);
|
|
177
|
+
return { ok: data.status === 'ok', data, version };
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
console.error('Error checking Apipos health:', e);
|
|
181
|
+
return { ok: false, data: null, version: null };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** GET /printers → nombres exactos de las impresoras instaladas; `[]` si falla. */
|
|
185
|
+
async listPrinters() {
|
|
186
|
+
try {
|
|
187
|
+
const data = await this.request('GET', '/printers');
|
|
188
|
+
if (!isStringArray(data))
|
|
189
|
+
throw unexpectedResponse('/printers');
|
|
190
|
+
return data;
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
console.error('Error fetching printers:', e);
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Fuentes instaladas en la máquina de Apipos, para elegir `settings.font.family`
|
|
199
|
+
* o `font_family` del renderizador PDF.
|
|
200
|
+
* GET /fonts (con `refresh: true` → GET /fonts?refresh=1, que vuelve a
|
|
201
|
+
* escanear; útil tras instalar una fuente).
|
|
202
|
+
*
|
|
203
|
+
* Devuelve `{ families, builtin, count, dirs, scannedAt }`: `builtin`
|
|
204
|
+
* (`sans-serif`, `serif`, `monospace`) existe siempre, aunque la máquina no
|
|
205
|
+
* tenga fuentes. A diferencia de `listPrinters`, LANZA ante cualquier error
|
|
206
|
+
* (`ApiposError`, sin conexión o `Respuesta inesperada de Apipos en /fonts`).
|
|
207
|
+
*/
|
|
208
|
+
async listFonts({ refresh = false } = {}) {
|
|
209
|
+
const data = await this.request('GET', '/fonts', refresh ? { query: { refresh: true } } : {});
|
|
210
|
+
if (!isFontCatalogData(data))
|
|
211
|
+
throw unexpectedResponse('/fonts');
|
|
212
|
+
return {
|
|
213
|
+
families: data.families,
|
|
214
|
+
builtin: data.builtin,
|
|
215
|
+
count: data.count,
|
|
216
|
+
dirs: data.dirs,
|
|
217
|
+
scannedAt: data.scanned_at
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Vista previa REAL de un ticket: Apipos lo maqueta con el mismo renderizador
|
|
222
|
+
* PDF que imprime (`settings.renderer: 'pdf'`), sin validar ni tocar ninguna
|
|
223
|
+
* impresora. POST /render con `{ settings, content, format, dpi }`.
|
|
224
|
+
*
|
|
225
|
+
* `content` pasa por `transformContent`, igual que al imprimir: lo que se ve
|
|
226
|
+
* es lo que saldría en papel. `dpi` solo aplica con `png` (72–600).
|
|
227
|
+
*
|
|
228
|
+
* Devuelve `{ format, dpi, base64, pageMm, pages, fonts, warnings, blob }`;
|
|
229
|
+
* `blob` es un `Blob` `application/pdf` o `image/png` listo para
|
|
230
|
+
* `URL.createObjectURL`, y `dpi` es `null` con `pdf`. Lanza ante cualquier error.
|
|
231
|
+
*/
|
|
232
|
+
async renderTicket(content, { settings = {}, format = 'pdf', dpi = 203 } = {}) {
|
|
233
|
+
const data = await this.request('POST', '/render', {
|
|
234
|
+
body: { settings, content: this.applyTransform(content), format, dpi }
|
|
235
|
+
});
|
|
236
|
+
if (!isRenderData(data))
|
|
237
|
+
throw unexpectedResponse('/render');
|
|
238
|
+
return {
|
|
239
|
+
format: data.format,
|
|
240
|
+
dpi: data.dpi,
|
|
241
|
+
base64: data.base64,
|
|
242
|
+
pageMm: data.page_mm,
|
|
243
|
+
pages: data.pages,
|
|
244
|
+
fonts: data.fonts,
|
|
245
|
+
warnings: data.warnings,
|
|
246
|
+
blob: base64ToBlob(data.base64, RENDER_MIME[data.format])
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Impresora con nombre exacto (de `listPrinters()`) y sus `settings`
|
|
251
|
+
* (`paper_size` numérico obligatorio). Sin nombre devuelve `null` y lo
|
|
252
|
+
* reporta en consola, como v1.
|
|
253
|
+
*/
|
|
254
|
+
createPrinter(name, settings = {}) {
|
|
255
|
+
if (!name) {
|
|
256
|
+
console.error('Printer name is required');
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
return new ApiPosPrinter(name, settings, this);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Acceso a la red para `ApiPosPrinter` (y para endpoints que la librería aún
|
|
263
|
+
* no envuelve): devuelve el `data` del sobre o lanza `ApiposError`.
|
|
264
|
+
*/
|
|
265
|
+
request(method, path, options) {
|
|
266
|
+
return sendRequest({ baseUrl: this.#baseUrl, fetch: this.#fetch, onVersion: info => this.#notifyVersion(info) }, method, path, options);
|
|
267
|
+
}
|
|
268
|
+
/** Pasa el `content` de un trabajo por `transformContent`. */
|
|
269
|
+
applyTransform(content) {
|
|
270
|
+
return this.#transformContent(content);
|
|
271
|
+
}
|
|
272
|
+
// Aviso de versión: solo si difieren y una vez por versión de Apipos (por
|
|
273
|
+
// página con el aviso por defecto; por cliente con `onVersionMismatch`). Es
|
|
274
|
+
// informativo: si `onVersionMismatch` lanza, se reporta y la petición (la
|
|
275
|
+
// impresión) sigue su curso.
|
|
276
|
+
#notifyVersion(info) {
|
|
277
|
+
if (info.status === 'equal')
|
|
278
|
+
return;
|
|
279
|
+
const key = String(info.backend);
|
|
280
|
+
const warned = this.#onVersionMismatch ? this.#warnedBackends : warnedInConsole;
|
|
281
|
+
if (warned.has(key))
|
|
282
|
+
return;
|
|
283
|
+
warned.add(key);
|
|
284
|
+
try {
|
|
285
|
+
(this.#onVersionMismatch ?? warnInConsole)(info);
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
console.error('Error in Apipos onVersionMismatch:', e);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { HttpMethod, RequestContext, RequestOptions } from './types.js';
|
|
2
|
+
export declare const CLIENT_HEADER = "X-Apipos-Client";
|
|
3
|
+
export declare const VERSION_HEADER = "X-Apipos-Version";
|
|
4
|
+
export declare const CONNECT_ERROR: (baseUrl: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Error de negocio que respondió Apipos (`status: 'error'` o HTTP no-2xx).
|
|
7
|
+
* `message` es el del backend (p. ej. `Printer 'X' not found.`) y `data` lo que
|
|
8
|
+
* traiga el sobre (p. ej. `{ available: [...] }` con las impresoras válidas).
|
|
9
|
+
* Sigue siendo `instanceof Error`.
|
|
10
|
+
*/
|
|
11
|
+
export declare class ApiposError extends Error {
|
|
12
|
+
data: unknown;
|
|
13
|
+
constructor(message: string, data?: unknown);
|
|
14
|
+
}
|
|
15
|
+
/** Objeto JSON (no `null`). Los arreglos también pasan: se leen sus claves como `undefined`. */
|
|
16
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
17
|
+
/** `Array.isArray` sin dejar escapar elementos sin tipo. */
|
|
18
|
+
export declare function isUnknownArray(value: unknown): value is readonly unknown[];
|
|
19
|
+
/** Error para cuando Apipos respondió éxito pero con una forma que no es la del contrato. */
|
|
20
|
+
export declare function unexpectedResponse(path: string): Error;
|
|
21
|
+
/**
|
|
22
|
+
* base64 (puro o data URI `data:<mime>;base64,...`) → `Blob` del `mime` dado
|
|
23
|
+
* (el del data URI se descarta). Usa `atob`, que existe en el navegador y en
|
|
24
|
+
* Node: así lo que devuelve `POST /render` se puede abrir, descargar o mostrar
|
|
25
|
+
* con `URL.createObjectURL(blob)`. Un base64 inválido lanza el error de `atob`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function base64ToBlob(base64: string, mime: string): Blob;
|
|
28
|
+
/**
|
|
29
|
+
* Único punto de red de la librería. Manda `X-Apipos-Client: apipos-js/<VERSION>`,
|
|
30
|
+
* evalúa `X-Apipos-Version` (handshake: `ctx.onVersion`), interpreta el sobre
|
|
31
|
+
* `{ status, data, message }` y devuelve `data` (o `null`), o lanza
|
|
32
|
+
* `ApiposError(message)` con `error.data` (p. ej. `available`).
|
|
33
|
+
*
|
|
34
|
+
* @param path p. ej. `'/print'`
|
|
35
|
+
*/
|
|
36
|
+
export declare function request(ctx: RequestContext, method: HttpMethod, path: string, { body, query }?: RequestOptions): Promise<unknown>;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Único punto de red de la librería: todo lo que habla con Apipos pasa por
|
|
3
|
+
* `request()`. Aquí viven el header del cliente, el handshake de versión, el
|
|
4
|
+
* manejo del sobre `{ status, data, message }` y los mensajes de error que ve
|
|
5
|
+
* el usuario del POS.
|
|
6
|
+
*/
|
|
7
|
+
import { CLIENT_NAME, VERSION } from './version.js';
|
|
8
|
+
import { describeVersions } from './version-check.js';
|
|
9
|
+
export const CLIENT_HEADER = 'X-Apipos-Client';
|
|
10
|
+
export const VERSION_HEADER = 'X-Apipos-Version';
|
|
11
|
+
export const CONNECT_ERROR = (baseUrl) => `No se pudo conectar con Apipos (${baseUrl}). ¿Está abierta la app de bandeja?`;
|
|
12
|
+
/**
|
|
13
|
+
* Error de negocio que respondió Apipos (`status: 'error'` o HTTP no-2xx).
|
|
14
|
+
* `message` es el del backend (p. ej. `Printer 'X' not found.`) y `data` lo que
|
|
15
|
+
* traiga el sobre (p. ej. `{ available: [...] }` con las impresoras válidas).
|
|
16
|
+
* Sigue siendo `instanceof Error`.
|
|
17
|
+
*/
|
|
18
|
+
export class ApiposError extends Error {
|
|
19
|
+
data;
|
|
20
|
+
constructor(message, data = null) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'ApiposError';
|
|
23
|
+
this.data = data;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Objeto JSON (no `null`). Los arreglos también pasan: se leen sus claves como `undefined`. */
|
|
27
|
+
export function isRecord(value) {
|
|
28
|
+
return typeof value === 'object' && value !== null;
|
|
29
|
+
}
|
|
30
|
+
/** `Array.isArray` sin dejar escapar elementos sin tipo. */
|
|
31
|
+
export function isUnknownArray(value) {
|
|
32
|
+
return Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
/** Error para cuando Apipos respondió éxito pero con una forma que no es la del contrato. */
|
|
35
|
+
export function unexpectedResponse(path) {
|
|
36
|
+
return new Error(`Respuesta inesperada de Apipos en ${path}`);
|
|
37
|
+
}
|
|
38
|
+
// Prefijo de un data URI en base64 (`data:image/png;base64,`), con o sin mime.
|
|
39
|
+
const DATA_URI_PREFIX = /^data:[^,]*;base64,/i;
|
|
40
|
+
/**
|
|
41
|
+
* base64 (puro o data URI `data:<mime>;base64,...`) → `Blob` del `mime` dado
|
|
42
|
+
* (el del data URI se descarta). Usa `atob`, que existe en el navegador y en
|
|
43
|
+
* Node: así lo que devuelve `POST /render` se puede abrir, descargar o mostrar
|
|
44
|
+
* con `URL.createObjectURL(blob)`. Un base64 inválido lanza el error de `atob`.
|
|
45
|
+
*/
|
|
46
|
+
export function base64ToBlob(base64, mime) {
|
|
47
|
+
const binary = atob(base64.replace(DATA_URI_PREFIX, ''));
|
|
48
|
+
const bytes = new Uint8Array(binary.length);
|
|
49
|
+
for (let index = 0; index < binary.length; index++) {
|
|
50
|
+
bytes[index] = binary.charCodeAt(index);
|
|
51
|
+
}
|
|
52
|
+
return new Blob([bytes], { type: mime });
|
|
53
|
+
}
|
|
54
|
+
function queryString(query) {
|
|
55
|
+
if (!query)
|
|
56
|
+
return '';
|
|
57
|
+
const params = new URLSearchParams();
|
|
58
|
+
for (const [key, value] of Object.entries(query)) {
|
|
59
|
+
if (value === null || value === undefined)
|
|
60
|
+
continue;
|
|
61
|
+
params.append(key, typeof value === 'boolean' ? (value ? '1' : '0') : String(value));
|
|
62
|
+
}
|
|
63
|
+
const text = params.toString();
|
|
64
|
+
return text ? `?${text}` : '';
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Único punto de red de la librería. Manda `X-Apipos-Client: apipos-js/<VERSION>`,
|
|
68
|
+
* evalúa `X-Apipos-Version` (handshake: `ctx.onVersion`), interpreta el sobre
|
|
69
|
+
* `{ status, data, message }` y devuelve `data` (o `null`), o lanza
|
|
70
|
+
* `ApiposError(message)` con `error.data` (p. ej. `available`).
|
|
71
|
+
*
|
|
72
|
+
* @param path p. ej. `'/print'`
|
|
73
|
+
*/
|
|
74
|
+
export async function request(ctx, method, path, { body, query } = {}) {
|
|
75
|
+
const headers = { [CLIENT_HEADER]: `${CLIENT_NAME}/${VERSION}` };
|
|
76
|
+
const init = { method, headers };
|
|
77
|
+
if (body !== undefined) {
|
|
78
|
+
headers['Content-Type'] = 'application/json';
|
|
79
|
+
init.body = JSON.stringify(body);
|
|
80
|
+
}
|
|
81
|
+
// Se llama suelta (no como `ctx.fetch(...)`): un `fetch` nativo inyectado tal
|
|
82
|
+
// cual (`{ fetch: window.fetch }`) falla con "Illegal invocation" si recibe
|
|
83
|
+
// otro objeto como `this`.
|
|
84
|
+
const send = ctx.fetch;
|
|
85
|
+
let response;
|
|
86
|
+
try {
|
|
87
|
+
response = await send(ctx.baseUrl + path + queryString(query), init);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new Error(CONNECT_ERROR(ctx.baseUrl));
|
|
91
|
+
}
|
|
92
|
+
let payload = null;
|
|
93
|
+
try {
|
|
94
|
+
payload = await response.json();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// respuesta sin cuerpo JSON
|
|
98
|
+
}
|
|
99
|
+
// Handshake: TODA respuesta de Apipos ≥ 1.0.6 trae `X-Apipos-Version` (también
|
|
100
|
+
// las de error, por eso va antes de lanzar). Sin el header → `unknown`: es un
|
|
101
|
+
// Apipos anterior. Si la respuesta no trae `headers` (dobles de `fetch` de
|
|
102
|
+
// pruebas) no hay nada que comparar y no se evalúa.
|
|
103
|
+
if (response.headers) {
|
|
104
|
+
ctx.onVersion?.(describeVersions(VERSION, response.headers.get(VERSION_HEADER)));
|
|
105
|
+
}
|
|
106
|
+
const envelope = isRecord(payload) ? payload : null;
|
|
107
|
+
const data = envelope?.data ?? null;
|
|
108
|
+
if (!response.ok || envelope?.status === 'error') {
|
|
109
|
+
const message = envelope?.message;
|
|
110
|
+
throw new ApiposError((typeof message === 'string' && message) || response.statusText || 'Error de impresión', data);
|
|
111
|
+
}
|
|
112
|
+
return data;
|
|
113
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Apipos } from './apipos.js';
|
|
2
|
+
export { ApiPosPrinter } from './printer.js';
|
|
3
|
+
export { VERSION, CLIENT_NAME } from './version.js';
|
|
4
|
+
export { parseVersion, compareVersions, describeVersions } from './version-check.js';
|
|
5
|
+
export { ApiposError, base64ToBlob } from './http.js';
|
|
6
|
+
export { stripAccents, stripAccentsFromContent } from './strip-accents.js';
|
|
7
|
+
export { ticketColumns, TICKET_PREVIEW_CSS, renderTicketHtml, printTicketHtml, renderTicketPreviewPng } from './ticket-preview.js';
|
|
8
|
+
export type * from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// API pública de @tecsom/apipos-js. Lo que no se exporta aquí (ni en los
|
|
2
|
+
// subpaths de package.json) es interno y puede cambiar sin aviso.
|
|
3
|
+
export { Apipos } from './apipos.js';
|
|
4
|
+
export { ApiPosPrinter } from './printer.js';
|
|
5
|
+
export { VERSION, CLIENT_NAME } from './version.js';
|
|
6
|
+
export { parseVersion, compareVersions, describeVersions } from './version-check.js';
|
|
7
|
+
export { ApiposError, base64ToBlob } from './http.js';
|
|
8
|
+
export { stripAccents, stripAccentsFromContent } from './strip-accents.js';
|
|
9
|
+
export { ticketColumns, TICKET_PREVIEW_CSS, renderTicketHtml, printTicketHtml, renderTicketPreviewPng } from './ticket-preview.js';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Apipos } from './apipos.js';
|
|
2
|
+
import type { OpenDrawerResult, PrinterRenderOptions, PrintOptions, PrintPdfOptions, PrintResult, PrintSettings, RenderResult, TicketContent } from './types.js';
|
|
3
|
+
export declare class ApiPosPrinter {
|
|
4
|
+
#private;
|
|
5
|
+
settings: PrintSettings;
|
|
6
|
+
/**
|
|
7
|
+
* Valida `settings.paper_size` (number, obligatorio): lanza
|
|
8
|
+
* `Missing required setting: paper_size` o
|
|
9
|
+
* `Invalid type for setting paper_size: expected number, got <tipo>`.
|
|
10
|
+
*/
|
|
11
|
+
constructor(name: string, settings: PrintSettings | undefined, apipos: Apipos);
|
|
12
|
+
validateSettings(): void;
|
|
13
|
+
get name(): string;
|
|
14
|
+
get apiUrl(): string;
|
|
15
|
+
/**
|
|
16
|
+
* Trabajo RAW: `content` es la lista de elementos del ticket.
|
|
17
|
+
*
|
|
18
|
+
* Lo que haya que hacerle al texto (p. ej. una limpieza base o un
|
|
19
|
+
* interruptor por dispositivo "sin acentos ni ñ") lo aplica el
|
|
20
|
+
* `transformContent` del cliente en CADA trabajo, no al crear la impresora:
|
|
21
|
+
* así un cambio de ajustes vale desde el siguiente ticket sin recargar.
|
|
22
|
+
*
|
|
23
|
+
* SIN `options` (o con `{}`) el payload es el de v1, byte a byte:
|
|
24
|
+
* `{ printer, type: 'RAW', settings: this.settings, content }`.
|
|
25
|
+
*
|
|
26
|
+
* Con `options` se agregan claves SOLO dentro de `settings` (nunca al nivel
|
|
27
|
+
* raíz) y `this.settings` no se modifica:
|
|
28
|
+
* renderer → renderer ('escpos' | 'pdf'; con 'pdf' Apipos maqueta el ticket
|
|
29
|
+
* como PDF con fuentes del sistema y lo imprime por la ruta PDF)
|
|
30
|
+
* pdfMode → pdf_mode ; font → font ({ family, size, line_height })
|
|
31
|
+
* marginsMm → margins_mm ; widthMm → width_mm ; maxHeightMm → max_height_mm
|
|
32
|
+
* cut → cut ; settings → se mezcla al final y gana (claves nuevas del backend)
|
|
33
|
+
* Las opciones `undefined` no viajan.
|
|
34
|
+
*/
|
|
35
|
+
print(content: TicketContent, options?: PrintOptions): Promise<PrintResult | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Abre el cajón de dinero conectado a ESTA impresora, sin imprimir nada.
|
|
38
|
+
* POST /open-withdrawer con `{ printer }` → `{ printer }`. Lanza
|
|
39
|
+
* `ApiposError` si Apipos responde error (p. ej. `Printer 'X' not found.`).
|
|
40
|
+
*/
|
|
41
|
+
openDrawer(): Promise<OpenDrawerResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Vista previa con los settings de ESTA impresora y las mismas `options` que
|
|
44
|
+
* `print()`, con el mismo mapeo a `settings`:
|
|
45
|
+
* `apipos.renderTicket(content, { settings: { ...this.settings, ...mapeo(options) }, format, dpi })`.
|
|
46
|
+
* No imprime ni valida la impresora; `this.settings` no se modifica. Default
|
|
47
|
+
* `png` a 203 dpi (la resolución de una tiquetera: 72 mm → 576 px de ancho).
|
|
48
|
+
*/
|
|
49
|
+
render(content: TicketContent, { format, dpi, ...options }?: PrinterRenderOptions): Promise<RenderResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Trabajo PDF: en el navegador el PDF es un Blob; se envía en base64.
|
|
52
|
+
* Opciones:
|
|
53
|
+
* pdfMode: 'driver' (el driver de la impresora renderiza el PDF — el modo
|
|
54
|
+
* correcto para etiquetas, ej. AiYin) | 'raw' (rasteriza a ESC/POS, para
|
|
55
|
+
* tiqueteras de recibos). Default del servidor: 'driver' si se mandan
|
|
56
|
+
* labelSettings, 'raw' si no.
|
|
57
|
+
* labelSettings: { width, height, unit: 'mm'|'inch' } — tamaño de la
|
|
58
|
+
* etiqueta; la página del PDF debe medir lo mismo.
|
|
59
|
+
*/
|
|
60
|
+
printPDF(blob: Blob, { pdfMode, labelSettings }?: PrintPdfOptions): Promise<PrintResult | null>;
|
|
61
|
+
}
|