@brownandroot/api 1.2.1 → 2.0.1

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,135 @@ 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.
40
+ ## Unified Domain Contract
236
41
 
237
- ---
42
+ Each listable domain exposes the same shape:
238
43
 
239
- ## Remote Functions (server-side)
44
+ - `getAll(filters?)`
45
+ - `dropdown(filters?)`
46
+ - `get(id)` -> returns item or `null`
240
47
 
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.
48
+ Domains:
242
49
 
243
- ```svelte
244
- <script lang="ts">
245
- import { getEmployees } from '@brownandroot/api/employees'
246
- import { getBusinessUnits } from '@brownandroot/api/businessUnits'
50
+ - `employees`
51
+ - `workorders`
52
+ - `costcodes`
53
+ - `paytypes`
54
+ - `businessUnits`
55
+ - `jobtypejobsteps`
56
+ - `llm`
57
+ - `rag`
247
58
 
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:
59
+ ## Caching Behavior
255
60
 
256
- ```typescript
257
- import { query } from '$app/server'
258
- import { getSupervisorChain } from '@brownandroot/api/employees'
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
259
68
 
260
- export const getMyManagers = query(async () => {
261
- return (await getSupervisorChain('12345')).slice(0, 2)
262
- })
263
- ```
69
+ ## Error Diagnostics
264
70
 
265
- **Available sub-paths:** `employees`, `businessUnits`, `costcodes`, `paytypes`, `workorders`, `jobtypejobsteps`, `llm`, `rag`
71
+ The package now throws structured `ApiHubError` instances for request-layer failures.
266
72
 
267
- > `chatStream` is not available as a remote function — use `ApiHubClient` directly to proxy the SSE response.
73
+ `ApiHubError` includes:
268
74
 
269
- ---
75
+ - `status`
76
+ - `path`
77
+ - `url`
78
+ - `responseBody`
79
+ - `retriable`
80
+ - `service`
270
81
 
271
- ## ApiHubClient (direct usage)
82
+ Request layer behavior:
272
83
 
273
- For non-SvelteKit environments or when you need full control (caching, streaming):
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
274
87
 
275
- ### Setup
88
+ Dropdown defaults:
276
89
 
277
- ```typescript
278
- import { ApiHubClient } from '@brownandroot/api'
90
+ - Domains with `isActive` automatically default `isActive: true` for `dropdown(filters?)`
91
+ - Caller can override by passing `isActive` explicitly
279
92
 
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
- ```
93
+ ## Examples
286
94
 
287
- ---
95
+ ### Workorders
288
96
 
289
- ## Employees
97
+ ```ts
98
+ import { workorders } from '@brownandroot/api'
290
99
 
291
- ### Fetching
292
-
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
301
-
302
- ```typescript
303
- const chain = await client.getSupervisorChain('12345')
304
- ```
105
+ ### Employees
305
106
 
306
- ### Email → JDE lookup
107
+ ```ts
108
+ import { employees } from '@brownandroot/api'
307
109
 
308
- ```typescript
309
- const { jde, employee } = await client.getJdeFromEmail('john@example.com')
310
- // jde: string | null
311
- // employee: Employee | null
312
- ```
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')
313
113
 
314
- ### Identity verification
315
-
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
123
  ```
326
124
 
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
- }
125
+ ### LLM and RAG
377
126
 
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
- ```
127
+ ```ts
128
+ import { llm, rag } from '@brownandroot/api'
415
129
 
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
- ```
425
-
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
- ```
435
-
436
- ---
437
-
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
- })
451
-
452
- console.log(result.message.content)
453
- console.log(result.usage) // { tokensIn, tokensOut, totalTokens }
454
- ```
455
-
456
- ### Streaming chat
457
-
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',
135
+ enableRag: true,
464
136
  })
465
- // Proxy to the browser, or read the SSE stream directly
466
- ```
467
137
 
468
- ### LLM logs
469
-
470
- ```typescript
471
- const logs = await client.getLlmLogs() // newest first
138
+ const docs = await rag.list()
139
+ const results = await rag.search({ query: 'overtime policy', topK: 5 })
472
140
  ```
473
141
 
474
- ---
475
-
476
- ## Documents (RAG)
142
+ `llm.chat` RAG flags:
477
143
 
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
- ```
144
+ - `enableRag: true` tells the backend to enrich the chat prompt with retrieved document context before generating a response.
145
+ - `enableRag: false` (or omitted) sends a normal chat request without forcing retrieval augmentation.
146
+ - `enableRag` is the single supported parameter for toggling default tools/RAG retrieval.
485
147
 
486
- ---
148
+ ## Cache Management
487
149
 
488
- ## Cache management (ApiHubClient)
150
+ ```ts
151
+ import { clearCache } from '@brownandroot/api'
489
152
 
490
- ```typescript
491
- client.clearCache() // clear everything
492
- client.clearCache('/employees') // clear a specific path
153
+ clearCache()
154
+ clearCache('workorders')
155
+ clearCache('employees')
493
156
  ```
494
157
 
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
- ```
158
+ Call `clearCache(entity)` after mutations so next reads fetch fresh data.
530
159
 
531
- ---
160
+ ## Advanced Direct Client
532
161
 
533
- ## Error handling
162
+ `ApiHubClient` is still available for advanced usage (streaming/proxy control/custom behavior):
534
163
 
535
- All methods throw an `Error` when the API returns a non-OK response.
164
+ ```ts
165
+ import { ApiHubClient } from '@brownandroot/api'
536
166
 
537
- ```typescript
538
- try {
539
- const emp = await getEmployee('99999')
540
- } catch (err) {
541
- console.error(err.message) // "Employee not found"
542
- }
167
+ const client = new ApiHubClient({
168
+ baseUrl: process.env.APIHUB_URL!,
169
+ apiKey: process.env.APIHUB_API_KEY!,
170
+ })
543
171
  ```
544
-
545
- `verifyIdentity` returns `null` when no employee matches the inputs, and only throws for request errors (400, 503, network failure).
@@ -1,3 +1,3 @@
1
1
  export declare const getBusinessUnits: import("@sveltejs/kit").RemoteQueryFunction<void, import("./index.js").BusinessUnit[]>;
2
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>;
3
+ export declare const getBusinessUnit: import("@sveltejs/kit").RemoteQueryFunction<string, import("./index.js").BusinessUnit | null>;
@@ -1,6 +1,21 @@
1
1
  import { query } from '$app/server';
2
2
  import { z } from 'zod';
3
3
  import { getClient } from './client.js';
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
+ }
4
19
  export const getBusinessUnits = query(async () => getClient().getBusinessUnits());
5
20
  export const getBusinessUnitsDropdown = query(async () => getClient().getBusinessUnitsDropdown());
6
- export const getBusinessUnit = query(z.string(), async (id) => getClient().getBusinessUnit(id));
21
+ export const getBusinessUnit = query(z.string(), async (id) => asNullable(() => getClient().getBusinessUnit(id)));