ProfirestAPI v2
TypeScript 1.0.0 Finální smlouva · v ostrém provozu
ObsahContents
  1. PřehledOverview
  2. ZačínámeGetting started
  3. Ke staženíDownload
  4. InstalaceInstallation
  5. Příkladový klientExample client
  6. TypyTypes
  7. Typované cestyTyped paths
  8. ChybyErrors
  9. ModelyModels
  10. Chování klientaClient behaviour

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žadavkyRequirementsTypeScript 5.0+, lib: dom
SmlouvaContract26.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:

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 pairingqkonecquit
npx tsc pichacka.ts --module commonjs --target es2020 --lib es2020,dom --strict
node pichacka.js
examples/pichacka.ts
/**
 * 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.AttendancePunch

Chyby

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'];  // StaffMemberPage

Nepovinné 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.