@d19n/youfibre-odin-sdk 1.0.245 → 1.0.246

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.
Files changed (2) hide show
  1. package/README.md +223 -1100
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,1197 +1,320 @@
1
- # YouFibre ODIN SDK
1
+ # ODIN SDK
2
2
 
3
- Typed SDK for the ODIN platform providing full type safety for records, actions, and properties.
3
+ Type-safe SDK for the ODIN platform with comprehensive entity and action support.
4
4
 
5
5
  ## Table of Contents
6
6
 
7
7
  - [Installation](#installation)
8
8
  - [Quick Start](#quick-start)
9
- - [API Client (External Access)](#api-client-external-access)
10
- - [DB Client (Backend Services)](#db-client-backend-services)
11
- - [Working with Records](#working-with-records)
12
- - [Executing Actions](#executing-actions)
13
- - [Searching Records](#searching-records)
14
- - [Linking Records](#linking-records)
15
- - [Migration Guide](#migration-guide)
16
9
  - [Architecture](#architecture)
17
-
18
- ---
10
+ - [API Client](#api-client)
11
+ - [DB Client](#db-client)
12
+ - [Entities](#entities)
13
+ - [Actions](#actions)
14
+ - [Record Wrappers](#record-wrappers)
15
+ - [Events](#events)
16
+ - [Migration Guide](#migration-guide)
19
17
 
20
18
  ## Installation
21
19
 
22
20
  ```bash
23
- npm install @youfibre/odin-sdk
21
+ npm install @d19n/youfibre-odin-sdk
24
22
  ```
25
23
 
26
- ---
27
-
28
24
  ## Quick Start
29
25
 
30
- ### API Access (Scripts, External Clients)
26
+ ### API Client (Frontend/External Services)
31
27
 
32
28
  ```typescript
33
- import { OdinApiClient } from '@youfibre/odin-sdk';
29
+ import { OdinApiClient } from '@d19n/youfibre-odin-sdk';
34
30
 
31
+ // Initialize with OAuth credentials
35
32
  const odin = new OdinApiClient({
36
- host: 'api.youfibre.com',
37
- clientId: process.env.ODIN_CLIENT_ID!,
38
- clientSecret: process.env.ODIN_CLIENT_SECRET!,
33
+ host: process.env.ODIN_HOST_URL,
34
+ clientId: process.env.ODIN_CLIENT_ID,
35
+ clientSecret: process.env.ODIN_CLIENT_SECRET,
39
36
  });
40
37
 
41
- // Get an order and execute an action
42
- const order = await odin.orders.get('ORD-123');
43
- console.log(order.properties.Status);
44
-
45
- await order.activateOrder({ ActiveDate: '2025-01-15' });
46
- ```
47
-
48
- ### DB Access (Backend Services)
49
-
50
- ```typescript
51
- import { OdinDbClient } from '@youfibre/odin-sdk';
52
-
53
- // In a NestJS service
54
- @Injectable()
55
- export class OrderService {
56
- constructor(private readonly dbService: DbService) {}
57
-
58
- async activateOrder(principal: any, orderId: string) {
59
- const odin = new OdinDbClient(principal, this.dbService);
60
-
61
- const order = await odin.orders.get(orderId);
62
- await order.activateOrder({ ActiveDate: new Date().toISOString() });
63
- }
64
- }
65
- ```
66
-
67
- ---
68
-
69
- ## API Client (External Access)
70
-
71
- Use `OdinApiClient` for external API access from scripts, CLIs, or external services.
72
-
73
- ### Constructor Options
74
-
75
- ```typescript
76
- // Option 1: OAuth credentials
77
- const odin = new OdinApiClient({
78
- host: 'api.youfibre.com',
79
- clientId: 'your-client-id',
80
- clientSecret: 'your-client-secret',
81
- });
82
-
83
- // Option 2: Existing JWT token
84
- const odin = OdinApiClient.withToken('eyJhbG...', 'api.youfibre.com');
85
-
86
- // Option 3: Existing ApiProvider
87
- import { ApiProvider } from '@d19n/odin-sdk-generator';
88
- const provider = new ApiProvider({ host, clientId, clientSecret });
89
- const odin = new OdinApiClient(provider);
90
- ```
91
-
92
- ### Available Accessors
93
-
94
- All entity accessors are lazy-loaded on first access:
95
-
96
- ```typescript
97
- odin.orders // OrderAccessor
98
- odin.contacts // ContactAccessor
99
- odin.addresses // AddressAccessor
100
- odin.accounts // AccountAccessor
101
- odin.invoices // InvoiceAccessor
102
- odin.workOrders // WorkOrderAccessor
103
- odin.visits // VisitAccessor
104
- // ... and all other entities
105
- ```
106
-
107
- ---
108
-
109
- ## DB Client (Backend Services)
110
-
111
- Use `OdinDbClient` for direct database access in backend services (NestJS, etc.).
112
-
113
- ### Constructor
114
-
115
- ```typescript
116
- const odin = new OdinDbClient(principal, dbService);
117
- ```
118
-
119
- - `principal`: The authenticated user context
120
- - `dbService`: The database service instance (typically injected)
121
-
122
- ### Example: NestJS Service
123
-
124
- ```typescript
125
- import { Injectable } from '@nestjs/common';
126
- import { OdinDbClient } from '@youfibre/odin-sdk';
127
- import { DbService } from '@d19n/schema-manager/dist/db/db.service';
128
-
129
- @Injectable()
130
- export class WorkOrderService {
131
- constructor(private readonly dbService: DbService) {}
132
-
133
- async completeWorkOrder(principal: any, workOrderId: string) {
134
- const odin = new OdinDbClient(principal, this.dbService);
135
-
136
- const workOrder = await odin.workOrders.get(workOrderId);
137
-
138
- await workOrder.completeWorkOrder({
139
- CompletedDate: new Date().toISOString(),
140
- Notes: 'Work completed successfully',
141
- });
142
- }
143
- }
144
- ```
145
-
146
- ---
147
-
148
- ## Working with Records
149
-
150
- ### Getting Records
151
-
152
- ```typescript
153
- // Get single record by ID
154
- const order = await odin.orders.get('uuid-here');
155
-
156
- // Get multiple records by IDs
157
- const orders = await odin.orders.getMany(['uuid-1', 'uuid-2', 'uuid-3']);
158
- ```
159
-
160
- ### Record Properties
161
-
162
- Records are wrapped with typed access to all properties:
163
-
164
- ```typescript
38
+ // Get a record with typed properties
165
39
  const order = await odin.orders.get(orderId);
40
+ console.log(order.properties?.Status);
41
+
42
+ // Execute a simple action
43
+ await order.activateOrder({ ActivationDate: "2025-01-01" });
44
+
45
+ // Execute a multi-step flow action
46
+ const cboRec = await odin.contractBuyOuts.get(cboId);
47
+ await cboRec
48
+ .moveCboToApproved({ journeyId: order.id })
49
+ .approveCbo(cboRec.id, {
50
+ Amount: cboRec.properties.Amount,
51
+ FinalBillVerified: cboRec.properties.FinalBillVerified,
52
+ })
53
+ .execute();
166
54
 
167
- // Core properties
168
- order.id // string
169
- order.entity // string (e.g., 'OrderModule:Order')
170
- order.type // string | undefined
171
- order.title // string | undefined
172
- order.recordNumber // string | undefined
173
- order.stage // PipelineStageEntity | undefined
174
- order.createdAt // string | undefined
175
- order.updatedAt // string | undefined
176
-
177
- // Entity-specific properties (fully typed)
178
- order.properties.Status // string
179
- order.properties.ActiveDate // string
180
- order.properties.ContractType // string
181
- // ... all properties are typed
182
-
183
- // Access raw OdinRecord if needed
184
- order.raw // OdinRecord<OrderProperties>
185
- ```
186
-
187
- ---
188
-
189
- ## Executing Actions
190
-
191
- Actions are typed methods directly on the record wrapper:
192
-
193
- ```typescript
194
- const order = await odin.orders.get(orderId);
195
-
196
- // Actions with required properties
197
- await order.activateOrder({
198
- ActiveDate: '2025-01-15',
199
- });
200
-
201
- // Actions with optional properties
202
- await order.updateOrder({
203
- Status: 'Active',
204
- Notes: 'Updated via SDK',
205
- });
206
-
207
- // Actions with journey/stage options
208
- await order.activateOrder(
209
- { ActiveDate: '2025-01-15' },
210
- { journeyId: 'journey-uuid', stageKey: 'StageActive' }
211
- );
212
- ```
213
-
214
- ### Action Results
215
-
216
- All actions return `ActionResult[]`:
217
-
218
- ```typescript
219
- const results = await order.activateOrder({ ActiveDate: '2025-01-15' });
220
-
221
- for (const result of results) {
222
- console.log(result.id); // Record ID
223
- console.log(result.entity); // Entity name
224
- console.log(result.type); // Action type
225
- }
226
- ```
227
-
228
- ---
229
-
230
- ## Searching Records
231
-
232
- ODIN uses a structured query format with `OdinQuery` and `OdinFilter` types.
233
-
234
- ### Query Structure
235
-
236
- ```typescript
237
- interface OdinFilter {
238
- columnName: string; // Property name (e.g., 'Status', 'Contractor')
239
- operator: OdinOperator; // 'eq', 'in', 'not in', 'gte', 'lte', 'gt', 'lt', etc.
240
- value: any; // Filter value(s)
241
- // Optional:
242
- linkedEntity?: string; // For filtering on linked records
243
- linkedRelationType?: string; // 'child' | 'parent'
244
- caseSensitive?: boolean; // Default: false
245
- }
246
-
247
- interface OdinQuery {
248
- entity: string; // Use 'infer' to auto-detect from accessor
249
- type: 'and'; // Currently only 'and' is supported
250
- value: OdinFilter[]; // Array of filters (AND'd together)
251
- pageSize: number; // Max 1000 per request
252
- pageNumber: number; // 0-indexed
253
- sort?: Record<string, { order: 'asc' | 'desc' }>;
254
- returnProperties?: string[]; // Fields to return
255
- searchAfter?: any[]; // For cursor-based pagination
256
- includeDeleted?: boolean;
257
- includeArchived?: boolean;
258
- }
259
-
260
- // Available operators
261
- type OdinOperator =
262
- | 'eq' // Equals
263
- | 'in' // In array
264
- | 'not in' // Not in array
265
- | 'gte' // Greater than or equal
266
- | 'lte' // Less than or equal
267
- | 'gt' // Greater than
268
- | 'lt' // Less than
269
- | 'isNull' // Is null
270
- | 'notNull' // Is not null
271
- | 'anyTerm' // Contains any term (text search)
272
- | 'allTerm' // Contains all terms (text search)
273
- | 'phrase' // Exact phrase match
274
- | 'nested'; // Nested object query
275
- ```
276
-
277
- ### Basic Search
278
-
279
- ```typescript
280
- const result = await odin.orders.search({
281
- query: {
282
- entity: 'infer',
283
- type: 'and',
284
- value: [
285
- {
286
- columnName: 'Status',
287
- operator: 'eq',
288
- value: 'Active',
289
- },
290
- ],
291
- returnProperties: ['id', 'title', 'type', 'properties.*'],
292
- pageSize: 100,
293
- pageNumber: 0,
294
- },
295
- });
296
-
297
- console.log(result.records); // OrderRecord[]
298
- console.log(result.totalRecords); // number
299
- console.log(result.hasMore); // boolean
300
- ```
301
-
302
- ### Search with Multiple Filters
303
-
304
- Filters are AND'd together:
305
-
306
- ```typescript
307
- const result = await odin.orders.search({
308
- query: {
309
- entity: 'infer',
310
- type: 'and',
311
- value: [
312
- {
313
- columnName: 'Status',
314
- operator: 'in',
315
- value: ['Active', 'Pending'],
316
- },
317
- {
318
- columnName: 'ContractType',
319
- operator: 'eq',
320
- value: 'Residential',
321
- },
322
- {
323
- columnName: 'ActiveDate',
324
- operator: 'gte',
325
- value: '2025-01-01',
326
- },
327
- ],
328
- returnProperties: ['id', 'title', 'type', 'properties.*', 'stage'],
329
- sort: {
330
- createdAt: { order: 'desc' },
331
- },
332
- pageSize: 100,
333
- pageNumber: 0,
334
- },
335
- });
336
- ```
337
-
338
- ### Excluding Values (NOT IN)
339
-
340
- ```typescript
341
- const result = await odin.workOrders.search({
342
- query: {
343
- entity: 'infer',
344
- type: 'and',
345
- value: [
346
- {
347
- columnName: 'EntityType',
348
- operator: 'not in',
349
- value: ['COLLECTION', 'DISPATCH'],
350
- },
351
- {
352
- columnName: 'Contractor',
353
- operator: 'not in',
354
- value: ['INTERNAL'],
355
- },
356
- ],
357
- pageSize: 1000,
358
- pageNumber: 0,
359
- },
360
- });
361
- ```
362
-
363
- ### Date Range Queries
364
-
365
- ```typescript
366
- const result = await odin.visits.search({
367
- query: {
368
- entity: 'infer',
369
- type: 'and',
370
- value: [
371
- {
372
- columnName: 'ScheduledDate',
373
- operator: 'gte',
374
- value: '2025-01-01',
375
- },
376
- {
377
- columnName: 'ScheduledDate',
378
- operator: 'lte',
379
- value: '2025-01-31',
380
- },
381
- ],
382
- sort: {
383
- ScheduledDate: { order: 'asc' },
384
- },
385
- pageSize: 500,
386
- pageNumber: 0,
387
- },
388
- });
389
- ```
390
-
391
- ### Text Search
392
-
393
- ```typescript
394
- // Match any term
395
- const result = await odin.contacts.search({
396
- query: {
397
- entity: 'infer',
398
- type: 'and',
399
- value: [
400
- {
401
- columnName: 'FirstName',
402
- operator: 'anyTerm',
403
- value: 'john smith', // Matches 'john' OR 'smith'
404
- },
405
- ],
406
- pageSize: 50,
407
- pageNumber: 0,
408
- },
409
- });
410
-
411
- // Match exact phrase
412
- const result = await odin.contacts.search({
413
- query: {
414
- entity: 'infer',
415
- type: 'and',
416
- value: [
417
- {
418
- columnName: 'FullName',
419
- operator: 'phrase',
420
- value: 'John Smith', // Matches exact phrase
421
- },
422
- ],
423
- pageSize: 50,
424
- pageNumber: 0,
425
- },
426
- });
55
+ // Create a new record with links
56
+ const note = odin.notes.new();
57
+ await note
58
+ .link({ id: orderId, entity: "OrderModule:Order" })
59
+ .createNote({ Body: "Customer called about billing" });
427
60
  ```
428
61
 
429
- ### Null Checks
62
+ ### DB Client (Backend Services)
430
63
 
431
64
  ```typescript
432
- const result = await odin.orders.search({
433
- query: {
434
- entity: 'infer',
435
- type: 'and',
436
- value: [
437
- {
438
- columnName: 'CancelledDate',
439
- operator: 'isNull',
440
- value: true,
441
- },
442
- {
443
- columnName: 'ActiveDate',
444
- operator: 'notNull',
445
- value: true,
446
- },
447
- ],
448
- pageSize: 100,
449
- pageNumber: 0,
450
- },
451
- });
452
- ```
65
+ import { OdinDbClient } from '@d19n/youfibre-odin-sdk';
453
66
 
454
- ### Selecting Return Properties
67
+ // Initialize with principal and dbService (NestJS)
68
+ const odin = new OdinDbClient(principal, this.dbService);
455
69
 
456
- Control which fields are returned to reduce payload size:
457
-
458
- ```typescript
459
- const result = await odin.orders.search({
460
- query: {
461
- entity: 'infer',
462
- type: 'and',
463
- value: [
464
- { columnName: 'Status', operator: 'eq', value: 'Active' },
465
- ],
466
- returnProperties: [
467
- 'id',
468
- 'title',
469
- 'type',
470
- 'entity',
471
- 'recordNumber',
472
- 'properties.*', // All properties
473
- 'stage', // Pipeline stage
474
- 'links.*', // All linked records
475
- ],
476
- pageSize: 100,
477
- pageNumber: 0,
478
- },
479
- });
70
+ // Same API as ApiClient
71
+ const workOrder = await odin.workOrders.get(workOrderId);
72
+ await workOrder.completeWorkOrder({ ... });
480
73
  ```
481
74
 
482
- ### Sorting Results
75
+ ## Architecture
483
76
 
484
- ```typescript
485
- const result = await odin.orders.search({
486
- query: {
487
- entity: 'infer',
488
- type: 'and',
489
- value: [
490
- { columnName: 'Status', operator: 'eq', value: 'Active' },
491
- ],
492
- sort: {
493
- createdAt: { order: 'desc' }, // Primary sort
494
- 'properties.Priority': { order: 'asc' }, // Secondary sort
495
- },
496
- pageSize: 100,
497
- pageNumber: 0,
498
- },
499
- });
500
77
  ```
78
+ ┌─────────────────────────────────────────────────────────────┐
79
+ │ ODIN SDK │
80
+ ├─────────────────────────────────────────────────────────────┤
81
+ │ │
82
+ │ ┌─────────────────┐ ┌─────────────────┐ │
83
+ │ │ OdinApiClient │ │ OdinDbClient │ │
84
+ │ │ (HTTP/OAuth) │ │ (Direct DB) │ │
85
+ │ └────────┬────────┘ └────────┬────────┘ │
86
+ │ │ │ │
87
+ │ └───────────┬───────────────┘ │
88
+ │ │ │
89
+ │ ┌───────────▼───────────┐ │
90
+ │ │ Entity Accessors │ │
91
+ │ │ (orders, contacts) │ │
92
+ │ └───────────┬───────────┘ │
93
+ │ │ │
94
+ │ ┌───────────▼───────────┐ │
95
+ │ │ Record Wrappers │ │
96
+ │ │ (OrderRecord, etc) │ │
97
+ │ └───────────┬───────────┘ │
98
+ │ │ │
99
+ │ ┌───────────▼───────────┐ │
100
+ │ │ Action Methods │ │
101
+ │ │ (activateOrder...) │ │
102
+ │ └───────────────────────┘ │
103
+ │ │
104
+ └─────────────────────────────────────────────────────────────┘
105
+ ```
106
+
107
+ ### Key Components
108
+
109
+ | Component | Purpose |
110
+ |-----------|---------|
111
+ | **OdinApiClient** | HTTP-based client for frontend/external services |
112
+ | **OdinDbClient** | Direct database client for backend services |
113
+ | **Accessors** | Entity-specific access (`.orders`, `.contacts`) |
114
+ | **Record Wrappers** | Typed records with action methods |
115
+ | **Action DTOs** | Type-safe action parameters |
116
+ | **Properties** | Typed entity properties interfaces |
117
+
118
+ ## Entities
119
+
120
+ This SDK includes **0** entities across **0** modules.
121
+
122
+ ## Actions
123
+
124
+ This SDK includes **0** actions.
125
+
126
+ ### Action Types
127
+
128
+ | Type | Description |
129
+ |------|-------------|
130
+ | `CREATE` | Creates a new record (no existing record required) |
131
+ | `UPDATE` | Updates an existing record |
132
+ | `STEP_FLOW` | Multi-step workflow with FlowBuilder pattern |
133
+
134
+ ### Actions by Entity
135
+
136
+ ## Record Wrappers
137
+
138
+ Record wrappers provide typed access to record data and action methods.
501
139
 
502
- ### Search All (Auto-Pagination)
503
-
504
- Returns ALL matching records (up to ~1M) with automatic pagination:
140
+ ### Basic Usage
505
141
 
506
142
  ```typescript
507
- const allOrders = await odin.orders.searchAll({
508
- query: {
509
- entity: 'infer',
510
- type: 'and',
511
- value: [
512
- {
513
- columnName: 'Status',
514
- operator: 'in',
515
- value: ['Active', 'Pending'],
516
- },
517
- {
518
- columnName: 'UpdatedAt',
519
- operator: 'gte',
520
- value: '2025-01-01',
521
- },
522
- ],
523
- returnProperties: [
524
- 'id',
525
- 'title',
526
- 'type',
527
- 'entity',
528
- 'schemaId',
529
- 'recordNumber',
530
- 'properties.*',
531
- 'stage',
532
- ],
533
- sort: {
534
- createdAt: { order: 'asc' }, // Required for consistent pagination
535
- },
536
- pageSize: 1000, // Max per request
537
- pageNumber: 0,
538
- },
539
- });
540
-
541
- console.log(allOrders.length); // Could be thousands
542
- ```
543
-
544
- ### Paginate (Generator)
545
-
546
- For memory-efficient processing of large datasets:
143
+ // Get existing record
144
+ const order = await odin.orders.get(orderId);
547
145
 
548
- ```typescript
549
- for await (const batch of odin.orders.paginate({
550
- query: {
551
- entity: 'infer',
552
- type: 'and',
553
- value: [
554
- { columnName: 'Status', operator: 'eq', value: 'Active' },
555
- ],
556
- sort: {
557
- createdAt: { order: 'asc' },
558
- },
559
- pageSize: 500,
560
- pageNumber: 0,
561
- },
562
- })) {
563
- // Process batch of 500 records
564
- for (const order of batch) {
565
- await processOrder(order);
566
- }
146
+ // Access typed properties
147
+ console.log(order.id); // string
148
+ console.log(order.properties); // OrderProperties
149
+ console.log(order.raw); // Full OdinRecord
150
+
151
+ // Check if record is new (unsaved)
152
+ if (order.isNew) {
153
+ // Can only call CREATE actions
154
+ await order.createOrder({ ... });
155
+ } else {
156
+ // Can call UPDATE actions
157
+ await order.updateOrder({ ... });
567
158
  }
568
159
  ```
569
160
 
570
- ### Searching Linked Records
571
-
572
- Filter by properties on linked entities:
573
-
574
- ```typescript
575
- // Find orders linked to a specific account
576
- const result = await odin.orders.search({
577
- query: {
578
- entity: 'infer',
579
- type: 'and',
580
- value: [
581
- {
582
- columnName: 'Status',
583
- operator: 'eq',
584
- value: 'Active',
585
- },
586
- {
587
- columnName: 'AccountNumber',
588
- operator: 'eq',
589
- value: 'ACC-12345',
590
- linkedEntity: 'CrmModule:Account',
591
- linkedRelationType: 'parent',
592
- },
593
- ],
594
- pageSize: 100,
595
- pageNumber: 0,
596
- },
597
- });
598
- ```
599
-
600
- ---
601
-
602
- ## Fluent Query Builder
603
-
604
- For a cleaner, more readable API, use the fluent query builder:
605
-
606
- ### Basic Usage
161
+ ### Link Management
607
162
 
608
163
  ```typescript
609
- const results = await odin.orders
610
- .query()
611
- .where('Status', 'eq', 'Active')
612
- .limit(100)
613
- .execute();
164
+ // Queue links to be sent with next action
165
+ const note = odin.notes.new();
166
+ await note
167
+ .link({ id: orderId, entity: "OrderModule:Order" })
168
+ .link({ id: contactId, entity: "CrmModule:Contact" })
169
+ .createNote({ Body: "Note content" });
614
170
 
615
- console.log(results.records); // OrderRecord[]
616
- console.log(results.totalRecords); // number
617
- console.log(results.hasMore); // boolean
171
+ // Unlink (immediate API call)
172
+ await order.unlink(contactId);
618
173
  ```
619
174
 
620
- ### Chaining Filters
621
-
622
- All filters are AND'd together:
175
+ ### STEP_FLOW Actions (Multi-Step Workflows)
623
176
 
624
- ```typescript
625
- const results = await odin.orders
626
- .query()
627
- .where('Status', 'in', ['Active', 'Pending'])
628
- .where('ContractType', 'eq', 'Residential')
629
- .where('ActiveDate', 'gte', '2025-01-01')
630
- .whereNotIn('CancellationReason', ['Duplicate', 'Test'])
631
- .select('id', 'title', 'properties.*', 'stage')
632
- .sortBy('createdAt', 'desc')
633
- .limit(100)
634
- .execute();
635
- ```
177
+ STEP_FLOW actions allow executing multiple steps in a single transaction using a FlowBuilder pattern.
636
178
 
637
- ### Available Filter Methods
179
+ #### Basic Usage
638
180
 
639
181
  ```typescript
640
- // Equality
641
- .where('Status', 'eq', 'Active')
642
- .whereEq('Status', 'Active') // Shorthand
643
-
644
- // In / Not In
645
- .where('Status', 'in', ['Active', 'Pending'])
646
- .whereIn('Status', ['Active', 'Pending']) // Shorthand
647
- .whereNotIn('Status', ['Cancelled', 'Draft'])
648
-
649
- // Comparison
650
- .where('Amount', 'gte', 100)
651
- .whereGte('Amount', 100) // Shorthand
652
- .whereLte('Amount', 1000)
653
- .whereGt('Amount', 0)
654
- .whereLt('Amount', 10000)
655
-
656
- // Date Range
657
- .whereBetween('CreatedAt', '2025-01-01', '2025-01-31')
658
-
659
- // Null Checks
660
- .whereNull('CancelledDate')
661
- .whereNotNull('ActiveDate')
662
-
663
- // Text Search
664
- .whereAnyTerm('Description', 'urgent priority') // OR match
665
- .whereAllTerms('Description', 'urgent priority') // AND match
666
- .wherePhrase('Title', 'Service Order') // Exact phrase
667
-
668
- // Linked Entity
669
- .whereLinked('AccountNumber', 'eq', 'ACC-123', 'CrmModule:Account', 'parent')
670
- ```
671
-
672
- ### Projection & Sorting
182
+ // Get the record
183
+ const cboRec = await odin.contractBuyOuts.get(cbo.id);
673
184
 
674
- ```typescript
675
- const results = await odin.orders
676
- .query()
677
- .where('Status', 'eq', 'Active')
678
- .select('id', 'title', 'type', 'properties.*', 'stage', 'links.*')
679
- .sortBy('createdAt', 'desc')
680
- .sortBy('properties.Priority', 'asc') // Secondary sort
681
- .execute();
185
+ // Execute a multi-step flow
186
+ await cboRec
187
+ .moveCboToApproved({ journeyId: order.id }) // Start flow (returns FlowBuilder)
188
+ .approveCbo(cbo.id, { // UPDATE step - id required first
189
+ Amount: cbo.properties.Amount,
190
+ FinalBillVerified: cbo.properties.FinalBillVerified,
191
+ PreviousProvider: cbo.properties.PreviousProvider,
192
+ })
193
+ .execute(); // Execute the flow
682
194
  ```
683
195
 
684
- ### Pagination
196
+ #### Step Method Signatures
685
197
 
686
- ```typescript
687
- // Limit and page
688
- const page1 = await odin.orders.query()
689
- .where('Status', 'eq', 'Active')
690
- .limit(100)
691
- .page(0)
692
- .execute();
693
-
694
- const page2 = await odin.orders.query()
695
- .where('Status', 'eq', 'Active')
696
- .limit(100)
697
- .page(1)
698
- .execute();
699
-
700
- // Using offset
701
- const results = await odin.orders.query()
702
- .limit(100)
703
- .offset(200) // Skip first 200, returns 200-299
704
- .execute();
705
- ```
706
-
707
- ### Convenience Methods
198
+ Step methods have different signatures based on their action type:
708
199
 
200
+ **UPDATE steps** - Require record `id` as first parameter:
709
201
  ```typescript
710
- // Get just records (no metadata)
711
- const records = await odin.orders
712
- .query()
713
- .where('Status', 'eq', 'Active')
714
- .getMany();
715
-
716
- // Get single record
717
- const order = await odin.orders
718
- .query()
719
- .where('OrderNumber', 'eq', 'ORD-12345')
720
- .getOne(); // Returns OrderRecord | null
721
-
722
- // Get single record or throw
723
- const order = await odin.orders
724
- .query()
725
- .where('OrderNumber', 'eq', 'ORD-12345')
726
- .getOneOrFail(); // Throws if not found
727
-
728
- // Count matching records
729
- const count = await odin.orders
730
- .query()
731
- .where('Status', 'eq', 'Active')
732
- .count();
733
-
734
- // Check if any match
735
- const hasActive = await odin.orders
736
- .query()
737
- .where('Status', 'eq', 'Active')
738
- .exists();
202
+ // UPDATE step signature: (id, properties, options?)
203
+ .approveCbo(id: string, properties: ApproveCboProperties, options?)
204
+ .updateWorkOrder(id: string, properties: UpdateWorkOrderProperties, options?)
739
205
  ```
740
206
 
741
- ### Fetch All with Auto-Pagination
742
-
207
+ **CREATE steps** - No `id` parameter (creates new record):
743
208
  ```typescript
744
- // WARNING: Can return up to ~1M records
745
- const allOrders = await odin.orders
746
- .query()
747
- .where('Status', 'in', ['Active', 'Pending'])
748
- .where('UpdatedAt', 'gte', '2025-01-01')
749
- .select('id', 'title', 'properties.*')
750
- .sortBy('createdAt', 'asc') // Required for consistent pagination
751
- .limit(1000)
752
- .executeAll();
753
-
754
- console.log(allOrders.length); // Could be thousands
209
+ // CREATE step signature: (properties, options?)
210
+ .createFollowUp(properties: CreateFollowUpProperties, options?)
211
+ .createNote(properties: CreateNoteProperties, options?)
755
212
  ```
756
213
 
757
- ### Memory-Efficient Pagination
758
-
759
- For processing large datasets without loading all into memory:
214
+ #### Complex Multi-Step Example
760
215
 
761
216
  ```typescript
762
- for await (const batch of odin.orders
763
- .query()
764
- .where('Status', 'eq', 'Active')
765
- .sortBy('createdAt', 'asc')
766
- .limit(500)
767
- .paginate()
768
- ) {
769
- // Process batch of 500 records
770
- for (const order of batch) {
771
- await processOrder(order);
772
- }
773
- }
774
- ```
775
-
776
- ### Include Deleted/Archived
217
+ // Get the work order record
218
+ const workOrderRec = await odin.workOrders.get(workOrderId);
777
219
 
778
- ```typescript
779
- const results = await odin.orders
780
- .query()
781
- .where('Status', 'eq', 'Cancelled')
782
- .includeDeleted()
783
- .includeArchived()
220
+ // Flow with both UPDATE and CREATE steps
221
+ await workOrderRec
222
+ .completeWorkOrderFlow({ journeyId: parentOrder.id })
223
+ .updateWorkOrder(workOrderRec.id, { // UPDATE step - id required
224
+ Status: "COMPLETE",
225
+ CompletedAt: new Date().toISOString(),
226
+ })
227
+ .createFollowUp({ // CREATE step - no id
228
+ Type: "QUALITY_CHECK",
229
+ Description: "Post-completion review",
230
+ })
231
+ .addGroup("QUALITY") // Optional: add groups
784
232
  .execute();
785
233
  ```
786
234
 
787
- ### Clone & Reuse Queries
235
+ #### With Associations
788
236
 
789
237
  ```typescript
790
- const baseQuery = odin.orders
791
- .query()
792
- .where('ContractType', 'eq', 'Residential')
793
- .select('id', 'title', 'properties.*')
794
- .sortBy('createdAt', 'desc');
795
-
796
- // Clone and add more filters
797
- const activeOrders = await baseQuery
798
- .clone()
799
- .where('Status', 'eq', 'Active')
800
- .execute();
801
-
802
- const pendingOrders = await baseQuery
803
- .clone()
804
- .where('Status', 'eq', 'Pending')
805
- .execute();
806
- ```
807
-
808
- ### Build Query Without Executing
238
+ // Link records during flow execution
239
+ const invoiceRec = await odin.invoices.get(invoiceId);
809
240
 
810
- ```typescript
811
- // Get the raw OdinQuery object
812
- const query = odin.orders
813
- .query()
814
- .where('Status', 'eq', 'Active')
815
- .limit(100)
816
- .build();
817
-
818
- console.log(query);
819
- // {
820
- // entity: 'infer',
821
- // type: 'and',
822
- // value: [{ columnName: 'Status', operator: 'eq', value: 'Active' }],
823
- // pageSize: 100,
824
- // pageNumber: 0
825
- // }
241
+ await invoiceRec
242
+ .link({ id: contactId, entity: "CrmModule:Contact" }) // Queue link
243
+ .processInvoiceFlow({ journeyId: orderId })
244
+ .approveInvoice(invoiceRec.id, {
245
+ ApprovedBy: userId,
246
+ ApprovedAt: new Date().toISOString(),
247
+ })
248
+ .execute(); // Links are sent with the action
826
249
  ```
827
250
 
828
- ### Debug with Query Plan
251
+ ## Events
829
252
 
830
- See the exact Elasticsearch query that will be executed (useful for debugging):
253
+ Subscribe to RabbitMQ events using the exported routing keys:
831
254
 
832
255
  ```typescript
833
- const plan = await odin.orders
834
- .query()
835
- .where('Status', 'in', ['Active', 'Pending'])
836
- .where('UpdatedAt', 'gte', '2025-01-01')
837
- .select('id', 'title', 'properties.*')
838
- .sortBy('createdAt', 'asc')
839
- .limit(100)
840
- .returnQueryPlan();
841
-
842
- console.log(plan);
843
- // Shows the Elasticsearch query structure
844
- ```
845
-
846
- This is equivalent to:
256
+ import { ROUTING_KEY_ORDER_CREATED, ROUTING_KEY_ORDER_UPDATED } from '@d19n/youfibre-odin-sdk/entities';
257
+ import { ROUTING_KEY_ACTIVATE_ORDER } from '@d19n/youfibre-odin-sdk/actions';
847
258
 
848
- ```typescript
849
- const res = await odin.orders.search({
850
- returnQueryPlan: true,
851
- query: {
852
- entity: 'infer',
853
- type: 'and',
854
- value: [
855
- { columnName: 'Status', operator: 'in', value: ['Active', 'Pending'] },
856
- { columnName: 'UpdatedAt', operator: 'gte', value: '2025-01-01' },
857
- ],
858
- returnProperties: ['id', 'title', 'properties.*'],
859
- sort: { createdAt: { order: 'asc' } },
860
- pageSize: 100,
861
- pageNumber: 0,
862
- },
259
+ // Entity events
260
+ await channel.consume(ROUTING_KEY_ORDER_CREATED, (msg) => {
261
+ const event: OrderCreatedEvent = JSON.parse(msg.content.toString());
262
+ // Handle event
863
263
  });
864
- ```
865
-
866
- ---
867
-
868
- ## Linking Records
869
-
870
- ### Queue Links (Sent with Next Action)
871
-
872
- ```typescript
873
- const order = await odin.orders.get(orderId);
874
- const contact = await odin.contacts.get(contactId);
875
- const address = await odin.addresses.get(addressId);
876
-
877
- // Queue multiple links
878
- order
879
- .link(contact)
880
- .link(address);
881
-
882
- // Links are sent with the next action
883
- await order.updateOrder({ Notes: 'Linked contact and address' });
884
-
885
- // Check pending links
886
- console.log(order.pendingLinks); // OdinRecordLinkDto[]
887
-
888
- // Clear without executing
889
- order.clearPendingLinks();
890
- ```
891
-
892
- ### Immediate Unlink
893
264
 
894
- ```typescript
895
- // Unlink executes immediately (not queued)
896
- await order.unlink(contact);
897
- await order.unlink(contactId); // Can pass ID string directly
265
+ // Action events
266
+ await channel.consume(ROUTING_KEY_ACTIVATE_ORDER, (msg) => {
267
+ const event: ActivateOrderMessage = JSON.parse(msg.content.toString());
268
+ // Handle action completion
269
+ });
898
270
  ```
899
271
 
900
- ---
901
-
902
272
  ## Migration Guide
903
273
 
904
- ### From Legacy API SDK (v1)
905
-
906
- #### Before (Legacy)
274
+ ### From Legacy Entity APIs
907
275
 
908
276
  ```typescript
909
- import { OrderApi } from '@youfibre/odin-sdk/api-sdk-v2';
910
-
911
- const api = new OrderApi({
912
- host: 'api.youfibre.com',
913
- clientId,
914
- clientSecret,
915
- });
916
-
917
- // Get record
918
- const order = await api.getByPrimaryKey(orderId);
919
-
920
- // Execute action
921
- await api.applyAction({
922
- id: order.id,
923
- entity: order.entity,
924
- type: order.type,
925
- actionKey: 'ActivateOrder',
926
- properties: { ActiveDate: '2025-01-15' },
927
- });
928
- ```
929
-
930
- #### After (New SDK)
931
-
932
- ```typescript
933
- import { OdinApiClient } from '@youfibre/odin-sdk';
934
-
935
- const odin = new OdinApiClient({
936
- host: 'api.youfibre.com',
937
- clientId,
938
- clientSecret,
939
- });
277
+ // Old (deprecated)
278
+ const orderApi = new OrderApi(authParams);
279
+ const order = await orderApi.get(orderId);
280
+ await orderApi.applyAction(new ActivateOrderDto(order, { ... }));
940
281
 
941
- // Get record
282
+ // New (recommended)
942
283
  const order = await odin.orders.get(orderId);
943
-
944
- // Execute action (typed!)
945
- await order.activateOrder({ ActiveDate: '2025-01-15' });
284
+ await order.activateOrder({ ... });
946
285
  ```
947
286
 
948
- ### From Legacy DB SDK (v1)
949
-
950
- #### Before (Legacy)
287
+ ### STEP_FLOW Actions
951
288
 
952
289
  ```typescript
953
- import { OrderDb } from '@youfibre/odin-sdk/db-sdk-v2';
954
-
955
- const db = new OrderDb(principal, dbService);
956
-
957
- // Get record
958
- const order = await db.getByPrimaryKey(orderId);
959
-
960
- // Execute action
961
- await db.applyAction({
962
- id: order.id,
963
- entity: order.entity,
964
- type: order.type,
965
- actionKey: 'ActivateOrder',
966
- properties: { ActiveDate: '2025-01-15' },
290
+ // Old (manual DTO construction)
291
+ const dto = new MoveCboToApprovedDto(record, {
292
+ journeyId: order.id,
293
+ steps: [
294
+ { actionName: "ApproveCBO", id: cbo.id, properties: { Amount: "100" } }
295
+ ]
967
296
  });
968
- ```
969
-
970
- #### After (New SDK)
971
-
972
- ```typescript
973
- import { OdinDbClient } from '@youfibre/odin-sdk';
974
-
975
- const odin = new OdinDbClient(principal, dbService);
976
-
977
- // Get record
978
- const order = await odin.orders.get(orderId);
979
-
980
- // Execute action (typed!)
981
- await order.activateOrder({ ActiveDate: '2025-01-15' });
982
- ```
983
-
984
- ### Key Differences
985
-
986
- | Feature | Legacy SDK | New SDK |
987
- |---------|-----------|---------|
988
- | Action execution | `api.applyAction({ actionKey, properties })` | `record.activateOrder(properties)` |
989
- | Type safety | Partial | Full |
990
- | Record properties | `record.properties.Status` (any) | `record.properties.Status` (typed) |
991
- | Action properties | Manual interface lookup | Auto-complete in IDE |
992
- | Linking | Separate API calls | `record.link(target)` fluent API |
993
- | Multi-entity client | Import each class separately | Single `OdinApiClient` / `OdinDbClient` |
994
-
995
- ### Migration Steps
996
-
997
- 1. **Update imports:**
998
- ```typescript
999
- // Before
1000
- import { OrderApi, ContactApi } from '@youfibre/odin-sdk/api-sdk-v2';
1001
-
1002
- // After
1003
- import { OdinApiClient } from '@youfibre/odin-sdk';
1004
- ```
1005
-
1006
- 2. **Replace class instantiation:**
1007
- ```typescript
1008
- // Before
1009
- const orderApi = new OrderApi(authParams);
1010
- const contactApi = new ContactApi(authParams);
1011
-
1012
- // After
1013
- const odin = new OdinApiClient(authParams);
1014
- // Access via odin.orders, odin.contacts
1015
- ```
1016
-
1017
- 3. **Update get calls:**
1018
- ```typescript
1019
- // Before
1020
- const order = await orderApi.getByPrimaryKey(id);
1021
-
1022
- // After
1023
- const order = await odin.orders.get(id);
1024
- ```
1025
-
1026
- 4. **Update action calls:**
1027
- ```typescript
1028
- // Before
1029
- await orderApi.applyAction({
1030
- id: order.id,
1031
- entity: order.entity,
1032
- type: order.type,
1033
- actionKey: 'ActivateOrder',
1034
- properties: { ActiveDate: date },
1035
- });
1036
-
1037
- // After
1038
- await order.activateOrder({ ActiveDate: date });
1039
- ```
1040
-
1041
- 5. **Update search calls:**
1042
- ```typescript
1043
- // Before
1044
- const result = await orderApi.searchRecords({ query: { ... } });
1045
-
1046
- // After
1047
- const result = await odin.orders.search({ query: { ... } });
1048
- ```
1049
-
1050
- ---
1051
-
1052
- ## Architecture
1053
-
1054
- ```
1055
- @youfibre/odin-sdk/
1056
- ├── OdinApiClient.ts # Unified API client
1057
- ├── OdinDbClient.ts # Unified DB client
1058
- ├── EntityRegistry.ts # Dynamic entity access
1059
- ├── index.ts # Barrel exports
1060
-
1061
- ├── accessors-v2/ # API entity accessors
1062
- │ ├── OrderAccessor.ts
1063
- │ ├── ContactAccessor.ts
1064
- │ └── ...
1065
-
1066
- ├── db-accessors-v2/ # DB entity accessors
1067
- │ ├── OrderDbAccessor.ts
1068
- │ ├── ContactDbAccessor.ts
1069
- │ └── ...
1070
-
1071
- ├── records-v2/ # Typed record wrappers
1072
- │ ├── OrderRecord.ts
1073
- │ ├── ContactRecord.ts
1074
- │ └── ...
1075
-
1076
- ├── entities-v2/ # Property interfaces
1077
- │ ├── Order.ts # OrderProperties
1078
- │ ├── Contact.ts # ContactProperties
1079
- │ └── ...
1080
-
1081
- ├── actions-v2/ # Action DTOs
1082
- │ ├── ActivateOrderDto.ts
1083
- │ ├── UpdateContactDto.ts
1084
- │ └── ...
1085
-
1086
- └── [deprecated]
1087
- ├── api-sdk-v2/ # Legacy API classes
1088
- └── db-sdk-v2/ # Legacy DB classes
1089
- ```
1090
-
1091
- ### Dependency Flow
1092
-
1093
- ```
1094
- OdinApiClient / OdinDbClient
1095
-
1096
-
1097
- *Accessor classes (accessors-v2/, db-accessors-v2/)
1098
-
1099
-
1100
- *Record wrappers (records-v2/)
1101
-
1102
- ├──▶ *Properties interfaces (entities-v2/)
1103
-
1104
- └──▶ Action methods → *Action DTOs (actions-v2/)
1105
- ```
1106
-
1107
- ---
1108
-
1109
- ## Advanced Usage
1110
-
1111
- ### Dynamic Entity Access
1112
-
1113
- ```typescript
1114
- import { EntityRegistry, getAccessor } from '@youfibre/odin-sdk';
1115
-
1116
- // Get accessor dynamically by entity name
1117
- const accessor = getAccessor('Order', provider);
1118
- const order = await accessor.get(orderId);
1119
-
1120
- // List available entities
1121
- const entities = Object.keys(EntityRegistry);
1122
- ```
297
+ await api.applyAction(dto);
1123
298
 
1124
- ### Direct Provider Access
1125
-
1126
- ```typescript
1127
- const odin = new OdinApiClient(config);
1128
-
1129
- // Access underlying provider for advanced operations
1130
- const provider = odin.provider;
1131
-
1132
- // Manual authentication (usually not needed)
1133
- await odin.authenticate();
1134
-
1135
- // Get current user
1136
- const principal = await odin.getPrincipal();
1137
- ```
1138
-
1139
- ### Type Imports
1140
-
1141
- ```typescript
1142
- // Import property types directly
1143
- import { OrderProperties } from '@youfibre/odin-sdk/entities-v2/Order';
1144
- import { ContactProperties } from '@youfibre/odin-sdk/entities-v2/Contact';
1145
-
1146
- // Import action property types
1147
- import { ActivateOrderProperties } from '@youfibre/odin-sdk/actions-v2/ActivateOrderDto';
1148
-
1149
- // Import record types
1150
- import { OrderRecord } from '@youfibre/odin-sdk/records-v2/OrderRecord';
1151
- ```
1152
-
1153
- ---
1154
-
1155
- ## Troubleshooting
1156
-
1157
- ### "Cannot find module" Errors
1158
-
1159
- Ensure you're importing from the correct paths:
1160
-
1161
- ```typescript
1162
- // ✅ Correct
1163
- import { OdinApiClient } from '@youfibre/odin-sdk';
1164
-
1165
- // ❌ Wrong
1166
- import { OdinApiClient } from '@youfibre/odin-sdk/dist/src/OdinApiClient';
1167
- ```
1168
-
1169
- ### Type Errors on Properties
1170
-
1171
- If TypeScript doesn't recognize properties, ensure your SDK is up to date:
1172
-
1173
- ```bash
1174
- npm update @youfibre/odin-sdk
299
+ // New (fluent FlowBuilder)
300
+ const cboRec = await odin.contractBuyOuts.get(cbo.id);
301
+ await cboRec
302
+ .moveCboToApproved({ journeyId: order.id })
303
+ .approveCbo(cbo.id, { Amount: "100" })
304
+ .execute();
1175
305
  ```
1176
306
 
1177
- ### Action Not Found
307
+ ### Key Changes
1178
308
 
1179
- If an action method doesn't exist on a record, it means the action isn't registered for that entity in the schema. Check the ODIN admin console.
309
+ | Old Pattern | New Pattern |
310
+ |-------------|-------------|
311
+ | `new OrderApi(auth).get(id)` | `odin.orders.get(id)` |
312
+ | `api.applyAction(dto)` | `record.actionName(props)` |
313
+ | `new ActionDto(record, props)` | `record.actionName(props)` |
314
+ | `api.address.list(recordId)` | `record.addresses.list()` |
315
+ | Manual pagination | `odin.orders.paginate(query)` |
316
+ | Manual step flow DTOs | `record.flowAction().step().execute()` |
1180
317
 
1181
318
  ---
1182
319
 
1183
- ## Contributing
1184
-
1185
- To regenerate the SDK after schema changes:
1186
-
1187
- ```bash
1188
- cd packages/youfibre-odin-sdk
1189
- npx ts-node src/runner.ts
1190
- npm run build
1191
- ```
1192
-
1193
- ---
1194
-
1195
- ## License
1196
-
1197
- Proprietary - YouFibre Ltd.
320
+ *This documentation was auto-generated from ODIN schema definitions.*