@brownandroot/api 1.2.1 → 2.0.0

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/README.md CHANGED
@@ -1,31 +1,33 @@
1
1
  # @brownandroot/api
2
2
 
3
- TypeScript client for the Brown & Root APIHub data service. Provides access to employee, business unit, cost code, pay type, work order, job type/job step, LLM chat, and document search data.
3
+ Unified TypeScript API for Brown & Root APIHub data.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @brownandroot/api
8
+ bun add @brownandroot/api
9
9
  ```
10
10
 
11
- ---
11
+ ## One Import Path
12
12
 
13
- ## Client-Side Cache API (recommended for SvelteKit)
13
+ Use only:
14
14
 
15
- The package ships a browser-side API at `@brownandroot/api/cache` that wraps every remote function with an **IndexedDB stale-while-revalidate cache**. On the first call the data is fetched from the server; on every subsequent call the cached data is returned instantly. A background refresh runs automatically when the cached data is older than **4 hours**.
15
+ ```ts
16
+ import { employees, workorders, clearCache } from '@brownandroot/api'
17
+ ```
16
18
 
17
- All list and dropdown functions accept an optional filter object — every entity field is available as an optional filter key. The `q` field performs a case-insensitive partial-text match across the entity's searchable text fields.
19
+ No domain subpath imports are required.
18
20
 
19
- ### Setup
21
+ ## Setup
20
22
 
21
- Add to your app's `.env`:
23
+ Add to your app `.env`:
22
24
 
23
- ```
25
+ ```bash
24
26
  APIHUB_URL=https://your-apihub-url.com
25
27
  APIHUB_API_KEY=your-api-key
26
28
  ```
27
29
 
28
- Enable remote functions in `svelte.config.js` (if not already):
30
+ Enable SvelteKit remote functions if needed:
29
31
 
30
32
  ```js
31
33
  kit: {
@@ -35,511 +37,128 @@ kit: {
35
37
  }
36
38
  ```
37
39
 
