EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
fd1Client.ts
Go to the documentation of this file.
1/**
2 * Fieldpine FD1/FD3 API Client
3 *
4 * Modern TypeScript client for Fieldpine's FD1 and FD3 protocol.
5 * Supports both HTTP POST and WebSocket connections.
6 *
7 * @see /docs/FIELDPINE_FD1_API.md
8 */
9
10export interface FD1QueryOptions {
11 [key: string]: boolean | string;
12}
13
14export interface FD1QueryFilters {
15 [key: string]: any;
16}
17
18export interface FD1Request {
19 a: string; // Action name
20 q?: FD1QueryFilters; // Query filters
21 qo?: FD1QueryOptions; // Query options (field selection)
22 limit?: number;
23 offset?: number;
24 [key: string]: any; // Additional parameters
25}
26
27export interface FD1Response<T = any> {
28 success?: boolean;
29 data?: {
30 rows?: T[];
31 };
32 rows?: T[]; // Alternative format
33 error?: {
34 code: string;
35 message: string;
36 details?: any;
37 };
38}
39
40export interface FD1Product {
41 pid?: number;
42 physkey?: string;
43 description?: string;
44 sku?: string;
45 barcode?: string;
46 price?: number;
47 cost?: number;
48 stock_level?: number;
49 department_id?: number;
50 supplier_id?: number;
51}
52
53export interface FD1Sale {
54 sale_id?: number;
55 created_date?: string;
56 completed_date?: string;
57 total_inc_tax?: number;
58 total_ex_tax?: number;
59 tax_amount?: number;
60 status?: string;
61 phase_code?: number;
62 location_id?: number;
63 customer_id?: number;
64 operator_id?: number;
65}
66
67export interface FD1SaleLine {
68 line_id?: number;
69 sale_id?: number;
70 product_id?: number;
71 description?: string;
72 qty?: number;
73 unit_price?: number;
74 line_total?: number;
75 tax_amount?: number;
76}
77
78export interface FD1Customer {
79 customer_id?: number;
80 name?: string;
81 email?: string;
82 phone?: string;
83 address?: string;
84 city?: string;
85 state?: string;
86 postcode?: string;
87 country?: string;
88 loyalty_points?: number;
89}
90
91export interface FD1Department {
92 department_id?: number;
93 name?: string;
94 parent_id?: number;
95 description?: string;
96}
97
98export interface FD1Location {
99 location_id?: number;
100 name?: string;
101 address?: string;
102 city?: string;
103 state?: string;
104 postcode?: string;
105 country?: string;
106}
107
108export interface FD1SessionResponse {
109 success: boolean;
110 session_token?: string;
111 user_id?: number;
112 permissions?: string[];
113 error?: {
114 code: string;
115 message: string;
116 };
117}
118
119/**
120 * FD1 Client Configuration
121 */
122export interface FD1ClientConfig {
123 baseUrl: string; // e.g., 'http://127.0.0.1:3490' or 'https://store.fieldpine.com'
124 apiKey?: string;
125 sessionToken?: string;
126 protocol?: 'fd1' | 'fd3'; // fd1 for local/ws, fd3 for cloud/https
127}
128
129/**
130 * FD1 API Client
131 */
132export class FD1Client {
133 private config: FD1ClientConfig;
134 private ws?: WebSocket;
135
136 constructor(config: FD1ClientConfig) {
137 this.config = {
138 protocol: 'fd3',
139 ...config,
140 };
141 }
142
143 /**
144 * Execute a generic FD1 request
145 */
146 async request<T = any>(request: FD1Request): Promise<FD1Response<T>> {
147 const url = `${this.config.baseUrl}/${this.config.protocol}/${request.a}`;
148
149 const headers: Record<string, string> = {
150 'Content-Type': 'application/json',
151 };
152
153 if (this.config.apiKey) {
154 headers['x-api-key'] = this.config.apiKey;
155 }
156
157 if (this.config.sessionToken) {
158 headers['x-session-token'] = this.config.sessionToken;
159 }
160
161 const response = await fetch(url, {
162 method: 'POST',
163 headers,
164 body: JSON.stringify(request),
165 });
166
167 if (!response.ok) {
168 throw new Error(`FD1 request failed: ${response.status} ${response.statusText}`);
169 }
170
171 return response.json();
172 }
173
174 /**
175 * Products API
176 */
177 async listProducts(
178 filters?: FD1QueryFilters,
179 fields?: FD1QueryOptions,
180 limit = 100,
181 offset = 0
182 ): Promise<FD1Product[]> {
183 const response = await this.request<FD1Product>({
184 a: 'products.list',
185 q: filters,
186 qo: fields || {
187 internal_id_n: 'pid',
188 internal_guid: 'physkey',
189 description: true,
190 sku: true,
191 barcode: true,
192 price: true,
193 stock_level: true,
194 },
195 limit,
196 offset,
197 });
198
199 return response.data?.rows || response.rows || [];
200 }
201
202 async getProduct(productId: number, fields?: FD1QueryOptions): Promise<FD1Product | null> {
203 const products = await this.listProducts(
204 { pid: productId },
205 fields || {
206 internal_id_n: 'pid',
207 description: true,
208 sku: true,
209 price: true,
210 cost: true,
211 stock_level: true,
212 },
213 1
214 );
215
216 return products[0] || null;
217 }
218
219 async searchProducts(searchTerm: string, limit = 50): Promise<FD1Product[]> {
220 return this.listProducts(
221 { search: searchTerm },
222 {
223 internal_id_n: 'pid',
224 description: true,
225 sku: true,
226 barcode: true,
227 price: true,
228 },
229 limit
230 );
231 }
232
233 /**
234 * Sales API
235 */
236 async listSales(
237 filters?: FD1QueryFilters,
238 fields?: FD1QueryOptions,
239 limit = 100,
240 offset = 0
241 ): Promise<FD1Sale[]> {
242 const response = await this.request<FD1Sale>({
243 a: 'sales.list',
244 q: filters,
245 qo: fields || {
246 sale_id: true,
247 created_date: true,
248 completed_date: true,
249 total_inc_tax: true,
250 total_ex_tax: true,
251 status: true,
252 customer_id: true,
253 },
254 limit,
255 offset,
256 });
257
258 return response.data?.rows || response.rows || [];
259 }
260
261 async getSale(saleId: number): Promise<FD1Sale | null> {
262 const response = await this.request<FD1Sale>({
263 a: 'sale.read',
264 sale_id: saleId,
265 });
266
267 if (response.data?.rows && response.data.rows.length > 0) {
268 return response.data.rows[0];
269 }
270
271 return null;
272 }
273
274 async createSale(saleData: {
275 lines: Array<{ product_id: number; qty: number; price?: number }>;
276 customer_id?: number;
277 location_id?: number;
278 }): Promise<FD1Sale> {
279 const response = await this.request<FD1Sale>({
280 a: 'sale.create',
281 ...saleData,
282 });
283
284 if (response.data?.rows && response.data.rows.length > 0) {
285 return response.data.rows[0];
286 }
287
288 throw new Error('Failed to create sale');
289 }
290
291 /**
292 * Customers API
293 */
294 async listCustomers(
295 filters?: FD1QueryFilters,
296 fields?: FD1QueryOptions,
297 limit = 100,
298 offset = 0
299 ): Promise<FD1Customer[]> {
300 const response = await this.request<FD1Customer>({
301 a: 'customers.list',
302 q: filters,
303 qo: fields || {
304 customer_id: true,
305 name: true,
306 email: true,
307 phone: true,
308 loyalty_points: true,
309 },
310 limit,
311 offset,
312 });
313
314 return response.data?.rows || response.rows || [];
315 }
316
317 async getCustomer(customerId: number): Promise<FD1Customer | null> {
318 const customers = await this.listCustomers({ customer_id: customerId }, undefined, 1);
319 return customers[0] || null;
320 }
321
322 async searchCustomers(searchTerm: string, limit = 50): Promise<FD1Customer[]> {
323 return this.listCustomers(
324 { search: searchTerm },
325 {
326 customer_id: true,
327 name: true,
328 email: true,
329 phone: true,
330 },
331 limit
332 );
333 }
334
335 /**
336 * Departments API
337 */
338 async listDepartments(fields?: FD1QueryOptions): Promise<FD1Department[]> {
339 const response = await this.request<FD1Department>({
340 a: 'departments.list',
341 qo: fields || {
342 department_id: true,
343 name: true,
344 parent_id: true,
345 description: true,
346 },
347 });
348
349 return response.data?.rows || response.rows || [];
350 }
351
352 /**
353 * Locations API
354 */
355 async listLocations(fields?: FD1QueryOptions): Promise<FD1Location[]> {
356 const response = await this.request<FD1Location>({
357 a: 'locations.list',
358 qo: fields || {
359 location_id: true,
360 name: true,
361 address: true,
362 city: true,
363 },
364 });
365
366 return response.data?.rows || response.rows || [];
367 }
368
369 /**
370 * Session Management
371 */
372 async login(credentials: {
373 username?: string;
374 password?: string;
375 api_key?: string;
376 }): Promise<FD1SessionResponse> {
377 const response = await this.request<FD1SessionResponse>({
378 a: 'session.login',
379 ...credentials,
380 });
381
382 if (response.success && response.data?.rows?.[0]?.session_token) {
383 this.config.sessionToken = response.data.rows[0].session_token;
384 }
385
386 const result = response.data?.rows?.[0];
387 if (result) {
388 return result;
389 }
390
391 // If no row data, construct response from top-level response
392 return {
393 success: response.success || false,
394 session_token: response.data?.rows?.[0]?.session_token,
395 user_id: response.data?.rows?.[0]?.user_id,
396 permissions: response.data?.rows?.[0]?.permissions,
397 error: response.error,
398 };
399 }
400
401 async logout(): Promise<void> {
402 await this.request({
403 a: 'session.logout',
404 });
405
406 this.config.sessionToken = undefined;
407 }
408
409 /**
410 * WebSocket Support
411 */
412 connectWebSocket(
413 onMessage: (data: any) => void,
414 onError?: (error: Event) => void
415 ): Promise<void> {
416 return new Promise((resolve, reject) => {
417 const wsProtocol = this.config.baseUrl.startsWith('https') ? 'wss' : 'ws';
418 const wsUrl = `${wsProtocol}://${this.config.baseUrl.replace(/^https?:\/\//, '')}/fd1/ws`;
419
420 this.ws = new WebSocket(wsUrl);
421
422 this.ws.onopen = () => {
423 // Send authentication as first message
424 if (this.config.apiKey) {
425 this.ws?.send(
426 JSON.stringify({
427 a: 'session.login',
428 api_key: this.config.apiKey,
429 })
430 );
431 }
432 resolve();
433 };
434
435 this.ws.onmessage = (event) => {
436 try {
437 const data = JSON.parse(event.data);
438 onMessage(data);
439 } catch (error) {
440 console.error('Failed to parse WebSocket message:', error);
441 }
442 };
443
444 this.ws.onerror = (error) => {
445 if (onError) {
446 onError(error);
447 }
448 reject(error);
449 };
450
451 this.ws.onclose = () => {
452 console.log('WebSocket connection closed');
453 };
454 });
455 }
456
457 sendWebSocketMessage(message: FD1Request): void {
458 if (this.ws && this.ws.readyState === WebSocket.OPEN) {
459 this.ws.send(JSON.stringify(message));
460 } else {
461 throw new Error('WebSocket not connected');
462 }
463 }
464
465 disconnectWebSocket(): void {
466 if (this.ws) {
467 this.ws.close();
468 this.ws = undefined;
469 }
470 }
471
472 /**
473 * Firehose (Real-time Events)
474 */
475 async subscribeToFirehose(topics: string[]): Promise<void> {
476 await this.request({
477 a: 'firehose.subscribe',
478 topics,
479 });
480 }
481
482 /**
483 * Generic Data Query (using /fd1/data endpoint)
484 */
485 async queryData(table: string, filter?: string, fields?: string): Promise<any> {
486 const params = new URLSearchParams({
487 '_v.table': filter ? `${table}[${filter}]` : table,
488 });
489
490 if (fields) {
491 params.append('_qo', fields);
492 }
493
494 const url = `${this.config.baseUrl}/fd1/data?${params.toString()}`;
495
496 const headers: Record<string, string> = {};
497
498 if (this.config.apiKey) {
499 headers['x-api-key'] = this.config.apiKey;
500 }
501
502 const response = await fetch(url, { headers });
503
504 if (!response.ok) {
505 throw new Error(`FD1 data query failed: ${response.status} ${response.statusText}`);
506 }
507
508 return response.json();
509 }
510}
511
512/**
513 * Create FD1 client instance
514 */
515export function createFD1Client(config: FD1ClientConfig): FD1Client {
516 return new FD1Client(config);
517}