@brownandroot/api 1.0.0 → 1.2.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
@@ -8,7 +8,271 @@ TypeScript client for the Brown & Root APIHub data service. Provides access to e
8
8
  npm install @brownandroot/api
9
9
  ```
10
10
 
11
- ## Setup
11
+ ---
12
+
13
+ ## Client-Side Cache API (recommended for SvelteKit)
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 and a background refresh keeps the cache warm.
16
+
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.
18
+
19
+ ### Setup
20
+
21
+ Add to your app's `.env`:
22
+
23
+ ```
24
+ APIHUB_URL=https://your-apihub-url.com
25
+ APIHUB_API_KEY=your-api-key
26
+ ```
27
+
28
+ Enable remote functions in `svelte.config.js` (if not already):
29
+
30
+ ```js
31
+ kit: {
32
+ experimental: {
33
+ remoteFunctions: true
34
+ }
35
+ }
36
+ ```
37
+
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
+ ---
238
+
239
+ ## Remote Functions (server-side)
240
+
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.
242
+
243
+ ```svelte
244
+ <script lang="ts">
245
+ import { getEmployees } from '@brownandroot/api/employees'
246
+ import { getBusinessUnits } from '@brownandroot/api/businessUnits'
247
+
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:
255
+
256
+ ```typescript
257
+ import { query } from '$app/server'
258
+ import { getSupervisorChain } from '@brownandroot/api/employees'
259
+
260
+ export const getMyManagers = query(async () => {
261
+ return (await getSupervisorChain('12345')).slice(0, 2)
262
+ })
263
+ ```
264
+
265
+ **Available sub-paths:** `employees`, `businessUnits`, `costcodes`, `paytypes`, `workorders`, `jobtypejobsteps`, `llm`, `rag`
266
+
267
+ > `chatStream` is not available as a remote function — use `ApiHubClient` directly to proxy the SSE response.
268
+
269
+ ---
270
+
271
+ ## ApiHubClient (direct usage)
272
+
273
+ For non-SvelteKit environments or when you need full control (caching, streaming):
274
+
275
+ ### Setup
12
276
 
13
277
  ```typescript
14
278
  import { ApiHubClient } from '@brownandroot/api'
@@ -27,36 +291,15 @@ const client = new ApiHubClient({
27
291
  ### Fetching
28
292
 
29
293
  ```typescript
30
- // All employees
31
294
  const employees = await client.getEmployees()
32
-
33
- // Dropdown format: { value: employeeId, label: name }[]
34
- const dropdown = await client.getEmployeesDropdown()
35
-
36
- // Single employee by ID — throws if not found
37
- const employee = await client.getEmployee('12345')
38
- ```
39
-
40
- ### Search
41
-
42
- ```typescript
43
- // By name (case-insensitive partial match)
44
- const results = await client.searchByName('John')
45
-
46
- // By email (case-insensitive partial match)
47
- const results = await client.searchByEmail('john@example.com')
48
-
49
- // By home business unit (case-insensitive partial match)
50
- const results = await client.searchByHbu('TX01')
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
51
298
  ```
52
299
 
53
300
  ### Org hierarchy
54
301
 
55
302
  ```typescript
56
- // All employees reporting directly to a supervisor
57
- const reports = await client.getBySupervisor('12345')
58
-
59
- // Full supervisor chain above an employee (excludes the employee themselves)
60
303
  const chain = await client.getSupervisorChain('12345')
61
304
  ```
62
305
 
@@ -70,21 +313,14 @@ const { jde, employee } = await client.getJdeFromEmail('john@example.com')
70
313
 
71
314
  ### Identity verification
72
315
 
73
- Verifies an employee's identity from name, date of birth, and last 4 of SSN alone — no employee ID needed. Returns the full employee record on a match.
74
-
75
316
  ```typescript
76
317
  const employee = await client.verifyIdentity({
77
- first3FirstName: 'joh', // first 3 chars of first name, case-insensitive
78
- first3LastName: 'doe', // first 3 chars of last name, case-insensitive
79
- dob: '1985-03-15', // YYYY-MM-DD
80
- ssn4: '4321', // last 4 digits of SSN
318
+ first3FirstName: 'joh',
319
+ first3LastName: 'doe',
320
+ dob: '1985-03-15',
321
+ ssn4: '4321',
81
322
  })
82
-
83
- if (employee) {
84
- // identity confirmed — full Employee object returned
85
- } else {
86
- // no employee matched these inputs
87
- }
323
+ // Employee on match, null on no match
88
324
  // Throws on 400 (missing fields) or 503 (not configured server-side)