38
- ### Usage
39
-
40
- ```svelte
41
- <script lang="ts">
42
- import { getWorkordersDropdown, getEmployees, clearCache } from '@brownandroot/api/cache'
43
-
44
- // First page load: fetches from server and caches in IndexedDB.
45
- // Subsequent loads: returns from IndexedDB instantly, refreshes in background.
46
- const workorders = $derived(await getWorkordersDropdown({ businessUnitId: 'BU001' }))
47
-
48
- // Multiple filters compose freely
49
- const employees = $derived(await getEmployees({
50
- hbu: 'TX01',
51
- payClass: 'H',
52
- q: 'john',
53
- }))
54
-
55
- // After a mutation, clear the affected entity so the next read re-fetches
56
- async function onCreate() {
57
- await createWorkorder(formData)
58
- clearCache('workorders')
59
- }
60
- </script>
61
- ```
62
-
63
- ### Filter reference
64
-
65
- All filter fields are optional. Omit the filter object entirely to return all records.
66
-
67
- #### Workorders
68
-
69
- ```typescript
70
- import { getWorkorders, getWorkordersDropdown } from '@brownandroot/api/cache'
71
-
72
- interface WorkorderFilters {
73
- businessUnitId?: string // exact match
74
- costCodeId?: string // exact match
75
- isActive?: boolean // exact match
76
- completed?: boolean // exact match
77
- area?: string // exact match
78
- parentWorkOrder?: string // exact match
79
- q?: string // partial match on description or clientWorkOrderId
80
- }
81
-
82
- // Returns Workorder[]
83
- const all = await getWorkorders()
84
- const active = await getWorkorders({ isActive: true })
85
- const byBu = await getWorkorders({ businessUnitId: 'BU001', isActive: true })
86
- const search = await getWorkorders({ q: 'pipe' })
87
-
88
- // Returns { value, label }[] — always applies isActive: true by default
89
- const dropdown = await getWorkordersDropdown({ businessUnitId: 'BU001' })
90
- ```
91
-
92
- #### Employees
93
-
94
- ```typescript
95
- import { getEmployees, getEmployeesDropdown } from '@brownandroot/api/cache'
96
-
97
- interface EmployeeFilters {
98
- businessUnitId?: string // exact match
99
- hbu?: string // exact match on home business unit
100
- supervisor?: string // exact match on supervisor employee ID
101
- payClass?: string // exact match
102
- jobType?: string // exact match
103
- jobStep?: string // exact match
104
- sector?: string // exact match
105
- division?: string // exact match
106
- q?: string // partial match on name or email
107
- }
108
-
109
- const byHbu = await getEmployees({ hbu: 'TX01' })
110
- const byName = await getEmployees({ q: 'john' })
111
- const dropdown = await getEmployeesDropdown({ businessUnitId: 'BU001' })
112
- ```
113
-
114
- #### Cost Codes
115
-
116
- ```typescript
117
- import { getCostcodes, getCostcodesDropdown } from '@brownandroot/api/cache'
118
-
119
- interface CostcodeFilters {
120
- businessUnitId?: string // exact match
121
- isActive?: boolean // exact match
122
- entryFlag?: boolean // exact match
123
- payTypeCode?: string // resolves pay type → objectAccount, filters by match
124
- q?: string // partial match on description or jdeCostCode
125
- }
126
-
127
- const byBu = await getCostcodes({ businessUnitId: 'BU001' })
128
- const search = await getCostcodes({ q: 'labor' })
129
-
130
- // Dropdown applies isActive: true and entryFlag: true by default,
131
- // and also excludes expired cost codes
132
- const dropdown = await getCostcodesDropdown({ businessUnitId: 'BU001' })
133
- const dropdownByPayType = await getCostcodesDropdown({
134
- businessUnitId: 'BU001',
135
- payTypeCode: 'PT01',
136
- })
137
- ```
138
-
139
- > **payTypeCode cross-reference:** When `payTypeCode` is set, the filter looks up that pay type's `objectAccount` from the (also cached) pay types list and only returns cost codes whose `objectAccount` matches. If the pay type has no `objectAccount`, all cost codes pass this filter.
140
-
141
- #### Pay Types
142
-
143
- ```typescript
144
- import { getPaytypes, getPaytypesDropdown } from '@brownandroot/api/cache'
145
-
146
- interface PaytypeFilters {
147
- payClass?: string // exact match
148
- category?: string // exact match
149
- type?: string // exact match
150
- isActive?: boolean // exact match
151
- perDiemPayType?: boolean // exact match
152
- q?: string // partial match on description
153
- }
154
-
155
- const hourly = await getPaytypes({ payClass: 'H' })
156
- const dropdown = await getPaytypesDropdown({ isActive: true })
157
- // Dropdown returns { value, label, payClass }[]
158
- ```
159
-
160
- #### Business Units
161
-
162
- ```typescript
163
- import { getBusinessUnits, getBusinessUnitsDropdown } from '@brownandroot/api/cache'
164
-
165
- interface BusinessUnitFilters {
166
- subsector?: string // exact match
167
- isActive?: boolean // exact match
168
- clientId?: string // exact match
169
- q?: string // partial match on description or clientDescription
170
- }
171
-
172
- const active = await getBusinessUnits({ isActive: true })
173
- const dropdown = await getBusinessUnitsDropdown({ subsector: 'Gulf Coast' })
174
- ```
175
-
176
- #### Job Type / Job Steps
177
-
178
- ```typescript
179
- import { getJobtypejobsteps, getJobtypejobstepsDropdown } from '@brownandroot/api/cache'
180
-
181
- interface JobtypejobstepFilters {
182
- jobType?: string // exact match
183
- payclass?: string // exact match
184
- isActive?: boolean // exact match
185
- grp?: string // exact match
186
- q?: string // partial match on description, jobType, or jobStep
187
- }
188
-
189
- const welders = await getJobtypejobsteps({ jobType: 'WE' })
190
- const dropdown = await getJobtypejobstepsDropdown({ isActive: true })
191
- ```
192
-
193
- ### Single-record lookups
194
-
195
- Single-record functions always fetch fresh from the server (no IndexedDB):
196
-
197
- ```typescript
198
- import {
199
- getEmployee,
200
- getSupervisorChain,
201
- getJdeFromEmail,
202
- verifyIdentity,
203
- getWorkorder,
204
- getCostcode,
205
- getPaytype,
206
- getBusinessUnit,
207
- getJobtypejobstep,
208
- } from '@brownandroot/api/cache'
209
-
210
- const emp = await getEmployee('12345')
211
- const chain = await getSupervisorChain('12345')
212
- const { jde, employee } = await getJdeFromEmail('john@example.com')
213
- const verified = await verifyIdentity({
214
- first3FirstName: 'joh',
215
- first3LastName: 'doe',
216
- dob: '1985-03-15',
217
- ssn4: '4321',
218
- })
219
- ```
220
-
221
- ### Cache management
222
-
223
- ```typescript
224
- import { clearCache } from '@brownandroot/api/cache'
225
-
226
- clearCache() // clear all entities
227
- clearCache('workorders') // clear a specific entity
228
- clearCache('employees')
229
- clearCache('costcodes')
230
- clearCache('paytypes')
231
- clearCache('businessUnits')
232
- clearCache('jobtypejobsteps')
233
- ```
234
-
235
- Call `clearCache(entity)` immediately after any mutation that changes that entity so the next read fetches fresh data.
236
-
237
- ---
40
+ ## Unified Domain Contract
238
41
 
