industrial-model 0.1.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 ADDED
@@ -0,0 +1,693 @@
1
+ # industrial-model
2
+
3
+ TypeScript SDK for querying [Cognite Flexible Data Models (FDM)](https://docs.cognite.com/cdf/data_modeling/) with a type-safe, graph-aware API.
4
+
5
+ ## Features
6
+
7
+ - **Type-safe queries** — define your data model types once, get compile-time validation on filters, selects, and sorts
8
+ - **Relation traversal** — query nested relations (edges/nodes) up to 3 levels deep with automatic pagination
9
+ - **Dual CJS/ESM** — works in Node.js and bundlers out of the box
10
+ - **Cursor-based pagination** — built-in support for iterating large result sets
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install industrial-model
16
+ ```
17
+
18
+ `@cognite/sdk` is a peer dependency and must be installed separately:
19
+
20
+ ```bash
21
+ npm install @cognite/sdk
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ```ts
27
+ import { CogniteClient } from "@cognite/sdk";
28
+ import { IndustrialModel } from "industrial-model";
29
+
30
+ const client = new CogniteClient({
31
+ appId: "my-app",
32
+ project: "my-project",
33
+ baseUrl: "https://az-eastus-1.cognitedata.com",
34
+ oidcTokenProvider: async () => getAccessToken(),
35
+ });
36
+
37
+ const model = new IndustrialModel(client, {
38
+ space: "cdf_cdm",
39
+ externalId: "CogniteCore",
40
+ version: "v1",
41
+ });
42
+
43
+ const { items } = await model.query({
44
+ viewExternalId: "CogniteAsset",
45
+ select: { name: true, description: true },
46
+ filters: { name: { prefix: "Pump" } },
47
+ limit: 10,
48
+ });
49
+ ```
50
+
51
+ ## Examples
52
+
53
+ | Topic | Section |
54
+ |-------|---------|
55
+ | Setup & types | [Shared type definitions](#shared-type-definitions) |
56
+ | Basic queries | [Query assets](#query-assets), [Single asset](#query-a-single-asset-by-externalid) |
57
+ | Relations | [Parent/root](#query-assets-with-parent-and-root-relations), [Path](#query-assets-with-their-full-path), [Children](#query-child-assets-reverse-relation), [Edges](#traverse-edge-relations-360-images-on-3d-objects) |
58
+ | Filters | [AND/OR/NOT](#combine-filters-with-and--or--not), [Nested](#filter-on-related-nodes), [Tags](#filter-assets-by-tags), [Batch IDs](#filter-by-multiple-external-ids) |
59
+ | Select & sort | [Select all scalars](#select-all-scalar-fields), [Multi-field sort](#sort-by-multiple-fields) |
60
+ | Pagination | [Manual cursor loop](#paginate-through-all-assets), [Fetch all pages](#fetch-all-pages-in-one-call) |
61
+ | Advanced | [Custom data model](#use-a-custom-data-model), [Full query](#full-example-assets-equipment-and-filters) |
62
+
63
+ All examples below use the [Cognite Core Data Model](https://docs.cognite.com/cdf/data_modeling/reference_data_models/cognite_core/), space `cdf_cdm`, version `v1`.
64
+
65
+ ### Shared type definitions
66
+
67
+ ```ts
68
+ import type { NodeId } from "industrial-model";
69
+
70
+ type CogniteAsset = {
71
+ name: string;
72
+ description: string;
73
+ tags: string[];
74
+ aliases: string[];
75
+ sourceId: string;
76
+ sourceCreatedTime: string;
77
+ sourceUpdatedTime: string;
78
+ parent?: NodeId;
79
+ root?: NodeId;
80
+ path: NodeId[];
81
+ assetClass?: NodeId;
82
+ type?: NodeId;
83
+ source?: NodeId;
84
+ };
85
+
86
+ type CogniteAssetRelations = {
87
+ parent: CogniteAsset;
88
+ root: CogniteAsset;
89
+ path: CogniteAsset[];
90
+ };
91
+
92
+ type CogniteEquipment = {
93
+ name: string;
94
+ description: string;
95
+ manufacturer: string;
96
+ serialNumber: string;
97
+ tags: string[];
98
+ asset?: NodeId;
99
+ equipmentType?: NodeId;
100
+ source?: NodeId;
101
+ };
102
+
103
+ type CogniteTimeSeries = {
104
+ name: string;
105
+ description: string;
106
+ isStep: boolean;
107
+ sourceUnit: string;
108
+ unit?: NodeId;
109
+ assets: NodeId[];
110
+ equipment: NodeId[];
111
+ };
112
+
113
+ type CogniteActivity = {
114
+ name: string;
115
+ description: string;
116
+ startTime: string;
117
+ endTime: string;
118
+ scheduledStartTime: string;
119
+ scheduledEndTime: string;
120
+ assets: NodeId[];
121
+ equipment: NodeId[];
122
+ timeSeries: NodeId[];
123
+ };
124
+
125
+ type CogniteUnit = {
126
+ name: string;
127
+ symbol: string;
128
+ quantity: string;
129
+ source: string;
130
+ };
131
+
132
+ type CogniteUnitRelations = {
133
+ unit: CogniteUnit;
134
+ };
135
+
136
+ type Cognite3DObject = {
137
+ name: string;
138
+ description: string;
139
+ };
140
+
141
+ type Cognite360ImageRelations = {
142
+ images360: { takenAt: string };
143
+ };
144
+ ```
145
+
146
+ ---
147
+
148
+ ### Query assets
149
+
150
+ Fetch the first 100 assets whose name starts with `"Pump"`, sorted alphabetically.
151
+
152
+ ```ts
153
+ const { items, cursor } = await model.query<CogniteAsset>({
154
+ viewExternalId: "CogniteAsset",
155
+ select: {
156
+ name: true,
157
+ description: true,
158
+ tags: true,
159
+ sourceId: true,
160
+ },
161
+ filters: {
162
+ name: { prefix: "Pump" },
163
+ },
164
+ sortClauses: { name: "ascending" },
165
+ limit: 100,
166
+ });
167
+ ```
168
+
169
+ ---
170
+
171
+ ### Query a single asset by externalId
172
+
173
+ ```ts
174
+ const { items } = await model.query<CogniteAsset>({
175
+ viewExternalId: "CogniteAsset",
176
+ select: { name: true, description: true, tags: true },
177
+ filters: {
178
+ externalId: { eq: "WMT:VAL" },
179
+ },
180
+ });
181
+
182
+ const asset = items[0];
183
+ ```
184
+
185
+ ---
186
+
187
+ ### Query assets with parent and root relations
188
+
189
+ Traverse up the asset hierarchy — fetch each asset alongside its direct parent and the root of the tree.
190
+
191
+ ```ts
192
+ const { items } = await model.query<CogniteAsset, CogniteAssetRelations>({
193
+ viewExternalId: "CogniteAsset",
194
+ select: {
195
+ name: true,
196
+ description: true,
197
+ parent: {
198
+ name: true,
199
+ description: true,
200
+ parent: {
201
+ name: true,
202
+ },
203
+ },
204
+ root: {
205
+ name: true,
206
+ },
207
+ },
208
+ filters: {
209
+ name: { prefix: "Pump" },
210
+ },
211
+ limit: 50,
212
+ });
213
+ ```
214
+
215
+ ---
216
+
217
+ ### Query assets with their full path
218
+
219
+ The `path` property is a list of `NodeId` references representing the ancestor chain. Use it to reconstruct breadcrumbs.
220
+
221
+ ```ts
222
+ const { items } = await model.query<CogniteAsset, CogniteAssetRelations>({
223
+ viewExternalId: "CogniteAsset",
224
+ select: {
225
+ name: true,
226
+ path: {
227
+ name: true,
228
+ description: true,
229
+ },
230
+ },
231
+ filters: {
232
+ externalId: { eq: "WMT:VAL" },
233
+ },
234
+ });
235
+ ```
236
+
237
+ ---
238
+
239
+ ### Query equipment linked to an asset
240
+
241
+ ```ts
242
+ const { items } = await model.query<CogniteEquipment>({
243
+ viewExternalId: "CogniteEquipment",
244
+ select: {
245
+ name: true,
246
+ manufacturer: true,
247
+ serialNumber: true,
248
+ tags: true,
249
+ asset: true,
250
+ },
251
+ filters: {
252
+ asset: { eq: { space: "my-space", externalId: "WMT:VAL" } },
253
+ manufacturer: { exists: true },
254
+ },
255
+ sortClauses: { name: "ascending" },
256
+ limit: 50,
257
+ });
258
+ ```
259
+
260
+ ---
261
+
262
+ ### Query time series with their unit
263
+
264
+ ```ts
265
+ const { items } = await model.query<CogniteTimeSeries, CogniteUnitRelations>({
266
+ viewExternalId: "CogniteTimeSeries",
267
+ select: {
268
+ name: true,
269
+ description: true,
270
+ isStep: true,
271
+ sourceUnit: true,
272
+ unit: {
273
+ name: true,
274
+ symbol: true,
275
+ quantity: true,
276
+ },
277
+ },
278
+ filters: {
279
+ isStep: { eq: false },
280
+ sourceUnit: { exists: true },
281
+ },
282
+ limit: 200,
283
+ });
284
+ ```
285
+
286
+ ---
287
+
288
+ ### Query activities in a time window
289
+
290
+ ```ts
291
+ const { items } = await model.query<CogniteActivity>({
292
+ viewExternalId: "CogniteActivity",
293
+ select: {
294
+ name: true,
295
+ description: true,
296
+ startTime: true,
297
+ endTime: true,
298
+ scheduledStartTime: true,
299
+ scheduledEndTime: true,
300
+ },
301
+ filters: {
302
+ startTime: { gte: "2024-01-01T00:00:00Z", lte: "2024-12-31T23:59:59Z" },
303
+ },
304
+ sortClauses: { startTime: "ascending" },
305
+ limit: 500,
306
+ });
307
+ ```
308
+
309
+ ---
310
+
311
+ ### Combine filters with AND / OR / NOT
312
+
313
+ Fetch assets that are either tagged `"critical"` or have a name starting with `"Compressor"`, but exclude those from source `"legacy-system"`.
314
+
315
+ ```ts
316
+ const { items } = await model.query<CogniteAsset>({
317
+ viewExternalId: "CogniteAsset",
318
+ select: { name: true, tags: true, sourceId: true },
319
+ filters: {
320
+ OR: [
321
+ { tags: { containsAny: ["critical"] } },
322
+ { name: { prefix: "Compressor" } },
323
+ ],
324
+ NOT: { sourceId: { eq: "legacy-system" } },
325
+ },
326
+ limit: 100,
327
+ });
328
+ ```
329
+
330
+ ---
331
+
332
+ ### Paginate through all assets
333
+
334
+ ```ts
335
+ let cursor: string | null = null;
336
+ const allAssets: Record<string, unknown>[] = [];
337
+
338
+ do {
339
+ const result = await model.query<CogniteAsset>({
340
+ viewExternalId: "CogniteAsset",
341
+ select: { name: true, description: true },
342
+ limit: 1000,
343
+ cursor,
344
+ });
345
+
346
+ allAssets.push(...result.items);
347
+ cursor = result.cursor;
348
+ } while (cursor !== null);
349
+
350
+ console.log(`Total assets: ${allAssets.length}`);
351
+ ```
352
+
353
+ ---
354
+
355
+ ### Fetch all pages in one call
356
+
357
+ Pass `limit: -1` to automatically follow cursors until every page is loaded. The returned `cursor` is always `null`.
358
+
359
+ ```ts
360
+ const { items } = await model.query<CogniteAsset>({
361
+ viewExternalId: "CogniteAsset",
362
+ select: { name: true, description: true },
363
+ filters: { tags: { containsAny: ["production"] } },
364
+ limit: -1,
365
+ });
366
+
367
+ console.log(`Loaded ${items.length} assets in one request chain`);
368
+ ```
369
+
370
+ ---
371
+
372
+ ### Select all scalar fields
373
+
374
+ Use `_all` to include every scalar property on the view without listing them individually. Relation fields are returned as `NodeId` references but are not expanded — add nested `select` blocks when you need related data.
375
+
376
+ ```ts
377
+ const { items } = await model.query<CogniteAsset>({
378
+ viewExternalId: "CogniteAsset",
379
+ select: { _all: true },
380
+ limit: 50,
381
+ });
382
+
383
+ // items[0] includes name, description, tags, parent (as NodeId), etc.
384
+ ```
385
+
386
+ Combine `_all` with explicit relation expansion:
387
+
388
+ ```ts
389
+ const { items } = await model.query<CogniteAsset, CogniteAssetRelations>({
390
+ viewExternalId: "CogniteAsset",
391
+ select: {
392
+ _all: true,
393
+ parent: { name: true },
394
+ },
395
+ limit: 25,
396
+ });
397
+ ```
398
+
399
+ ---
400
+
401
+ ### Filter by multiple external IDs
402
+
403
+ ```ts
404
+ const { items } = await model.query<CogniteAsset>({
405
+ viewExternalId: "CogniteAsset",
406
+ select: { name: true, description: true },
407
+ filters: {
408
+ externalId: {
409
+ in: ["WMT:VAL", "WMT:PUMP-01", "WMT:PUMP-02"],
410
+ },
411
+ },
412
+ });
413
+ ```
414
+
415
+ ---
416
+
417
+ ### Filter assets by tags
418
+
419
+ ```ts
420
+ // Match assets that have at least one of these tags
421
+ const critical = await model.query<CogniteAsset>({
422
+ viewExternalId: "CogniteAsset",
423
+ select: { name: true, tags: true },
424
+ filters: { tags: { containsAny: ["critical", "safety"] } },
425
+ limit: 100,
426
+ });
427
+
428
+ // Match assets that must have every tag
429
+ const fullyTagged = await model.query<CogniteAsset>({
430
+ viewExternalId: "CogniteAsset",
431
+ select: { name: true, tags: true },
432
+ filters: { tags: { containsAll: ["production", "verified"] } },
433
+ limit: 100,
434
+ });
435
+ ```
436
+
437
+ ---
438
+
439
+ ### Filter on related nodes
440
+
441
+ Filter the root view based on properties of a direct or nested relation. This uses Cognite nested filters under the hood.
442
+
443
+ ```ts
444
+ // Assets whose parent is named "Site Root"
445
+ const { items } = await model.query<CogniteAsset>({
446
+ viewExternalId: "CogniteAsset",
447
+ select: { name: true, parent: { name: true } },
448
+ filters: {
449
+ parent: { name: { eq: "Site Root" } },
450
+ },
451
+ limit: 50,
452
+ });
453
+
454
+ // Assets whose parent's asset class code is "PUMP"
455
+ const pumpsByClass = await model.query<CogniteAsset>({
456
+ viewExternalId: "CogniteAsset",
457
+ select: {
458
+ name: true,
459
+ parent: { assetClass: { name: true, code: true } },
460
+ },
461
+ filters: {
462
+ parent: { assetClass: { code: { eq: "PUMP" } } },
463
+ },
464
+ limit: 50,
465
+ });
466
+
467
+ // Combine root and nested conditions
468
+ const filtered = await model.query<CogniteAsset>({
469
+ viewExternalId: "CogniteAsset",
470
+ select: { name: true, parent: { name: true } },
471
+ filters: {
472
+ AND: [
473
+ { name: { prefix: "Pump" } },
474
+ { parent: { name: { exists: true } } },
475
+ ],
476
+ },
477
+ limit: 100,
478
+ });
479
+ ```
480
+
481
+ ---
482
+
483
+ ### Query child assets (reverse relation)
484
+
485
+ Declare reverse relations in the second generic parameter (`TRelation`). The library resolves the correct traversal direction from your data model.
486
+
487
+ ```ts
488
+ type AssetWithChildren = CogniteAsset & {
489
+ children?: CogniteAsset[];
490
+ };
491
+
492
+ const { items } = await model.query<AssetWithChildren, { children: CogniteAsset }>({
493
+ viewExternalId: "CogniteAsset",
494
+ select: {
495
+ name: true,
496
+ children: {
497
+ name: true,
498
+ description: true,
499
+ },
500
+ },
501
+ filters: {
502
+ externalId: { eq: "WMT:VAL" },
503
+ },
504
+ });
505
+ ```
506
+
507
+ ---
508
+
509
+ ### Traverse edge relations (360 images on 3D objects)
510
+
511
+ Some relations are modeled as edges rather than direct node links. Select them the same way — the SDK builds the edge hop automatically.
512
+
513
+ ```ts
514
+ const { items } = await model.query<Cognite3DObject, Cognite360ImageRelations>({
515
+ viewExternalId: "Cognite3DObject",
516
+ select: {
517
+ name: true,
518
+ images360: { takenAt: true },
519
+ },
520
+ filters: {
521
+ name: { prefix: "Tank" },
522
+ },
523
+ limit: 20,
524
+ });
525
+ ```
526
+
527
+ ---
528
+
529
+ ### Sort by multiple fields
530
+
531
+ Sort clauses apply to primitive fields on the root view, including node-level properties like `externalId`.
532
+
533
+ ```ts
534
+ const { items } = await model.query<CogniteAsset>({
535
+ viewExternalId: "CogniteAsset",
536
+ select: { name: true, sourceId: true },
537
+ sortClauses: {
538
+ name: "ascending",
539
+ externalId: "descending",
540
+ },
541
+ limit: 100,
542
+ });
543
+ ```
544
+
545
+ ---
546
+
547
+ ### Use a custom data model
548
+
549
+ Point the client at any FDM in your project — not only Cognite Core. Views and filters work the same way once your TypeScript types match the model.
550
+
551
+ ```ts
552
+ const model = new IndustrialModel(client, {
553
+ space: "my-custom-space",
554
+ externalId: "MyPlantModel",
555
+ version: "1",
556
+ });
557
+
558
+ type PlantArea = {
559
+ name: string;
560
+ code: string;
561
+ site?: NodeId;
562
+ };
563
+
564
+ const { items } = await model.query<PlantArea>({
565
+ viewExternalId: "PlantArea",
566
+ select: { name: true, code: true, site: true },
567
+ filters: { code: { prefix: "AREA-" } },
568
+ limit: 200,
569
+ });
570
+ ```
571
+
572
+ ---
573
+
574
+ ### Full example: assets, equipment, and filters
575
+
576
+ A single query combining nested selects, nested filters, sorting, and pagination.
577
+
578
+ ```ts
579
+ type AssetWithRelations = CogniteAsset & {
580
+ parent?: CogniteAsset & { assetClass?: { name: string; code: string } };
581
+ };
582
+
583
+ const { items, cursor } = await model.query<AssetWithRelations, CogniteAssetRelations>({
584
+ viewExternalId: "CogniteAsset",
585
+ select: {
586
+ name: true,
587
+ description: true,
588
+ tags: true,
589
+ parent: {
590
+ name: true,
591
+ assetClass: { name: true, code: true },
592
+ },
593
+ },
594
+ filters: {
595
+ name: { prefix: "WMT" },
596
+ parent: { name: { exists: true } },
597
+ OR: [
598
+ { tags: { containsAny: ["critical"] } },
599
+ { sourceId: { eq: "sap" } },
600
+ ],
601
+ },
602
+ sortClauses: { name: "ascending" },
603
+ limit: 25,
604
+ cursor: null,
605
+ });
606
+
607
+ // Follow-up page
608
+ if (cursor) {
609
+ const next = await model.query<AssetWithRelations, CogniteAssetRelations>({
610
+ viewExternalId: "CogniteAsset",
611
+ select: {
612
+ name: true,
613
+ description: true,
614
+ tags: true,
615
+ parent: { name: true, assetClass: { name: true, code: true } },
616
+ },
617
+ filters: {
618
+ name: { prefix: "WMT" },
619
+ parent: { name: { exists: true } },
620
+ },
621
+ sortClauses: { name: "ascending" },
622
+ limit: 25,
623
+ cursor,
624
+ });
625
+ }
626
+ ```
627
+
628
+ ---
629
+
630
+ ## API
631
+
632
+ ### `new IndustrialModel(client, dataModelId)`
633
+
634
+ | Parameter | Type | Description |
635
+ |-----------|------|-------------|
636
+ | `client` | `CogniteClient` | Authenticated Cognite SDK client |
637
+ | `dataModelId` | `DataModelId` | Space, externalId, and version of the data model |
638
+
639
+ ### `model.query<T, TRelation?>(options)`
640
+
641
+ | Option | Type | Description |
642
+ |--------|------|-------------|
643
+ | `viewExternalId` | `string` | The view to query |
644
+ | `select` | `QuerySelect<T, TRelation>` | Fields to include; use `_all: true` for all scalars |
645
+ | `filters` | `WhereInput<T, TRelation>` | Filter conditions (supports nested relation filters) |
646
+ | `sortClauses` | `SortInput<T>` | Sort by primitive fields |
647
+ | `limit` | `number` | Max items per page (default 1000, max 10000). Use `-1` to fetch all pages |
648
+ | `cursor` | `string \| null` | Pagination cursor from a previous response |
649
+
650
+ Returns `Promise<QueryResult>`:
651
+
652
+ ```ts
653
+ type QueryResult = {
654
+ items: Record<string, unknown>[];
655
+ cursor: string | null; // null when no more pages
656
+ };
657
+ ```
658
+
659
+ ### Filter operators
660
+
661
+ | Type | Operators |
662
+ |------|-----------|
663
+ | `string` | `eq`, `in`, `prefix`, `exists` |
664
+ | `number` | `eq`, `in`, `gt`, `gte`, `lt`, `lte`, `exists` |
665
+ | `boolean` | `eq`, `exists` |
666
+ | `Date` | `eq`, `in`, `gt`, `gte`, `lt`, `lte`, `exists` |
667
+ | `NodeId` | `eq`, `in`, `exists` |
668
+ | `T[]` | `containsAny`, `containsAll`, `exists` |
669
+
670
+ Logical combinators `AND`, `OR`, and `NOT` are supported at any nesting level, including inside nested relation filters (e.g. `parent: { OR: [...] }`).
671
+
672
+ ### Relation traversal
673
+
674
+ - **Direct relations** — `parent`, `asset`, `unit` (outwards from the current node)
675
+ - **Reverse relations** — declare in `TRelation` (e.g. `children` on `CogniteAsset`)
676
+ - **Edge relations** — declare in `TRelation` (e.g. `images360` on `Cognite3DObject`)
677
+ - **Depth** — nested selects and filters up to 3 levels; dependency pages are fetched automatically
678
+
679
+ ## Releasing
680
+
681
+ This project uses [Changesets](https://github.com/changesets/changesets) to manage versions and changelogs.
682
+
683
+ When you change something user-facing, add a changeset:
684
+
685
+ ```bash
686
+ npx changeset
687
+ ```
688
+
689
+ Commit the generated file under `.changeset/` with your PR. After merge to `main`, the release workflow opens a "Version packages" PR. Merging that PR publishes to npm.
690
+
691
+ ## License
692
+
693
+ MIT