TypeScript · API v2
Profirest API pro TypeScript
Typy pro klienta profirest-api.js vygenerované ze smlouvy: modely všech schémat, kódy chyb a typované cesty, takže api.get(`/restaurants/${rid}/staff`) rovnou vrací StaffMemberPage a překlep v těle požadavku odhalí překladač.
Types for the profirest-api.js client generated from the contract: models of all schemas, error codes and typed paths, so api.get(`/restaurants/${rid}/staff`) returns StaffMemberPage right away and the compiler catches a typo in a request body.
Knihovna dělá za vás to, co jinak musí každá aplikace napsat a hlavně nezapomenout: spáruje zařízení kódem, připojí aplikaci partnera se souhlasem majitele (OAuth s PKCE), uloží tokeny a obnoví je jen jednou i při souběžných požadavcích a mezi procesy, přihlásí zaměstnance PINem nebo čipem, pošle Idempotency-Key u každého POST (při opakování stejný), po výpadku opakuje podle Retry-After, chyby vrací s kódem z katalogu, prochází stránky seznamů a čte proud událostí. Cesty, těla a odpovědi jsou přesně podle reference; modely všech schémat jsou vygenerované ze smlouvy.
The library does what every application otherwise has to write and, above all, not forget: it pairs a device with a code, connects a partner application with the owner's consent (OAuth with PKCE), stores the tokens and refreshes them only once even for concurrent requests and across processes, signs employees in with a PIN or a card, sends an Idempotency-Key with every POST (the same one when retrying), retries after an outage following Retry-After, returns errors with a code from the catalog, walks list pages and reads the event feed. Paths, bodies and responses are exactly as in the reference; models of all schemas are generated from the contract.
Ke stažení
Download
Archiv obsahuje profirest-api.js, typy profirest-api.d.ts a models.generated.d.ts, příkladového klienta, README, licenci (MIT) a seznam změn.
The archive contains profirest-api.js, the types profirest-api.d.ts and models.generated.d.ts, the example client, a README, the licence (MIT) and the changelog.
| BalíčekPackage | @profirest/api-types (npm) |
| PožadavkyRequirements | TypeScript 5.0+, lib: dom |
| SmlouvaContract | 26.11.28 |
Instalace
Installation
Typy patří ke klientovi profirest-api.js (je v archivu, nebo ho stáhněte z /v2/profirest-api.js). Soubory profirest-api.d.ts a models.generated.d.ts dejte vedle profirest-api.js – TypeScript je najde sám – nebo modul namapujte v tsconfig.json:
The types belong to the profirest-api.js client (it is in the archive, or download it from /v2/profirest-api.js). Put profirest-api.d.ts and models.generated.d.ts next to profirest-api.js – TypeScript finds them itself – or map the module in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"lib": ["es2020", "dom"],
"paths": { "profirest-api": ["./lib/profirest-api.d.ts"] }
}
}import ProfirestApi = require('./profirest-api'); // CommonJS / Node 18+
import ProfirestApi from './profirest-api'; // s esModuleInterop
// v prohlížeči se <script src="/v2/profirest-api.js"> je ProfirestApi globálníPříkladový klient
Example client
Hotová aplikace k vyzkoušení: docházkový terminál v příkazové řádce. Při prvním spuštění zařízení spáruje (vypíše kód, vedoucí ho potvrdí v administraci v Správa PC / zařízení → Připojit kódem), tokeny uloží do souboru a pak přijímá příkazy. Ukazuje párování, píchnutí PINem, výběr zaměstnance při stejném PINu, rozlišení chyb podle kódu a seznam lidí v práci. Adresu API mění proměnná PROFIREST_BASE_URL, soubor s tokeny PROFIREST_TOKENS.
A ready application to try: an attendance terminal on the command line. On the first run it pairs the device (it prints a code that a manager confirms in the administration under PC / devices → Connect a device), stores the tokens in a file and then accepts commands. It shows pairing, punching with a PIN, choosing the employee when a PIN is shared, telling errors apart by code and the list of people at work. PROFIREST_BASE_URL changes the API address, PROFIREST_TOKENS the token file.
p PINpříchodarrivalo PINodchoddeparturepz PINzačátek přestávkybreak startkz PINkonec přestávkybreak endkkdo je v práciwho is at workodparovatzapomene spárováníforgets the pairingqkonecquitnpx tsc pichacka.ts --module commonjs --target es2020 --lib es2020,dom --strict
node pichacka.js/**
* Příkladový klient Profirest API v2 v TypeScriptu: docházkový terminál v příkazové řádce (Node 18+).
*
* npx tsc pichacka.ts --module commonjs --target es2020 --lib es2020,dom (vedle profirest-api.js a .d.ts)
* node pichacka.js
*
* Při prvním spuštění zařízení spáruje – vypíše kód, který vedoucí potvrdí v administraci (Správa PC / zařízení →
* Připojit zařízení). Pak čte příkazy:
* p PIN příchod o PIN odchod
* pz PIN začátek přestávky kz PIN konec přestávky
* k kdo je v práci odparovat zapomenout spárování q konec
*
* Proměnné prostředí: PROFIREST_BASE_URL (výchozí https://api.mujprofirest.cz), PROFIREST_TOKENS (soubor s tokeny,
* výchozí ./profirest-pichacka.json – chraňte ho jako heslo).
*/
import ProfirestApi = require('./profirest-api');
// moduly Node bez balíčku @types/node (s ním: import * as fs from 'fs' …)
declare const process: any;
declare function require(name: string): any;
const fs = require('fs');
const readline = require('readline');
/** Úložiště tokenů v JSON souboru (klient čeká getItem / setItem / removeItem). */
function fileStorage(path: string): ProfirestApi.Storage {
const read = (): Record<string, string> => {
try { return JSON.parse(fs.readFileSync(path, 'utf8')); } catch { return {}; }
};
const write = (data: Record<string, string>) => fs.writeFileSync(path, JSON.stringify(data), { mode: 0o600 });
return {
getItem: (k) => read()[k] ?? null,
setItem: (k, v) => { const d = read(); d[k] = v; write(d); },
removeItem: (k) => { const d = read(); delete d[k]; write(d); },
};
}
const api = new ProfirestApi({
baseUrl: process.env.PROFIREST_BASE_URL || 'https://api.mujprofirest.cz',
clientId: 'profirest-dochazka',
storage: fileStorage(process.env.PROFIREST_TOKENS || './profirest-pichacka.json'),
});
const STATES: Record<string, string> = { working: 'v práci', on_break: 'na přestávce', off: 'mimo práci' };
const TYPES: Record<string, ProfirestApi.Models.AttendancePunchCreate['type']> = {
p: 'clock_in', o: 'clock_out', pz: 'break_start', kz: 'break_end',
};
const rl = readline.createInterface({ input: process.stdin });
const lines = rl[Symbol.asyncIterator]();
async function readLine(): Promise<string | null> {
const r = await lines.next();
return r.done ? null : String(r.value);
}
/** Píchnutí: id vznikne jednou; při výběru zaměstnance se posílá znovu se stejným id. */
async function punch(rid: string, type: ProfirestApi.Models.AttendancePunchCreate['type'], pin: string): Promise<void> {
const body: ProfirestApi.Models.AttendancePunchCreate = {
id: ProfirestApi.uuid(), type, credential: { method: 'pin', secret: pin },
};
for (;;) {
try {
const r = await api.post(`/restaurants/${rid}/attendance/punches`, body); // typ AttendancePunch
console.log(`OK: ${r.employee.name} – ${STATES[r.state] || r.state}`);
return;
} catch (e) {
if (!(e instanceof ProfirestApi.ApiError)) { throw e; }
switch (e.code) {
case 'staff_ambiguous': { // stejný PIN má víc lidí – vybrat a poslat znovu
console.log('Tento PIN má víc lidí:');
e.candidates.forEach((c, i) => console.log(` ${i + 1}) ${c.name}`));
const chosen = e.candidates[Number(await readLine()) - 1];
if (!chosen) { console.log('Zrušeno.'); return; }
body.credential = { method: 'pin', secret: pin, employee_id: chosen.id };
continue;
}
case 'staff_credentials_invalid': console.log('Neznámý PIN.'); return;
case 'staff_locked': console.log(`Moc špatných pokusů – zkuste to za ${e.retryAfter} s.`); return;
case 'attendance_state_conflict': console.log(`Teď to nejde: ${e.title}`); return;
case 'network_error':
case 'timeout': console.log('Bez sítě – zkuste znovu.'); return; // terminál by uložil a poslal dávkou
default: console.log(`Chyba: ${e.title} (${e.code})`); return;
}
}
}
}
async function main(): Promise<number> {
// 1) spárování kódem (RFC 8628) – jen poprvé, tokeny pak zůstávají v souboru
if (!api.isPaired()) {
const pairing = await api.pair({
scope: 'restaurants:read staff:read staff:authenticate attendance:read attendance:punch',
deviceName: 'Píchačka (příklad)',
});
console.log('Zařízení není spárované. Vedoucí potvrdí kód v administraci (Správa PC / zařízení → Připojit zařízení).');
console.log(`KÓD: ${pairing.userCode}`);
console.log(`nebo otevřete: ${pairing.verificationUriComplete}`);
try {
await pairing.connected;
} catch (e) {
const err = e instanceof ProfirestApi.OAuthError ? e.error : String(e);
console.log(err === 'access_denied' ? 'Vedoucí párování odmítl.'
: err === 'expired_token' ? 'Kód vypršel (10 minut). Spusťte příklad znovu.' : `Párování se nepovedlo: ${err}`);
return 1;
}
}
if (api.restaurantId === null) { await api.connection(); }
const rid = api.restaurantId as string;
console.log(`Provozovna ${rid}. Příkazy: p/o/pz/kz PIN, k = kdo je v práci, odparovat, q = konec.`);
// 2) smyčka příkazů
for (let line = await readLine(); line !== null; line = await readLine()) {
const [cmd = '', pin] = line.trim().split(/\s+/, 2);
try {
if (TYPES[cmd.toLowerCase()] && pin) {
await punch(rid, TYPES[cmd.toLowerCase()], pin);
} else if (cmd === 'k') {
for (const p of await api.all(`/restaurants/${rid}/attendance/presence`)) {
console.log(` ${p.employee.name}: ${STATES[p.state] || p.state}`);
}
} else if (cmd === 'odparovat') {
await api.unpair();
console.log('Spárování zapomenuto.');
break;
} else if (cmd === 'q') {
break;
} else if (cmd !== '') {
console.log('Příkazy: p/o/pz/kz PIN, k, odparovat, q');
}
} catch (e) {
if (e instanceof ProfirestApi.ApiError && e.code === 'not_paired') {
console.log('Zařízení bylo odpárováno. Spusťte příklad znovu.');
return 1;
}
console.log(`Chyba: ${e instanceof Error ? e.message : String(e)}`);
}
}
return 0;
}
main().then((code) => { rl.close(); process.exitCode = code; });Typované cesty
Typed paths
Cesty v get, post, put, patch, delete, pages a all se typují podle smlouvy – i šablonové řetězce s proměnnými. Tělo požadavku se kontroluje proti schématu (chybějící povinné pole nebo překlep v názvu je chyba překladu). Neznámá cesta nebo cesta složená operátorem + má typ any; vlastní typ: api.get<string, MujTyp>('/…').
Paths in get, post, put, patch, delete, pages and all are typed by the contract – template strings with variables too. The request body is checked against the schema (a missing required field or a typo in a name is a compile error). An unknown path or a path built with + is any; your own type: api.get<string, MyType>('/…').
const api = new ProfirestApi({ clientId: 'profirest-dochazka' });
const staff = await api.get(`/restaurants/${rid}/staff`); // ProfirestApi.Models.StaffMemberPage
const all = await api.all(`/restaurants/${rid}/staff`); // StaffMember[]
for await (const page of api.pages('/events', { query: { restaurant_id: rid } })) { /* EventPage */ }
const punch = await api.post(`/restaurants/${rid}/attendance/punches`, {
id: ProfirestApi.uuid(),
type: 'clock_in', // doplňuje se: clock_in | clock_out | break_start | break_end
credential: { method: 'pin', secret: '5678' },
});
console.log(punch.employee.name, punch.state); // punch: ProfirestApi.Models.AttendancePunchChyby
Errors
try {
await api.signInStaff(rid, { method: 'pin', secret: pin });
} catch (e) {
if (e instanceof ProfirestApi.ApiError && e.code === 'staff_ambiguous') {
const names = e.candidates.map((c) => c.name); // EmployeeRef[]
}
if (e instanceof ProfirestApi.OAuthError) { console.log(e.error); }
}ProfirestApi.ErrorCode nabízí všechny kódy z katalogu i kódy klienta network_error, timeout, not_paired, unexpected_redirect; budoucí kódy typ také připustí. Výčtové vlastnosti modelů jsou stejně otevřené – nové hodnoty mohou přibýt.
ProfirestApi.ErrorCode offers every catalog code and the client codes network_error, timeout, not_paired, unexpected_redirect; future codes are accepted too. Enum properties of models are open in the same way – new values may come.
Modely
Models
type Order = ProfirestApi.Models.Order;
type Create = ProfirestApi.Models.Operations['createOrder']['request']; // OrderCreate
type Get = ProfirestApi.Models.ApiGet['/restaurants/{}/staff']['response']; // StaffMemberPageNepovinné vlastnosti jsou ?, hodnoty, které mohou být null, mají | null. Časy jsou řetězce RFC 3339, částky Money v haléřích. Typy jsou vygenerované ze smlouvy, takže po její změně stačí stáhnout nový archiv.
Optional properties are ?, values that can be null have | null. Times are RFC 3339 strings, amounts Money in minor units. The types are generated from the contract, so after it changes just download the new archive.
Chování klienta
Client behaviour
Párování, tokeny, zaměstnance, opakování, stránkování a proud událostí popisuje stránka JavaScript – typy jen dávají jejímu klientovi kontrolu překladačem a doplňování v editoru.
Pairing, tokens, employees, retries, paging and the event feed are described on the JavaScript page – the types just give its client compiler checks and editor completion.
Offline schválení účtenky vedoucím má typy ProfirestApi.offlineApproval.verifyPin(member, pin), sign(member, pin, receipt) a message(receipt); member je StaffMember nebo samotné OfflineApproval.
Offline approval of a receipt by a manager is typed as ProfirestApi.offlineApproval.verifyPin(member, pin), sign(member, pin, receipt) and message(receipt); member is a StaffMember or just the OfflineApproval.