239
- ## Remote Functions (server-side)
42
+ Each listable domain exposes the same shape:
240
43
 
241
- For SvelteKit apps that need direct server-side access without the IndexedDB layer, the package also ships ready-to-use remote functions that run on the server and read credentials from environment variables automatically.
44
+ - `getAll(filters?)`
45
+ - `dropdown(filters?)`
46
+ - `get(id)` -> returns item or `null`
242
47
 
243
- ```svelte
244
- <script lang="ts">
245
- import { getEmployees } from '@brownandroot/api/employees'
246
- import { getBusinessUnits } from '@brownandroot/api/businessUnits'
48
+ Domains:
247
49
 
248
- // These always fetch from the server on every call
249
- const employees = $derived(await getEmployees())
250
- const businessUnits = $derived(await getBusinessUnits())
251
- </script>
252
- ```
253
-
254
- Or compose with your own remote functions:
50
+ - `employees`
51
+ - `workorders`
52
+ - `costcodes`
53
+ - `paytypes`
54
+ - `businessUnits`
55
+ - `jobtypejobsteps`
56
+ - `llm`
57
+ - `rag`
255
58
 
256
- ```typescript
257
- import { query } from '$app/server'
258
- import { getSupervisorChain } from '@brownandroot/api/employees'
59
+ ## Caching Behavior
259
60
 
260
- export const getMyManagers = query(async () => {
261
- return (await getSupervisorChain('12345')).slice(0, 2)
262
- })
263
- ```
61
+ - Browser/client: IndexedDB stale-while-revalidate cache for `getAll` and `dropdown`
62
+ - Server/SSR: in-memory TTL cache for `getAll` and `dropdown`
63
+ - Single-record `get(id)` reads are fresh and return `null` when not found
64
+ - Read failures in SSR fail open by default in unified domain APIs:
65
+ - list reads return `[]`
66
+ - single reads return `null`
67
+ - chain/lookup helpers return safe empty defaults
264
68
 
265
- **Available sub-paths:** `employees`, `businessUnits`, `costcodes`, `paytypes`, `workorders`, `jobtypejobsteps`, `llm`, `rag`
69
+ ## Error Diagnostics
266
70
 
267
- > `chatStream` is not available as a remote function — use `ApiHubClient` directly to proxy the SSE response.
71
+ The package now throws structured `ApiHubError` instances for request-layer failures.
268
72
 
269
- ---
73
+ `ApiHubError` includes:
270
74
 
271
- ## ApiHubClient (direct usage)
75
+ - `status`
76
+ - `path`
77
+ - `url`
78
+ - `responseBody`
79
+ - `retriable`
80
+ - `service`
272
81
 
273
- For non-SvelteKit environments or when you need full control (caching, streaming):
82
+ Request layer behavior:
274
83
 
275
- ### Setup
84
+ - timeout with `AbortController` (default `10000ms`)
85
+ - retries for idempotent GETs on `429/502/503/504` (default `retryCount: 2`)
86
+ - optional `onError` callback via `ApiHubClient` options
276
87
 