89
325
  ```
90
326
 
@@ -92,75 +328,59 @@ if (employee) {
92
328
 
93
329
  ```typescript
94
330
  interface Employee {
95
- // Identity
96
- employeeId: string
97
- name: string | null
98
- email: string | null
99
- badgeNumber: string | null
100
- nccerNumber: string | null
101
-
102
- // Organization
103
- company: string | null // company code
104
- businessUnitId: string | null
105
- hbu: string | null // home business unit
106
- departmentCode: string | null
107
- division: string | null
108
- sector: string | null
109
- subsector: string | null
110
-
111
- // Contact
112
- phone: string | null
113
-
114
- // Employment
115
- employementStatus: string | null
116
- employeePayStatus: string | null
117
- recordType: string | null
118
-
119
- // Job
120
- jobType: string | null
121
- jobStep: string | null
122
- jobDescription: string | null
123
- workSchedule: string | null
124
- shift: string | null
125
-
126
- // Pay & compensation
127
- payClass: string | null
128
- payFrequency: string | null
129
- payCycleCode: string | null
130
- checkRouteCode: string | null
131
- hourlyRate: string | null // string — format as needed
132
- annualSalary: string | null // string — format as needed
133
-
134
- // Tax
135
- residentTaxArea: string | null
136
- workTaxArea: string | null
137
-
138
- // Benefits
139
- benefitGroup: string | null
140
-
141
- // PTO
142
- topFlexPtoDate: string | null // ISO timestamp
143
- clientPtoDate: string | null // ISO timestamp
144
-
145
- // Security & reporting
146
- securityLevel: string | null
147
- reportingLevel: string | null
148
-
149
- // Relationships
150
- supervisor: string | null // supervisor employee ID
151
- mentor: string | null
152
-
153
- // Dates
154
- hireDate: string | null
155
- termDate: string | null
156
- adjustedServiceDate: string | null
157
-
158
- // Metadata
159
- source: string | null
160
- createdAt: string | null
161
- updatedAtJulian: number | null
162
- updatedAt: string | null
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
163
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.
164
384
  ```
165
385
 
166
386
  ---
@@ -171,9 +391,6 @@ interface Employee {
171
391
  const units = await client.getBusinessUnits()
172
392
  const dropdown = await client.getBusinessUnitsDropdown() // { value, label }[]
173
393
  const unit = await client.getBusinessUnit('BU001')
174
-
175
- // Search by description (case-insensitive partial match)
176
- const results = await client.searchBusinessUnits('west')
177
394
  ```
178
395
 
179
396
  ---
@@ -184,15 +401,6 @@ const results = await client.searchBusinessUnits('west')
184
401
  const codes = await client.getCostcodes()
185
402
  const dropdown = await client.getCostcodesDropdown() // { value, label }[]
186
403
  const code = await client.getCostcode('CC001')
187
-
188
- // Filtered by business unit
189
- const buDropdown = await client.getCostcodesDropdownByBu('BU001')
190
-
191
- // Filtered by business unit and pay type
192
- const buPtDropdown = await client.getCostcodesDropdownByBuAndPayType('BU001', 'PT01')
193
-
194
- // Search by description or JDE cost code (case-insensitive partial match)
195
- const results = await client.searchCostcodes('labor')
196
404
  ```
197
405
 
198
406
  ---
@@ -203,9 +411,6 @@ const results = await client.searchCostcodes('labor')
203
411
  const types = await client.getPaytypes()
204
412
  const dropdown = await client.getPaytypesDropdown() // { value, label, payClass }[]
205
413
  const type = await client.getPaytype('PT001')
206
-
207
- // Search by description (case-insensitive partial match)
208
- const results = await client.searchPaytypes('regular')
209
414
  ```
210
415
 
211
416
  ---
@@ -216,12 +421,6 @@ const results = await client.searchPaytypes('regular')
216
421
  const orders = await client.getWorkorders()
217
422
  const dropdown = await client.getWorkordersDropdown() // { value, label }[]
218
423
  const order = await client.getWorkorder('WO001')
219
-
220
- // Filtered by business unit
221
- const buDropdown = await client.getWorkordersDropdownByBu('BU001')
222
-
223
- // Search by description or client work order ID (case-insensitive partial match)
224
- const results = await client.searchWorkorders('pipe')
225
424
  ```
226
425
 
227
426
  ---
@@ -243,11 +442,11 @@ const item = await client.getJobtypejobstep('JTJS001')
243
442
  ```typescript
244
443
  const result = await client.chat({
245
444
  messages: [{ role: 'user', content: 'Summarize this document...' }],
246
- source: 'my-app', // your application name
247
- user: 'jane.doe', // user identifier for logging
248
- function: 'summarize', // optional label for logging
249
- temperature: 0.7, // optional, default 0.7
250
- maxTokens: 1000, // optional
445
+ source: 'my-app',
446
+ user: 'jane.doe',
447
+ function: 'summarize',
448
+ temperature: 0.7,
449
+ maxTokens: 1000,
251
450
  })
252
451
 
253
452
  console.log(result.message.content)
@@ -256,17 +455,13 @@ console.log(result.usage) // { tokensIn, tokensOut, totalTokens }
256
455
 
257
456
  ### Streaming chat
258
457
 
259
- Returns a raw SSE `Response` for you to proxy or consume directly.
260
-
261
458
  ```typescript
262
459
  const response = await client.chatStream({
263
460
  messages: [{ role: 'user', content: 'Hello' }],
264
461
  userContext: { name: 'Jane Doe', department: 'Engineering', roles: ['admin'] },
265
- useRag: true, // optional, search document knowledge base
266
- tools: ['...'], // optional
462
+ useRag: true,
267
463
  source: 'my-app',
268
464
  })
269
-
270
465
  // Proxy to the browser, or read the SSE stream directly
271
466
  ```
272
467
 
@@ -280,31 +475,20 @@ const logs = await client.getLlmLogs() // newest first
280
475
 
281
476
  ## Documents (RAG)
282
477
 
283
- Upload and search documents in the knowledge base.
284
-
285
478
  ```typescript
286
- // Upload a document (PDF or CSV)
287
479
  const doc = await client.uploadDocument('report.pdf', base64Content, 'jane.doe')
288
-
289
- // List all documents
290
480
  const docs = await client.listDocuments()
291
-
292
- // Search with a natural language query
293
481
  const results = await client.searchDocuments('overtime policy', 5)
294
482
  // results: { chunkId, content, fileName, documentType, score }[]
295
-
296
- // Delete a document and all its chunks
297
483
  await client.deleteDocument(doc.id)
298
484
  ```
299
485
 
300
486
  ---
301
487
 
302
- ## Cache management
303
-
304
- All GET methods cache responses client-side for `cacheTtl` milliseconds (default 5 minutes). To invalidate:
488
+ ## Cache management (ApiHubClient)
305
489
 
306
490
  ```typescript
307
- client.clearCache() // clear everything
491
+ client.clearCache() // clear everything
308
492
  client.clearCache('/employees') // clear a specific path
309
493
  ```
310
494
 
@@ -332,20 +516,30 @@ import type {
332
516
  DropdownOption,
333
517
  PaytypeDropdownOption,
334
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'
335
529
  ```
336
530
 
337
531
  ---
338
532
 
339
533
  ## Error handling
340
534
 
341
- All methods throw an `Error` when the API returns a non-OK response. The error message contains the server's detail or the HTTP status.
535
+ All methods throw an `Error` when the API returns a non-OK response.
342
536
 
343
537
  ```typescript
344
538
  try {
345
- const emp = await client.getEmployee('99999')
539
+ const emp = await getEmployee('99999')
346
540
  } catch (err) {
347
541
  console.error(err.message) // "Employee not found"
348
542
  }
349
543
  ```
350
544
 
351
- `verifyIdentity` is the exception — it returns `null` when no employee matches the inputs, and only throws for request errors (400, 503, network failure).
545
+ `verifyIdentity` returns `null` when no employee matches the inputs, and only throws for request errors (400, 503, network failure).
@@ -1,4 +1,3 @@
1
- export declare const getBusinessUnits: any;
2
- export declare const getBusinessUnitsDropdown: any;
3
- export declare const getBusinessUnit: any;
4
- export declare const searchBusinessUnits: any;
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>;
@@ -4,4 +4,3 @@ import { getClient } from './client.js';
4
4
  export const getBusinessUnits = query(async () => getClient().getBusinessUnits());
5
5
  export const getBusinessUnitsDropdown = query(async () => getClient().getBusinessUnitsDropdown());
6
6
  export const getBusinessUnit = query(z.string(), async (id) => getClient().getBusinessUnit(id));
7
- export const searchBusinessUnits = query(z.string(), async (q) => getClient().searchBusinessUnits(q));