277
- ```typescript
278
- import { ApiHubClient } from '@brownandroot/api'
88
+ Dropdown defaults:
279
89
 
280
- const client = new ApiHubClient({
281
- baseUrl: 'https://your-apihub-url.com',
282
- apiKey: 'your-api-key',
283
- cacheTtl: 5 * 60 * 1000, // optional, default 5 minutes (0 to disable)
284
- })
285
- ```
90
+ - Domains with `isActive` automatically default `isActive: true` for `dropdown(filters?)`
91
+ - Caller can override by passing `isActive` explicitly
286
92
 
287
- ---
93
+ ## Examples
288
94
 
289
- ## Employees
95
+ ### Workorders
290
96
 
291
- ### Fetching
97
+ ```ts
98
+ import { workorders } from '@brownandroot/api'
292
99
 
293
- ```typescript
294
- const employees = await client.getEmployees()
295
- const dropdown = await client.getEmployeesDropdown() // { value: employeeId, label: name }[]
296
- const employee = await client.getEmployee('12345') // throws if not found
297
- const employeePrivileged = await client.getEmployeePrivileged('12345') // includes hourlyRate and annualSalary
100
+ const rows = await workorders.getAll({ businessUnitId: 'BU001' })
101
+ const options = await workorders.dropdown({ businessUnitId: 'BU001' })
102
+ const one = await workorders.get('WO001') // Workorder | null
298
103
  ```
299
104
 
300
- ### Org hierarchy
105
+ ### Employees
301
106
 
302
- ```typescript
303
- const chain = await client.getSupervisorChain('12345')
304
- ```
305
-
306
- ### Email → JDE lookup
307
-
308
- ```typescript
309
- const { jde, employee } = await client.getJdeFromEmail('john@example.com')
310
- // jde: string | null
311
- // employee: Employee | null
312
- ```
107
+ ```ts
108
+ import { employees } from '@brownandroot/api'
313
109
 
314
- ### Identity verification
110
+ const crew = await employees.getAll({ hbu: 'TX01', q: 'john' })
111
+ const list = await employees.dropdown({ businessUnitId: 'BU001' })
112
+ const emp = await employees.get('12345')
315
113
 
316
- ```typescript
317
- const employee = await client.verifyIdentity({
114
+ const privileged = await employees.getPrivileged('12345')
115
+ const chain = await employees.getSupervisorChain('12345')
116
+ const jde = await employees.getJdeFromEmail('john@example.com')
117
+ const verified = await employees.verifyIdentity({
318
118
  first3FirstName: 'joh',
319
119
  first3LastName: 'doe',
320
120
  dob: '1985-03-15',
321
121
  ssn4: '4321',
322
122
  })
323
- // Employee on match, null on no match
324
- // Throws on 400 (missing fields) or 503 (not configured server-side)
325
- ```
326
-
327
- ### Employee fields
328
-
329
- ```typescript
330
- interface Employee {
331
- employeeId: string
332
- name: string | null
333
- email: string | null
334
- personalEmail: string | null
335
- clientEmail: string | null
336
- workEmail: string | null
337
- badgeNumber: string | null
338
- nccerNumber: string | null
339
- company: string | null
340
- businessUnitId: string | null
341
- hbu: string | null
342
- departmentCode: string | null
343
- division: string | null
344
- sector: string | null
345
- subsector: string | null
346
- phone: string | null
347
- employementStatus: string | null
348
- employeePayStatus: string | null
349
- recordType: string | null
350
- jobType: string | null
351
- jobStep: string | null
352
- jobDescription: string | null
353
- workSchedule: string | null
354
- shift: string | null
355
- payClass: string | null
356
- payFrequency: string | null
357
- payCycleCode: string | null
358
- checkRouteCode: string | null
359
- residentTaxArea: string | null
360
- workTaxArea: string | null
361
- benefitGroup: string | null
362
- topFlexPtoDate: string | null
363
- clientPtoDate: string | null
364
- securityLevel: string | null
365
- reportingLevel: string | null
366
- supervisor: string | null
367
- mentor: string | null
368
- hireDate: string | null
369
- termDate: string | null
370
- adjustedServiceDate: string | null
371
- identityHash: string | null
372
- source: string | null
373
- createdAt: string | null
374
- updatedAtJulian: number | null
375
- updatedAt: string | null
376
- }
377
-
378
- interface EmployeePrivileged extends Employee {
379
- hourlyRate: string | null
380
- annualSalary: string | null
381
- }
382
-
383
- getEmployees, getEmployee, employee searches, getJdeFromEmail, and verifyIdentity all return the public Employee shape (without compensation fields). Use getEmployeePrivileged when compensation fields are required.
384
- ```
385
-
386
- ---
387
-
388
- ## Business Units
389
-
390
- ```typescript
391
- const units = await client.getBusinessUnits()
392
- const dropdown = await client.getBusinessUnitsDropdown() // { value, label }[]
393
- const unit = await client.getBusinessUnit('BU001')
394
- ```
395
-
396
- ---
397
-
398
- ## Cost Codes
399
-
400
- ```typescript
401
- const codes = await client.getCostcodes()
402
- const dropdown = await client.getCostcodesDropdown() // { value, label }[]
403
- const code = await client.getCostcode('CC001')
404
- ```
405
-
406
- ---
407
-
408
- ## Pay Types
409
-
410
- ```typescript
411
- const types = await client.getPaytypes()
412
- const dropdown = await client.getPaytypesDropdown() // { value, label, payClass }[]
413
- const type = await client.getPaytype('PT001')
414
- ```
415
-
416
- ---
417
-
418
- ## Work Orders
419
-
420
- ```typescript
421
- const orders = await client.getWorkorders()
422
- const dropdown = await client.getWorkordersDropdown() // { value, label }[]
423
- const order = await client.getWorkorder('WO001')
424
123
  ```
425
124
 
426
- ---
427
-
428
- ## Job Type / Job Steps
429
-
430
- ```typescript
431
- const items = await client.getJobtypejobsteps()
432
- const dropdown = await client.getJobtypejobstepsDropdown() // { value, label }[]
433
- const item = await client.getJobtypejobstep('JTJS001')
434
- ```
125
+ ### LLM and RAG
435
126
 
436
- ---
127
+ ```ts
128
+ import { llm, rag } from '@brownandroot/api'
437
129
 
438
- ## LLM
439
-
440
- ### Chat completion
441
-
442
- ```typescript
443
- const result = await client.chat({
444
- messages: [{ role: 'user', content: 'Summarize this document...' }],
130
+ const logs = await llm.getLogs()
131
+ const chat = await llm.chat({
132
+ messages: [{ role: 'user', content: 'Summarize this document.' }],
445
133
  source: 'my-app',
446
134
  user: 'jane.doe',
447
- function: 'summarize',
448
- temperature: 0.7,
449
- maxTokens: 1000,
450
135
  })
451
136
 
452
- console.log(result.message.content)
453
- console.log(result.usage) // { tokensIn, tokensOut, totalTokens }
137
+ const docs = await rag.list()
138
+ const results = await rag.search({ query: 'overtime policy', topK: 5 })
454
139
  ```
455
140
 
456
- ### Streaming chat
141
+ ## Cache Management
457
142
 
458
- ```typescript
459
- const response = await client.chatStream({
460
- messages: [{ role: 'user', content: 'Hello' }],
461
- userContext: { name: 'Jane Doe', department: 'Engineering', roles: ['admin'] },
462
- useRag: true,
463
- source: 'my-app',
464
- })
465
- // Proxy to the browser, or read the SSE stream directly
466
- ```
467
-
468
- ### LLM logs
143
+ ```ts
144
+ import { clearCache } from '@brownandroot/api'
469
145
 
470
- ```typescript
471
- const logs = await client.getLlmLogs() // newest first
472
- ```
473
-
474
- ---
475
-
476
- ## Documents (RAG)
477
-
478
- ```typescript
479
- const doc = await client.uploadDocument('report.pdf', base64Content, 'jane.doe')
480
- const docs = await client.listDocuments()
481
- const results = await client.searchDocuments('overtime policy', 5)
482
- // results: { chunkId, content, fileName, documentType, score }[]
483
- await client.deleteDocument(doc.id)
484
- ```
485
-
486
- ---
487
-
488
- ## Cache management (ApiHubClient)
489
-
490
- ```typescript
491
- client.clearCache() // clear everything
492
- client.clearCache('/employees') // clear a specific path
146
+ clearCache()
147
+ clearCache('workorders')
148
+ clearCache('employees')
493
149
  ```
494
150
 
495
- ---
496
-
497
- ## Types
498
-
499
- ```typescript
500
- import type {
501
- Employee,
502
- BusinessUnit,
503
- Costcode,
504
- Paytype,
505
- Workorder,
506
- Jobtypejobstep,
507
- LlmLog,
508
- ChatMessage,
509
- ChatRequest,
510
- ChatResponse,
511
- StreamChatRequest,
512
- StreamChatUserContext,
513
- DocumentRecord,
514
- SearchResult,
515
- ApiHubClientOptions,
516
- DropdownOption,
517
- PaytypeDropdownOption,
518
- } from '@brownandroot/api'
519
-
520
- // Filter interfaces (from cache module)
521
- import type {
522
- EmployeeFilters,
523
- WorkorderFilters,
524
- CostcodeFilters,
525
- PaytypeFilters,
526
- BusinessUnitFilters,
527
- JobtypejobstepFilters,
528
- } from '@brownandroot/api/cache'
529
- ```
151
+ Call `clearCache(entity)` after mutations so next reads fetch fresh data.
530
152
 
531
- ---
153
+ ## Advanced Direct Client
532
154
 
533
- ## Error handling
155
+ `ApiHubClient` is still available for advanced usage (streaming/proxy control/custom behavior):
534
156
 
535
- All methods throw an `Error` when the API returns a non-OK response.
157
+ ```ts
158
+ import { ApiHubClient } from '@brownandroot/api'
536
159
 
537
- ```typescript
538
- try {
539
- const emp = await getEmployee('99999')
540
- } catch (err) {
541
- console.error(err.message) // "Employee not found"
542
- }
160
+ const client = new ApiHubClient({
161
+ baseUrl: process.env.APIHUB_URL!,
162
+ apiKey: process.env.APIHUB_API_KEY!,
163
+ })
543
164
  ```
544
-
545
- `verifyIdentity` returns `null` when no employee matches the inputs, and only throws for request errors (400, 503, network failure).
@@ -1,3 +1,5 @@
1
- export declare const getBusinessUnits: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").BusinessUnit[]>;
2
- export declare const getBusinessUnitsDropdown: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").DropdownOption[]>;
3
- export declare const getBusinessUnit: import("@sveltejs/kit").RemoteQueryFunction<string, import("./index.js").BusinessUnit>;
1
+ export declare const businessUnits: {
2
+ getAll: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").BusinessUnit[]>;
3
+ dropdown: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").DropdownOption[]>;
4
+ get: import("@sveltejs/kit").RemoteQueryFunction<string, import("./index.js").BusinessUnit | null>;
5
+ };
@@ -1,6 +1,26 @@
1
1
  import { query } from '$app/server';
2
2
  import { z } from 'zod';
3
3
  import { getClient } from './client.js';
4
- export const getBusinessUnits = query(async () => getClient().getBusinessUnits());
5
- export const getBusinessUnitsDropdown = query(async () => getClient().getBusinessUnitsDropdown());
6
- export const getBusinessUnit = query(z.string(), async (id) => getClient().getBusinessUnit(id));
4
+ function isNotFoundError(error) {
5
+ if (!(error instanceof Error))
6
+ return false;
7
+ return /not found/i.test(error.message);
8
+ }
9
+ async function asNullable(fetcher) {
10
+ try {
11
+ return await fetcher();
12
+ }
13
+ catch (error) {
14
+ if (isNotFoundError(error))
15
+ return null;
16
+ throw error;
17
+ }
18
+ }
19
+ const getAll = query(async () => getClient().getBusinessUnits());
20
+ const dropdown = query(async () => getClient().getBusinessUnitsDropdown());
21
+ const get = query(z.string(), async (id) => asNullable(() => getClient().getBusinessUnit(id)));
22
+ export const businessUnits = {
23
+ getAll,
24
+ dropdown,
25
+ get,
26
+ };