industrial-model 0.5.0 → 0.7.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
@@ -2,32 +2,37 @@
2
2
 
3
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
4
 
5
- ## Features
5
+ `industrial-model` is designed for application code that needs to move through industrial data as a model, not as loosely typed query payloads. Start with a Cognite data model, describe the view shape in TypeScript, and query nodes, relations, filters, sorting, pagination, and aggregations with compiler support.
6
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
- - **Text search filters** use Cognite `instances.search` from query and aggregate filters on text fields
10
- - **Dual CJS/ESM** works in Node.js and bundlers out of the box
11
- - **Cursor-based pagination** — built-in support for iterating large result sets
7
+ ## What You Get
8
+
9
+ - **Typed model queries** - validate selected fields, filters, and sort keys at compile time.
10
+ - **Precise result types** - returned items follow the `select` tree, including nested relation selections.
11
+ - **Graph traversal** - expand direct, reverse, and edge relations up to 3 levels deep.
12
+ - **Industrial filters** - combine scalar filters, list filters, full-text search, and nested relation filters.
13
+ - **Pagination support** - use cursors manually or fetch all root pages with `limit: -1`.
14
+ - **Aggregation support** - count, group, list distinct values, and aggregate numeric properties.
15
+ - **Mutation support** - upsert model-shaped node patches and delete nodes by identity.
16
+ - **Runtime validation option** - parse query results with Zod schemas derived from Cognite view metadata.
17
+ - **CJS and ESM builds** - works in Node.js and common bundler setups.
12
18
 
13
19
  ## Installation
14
20
 
15
21
  ```bash
16
22
  npm install industrial-model
17
- ```
18
-
19
- `@cognite/sdk` is a peer dependency and must be installed separately:
20
-
21
- ```bash
22
23
  npm install @cognite/sdk
23
24
  ```
24
25
 
26
+ `@cognite/sdk` is a peer dependency and must be installed by your application.
27
+
25
28
  ## Requirements
26
29
 
27
30
  - Node.js `>=20`
28
- - `@cognite/sdk` `^10.10.0` (peer dependency)
31
+ - `@cognite/sdk` `^10.10.0`
29
32
 
30
- ## Quick start
33
+ ## First Query
34
+
35
+ Create a Cognite SDK client, point `IndustrialModelClient` at a data model, and query a view.
31
36
 
32
37
  ```ts
33
38
  import { CogniteClient } from "@cognite/sdk";
@@ -48,58 +53,55 @@ const model = new IndustrialModelClient(client, {
48
53
 
49
54
  const { items } = await model.query<{ name: string; description: string }>()({
50
55
  viewExternalId: "CogniteAsset",
51
- select: { name: true, description: true },
52
- filters: { name: { prefix: "Pump" } },
56
+ select: {
57
+ name: true,
58
+ description: true,
59
+ },
60
+ filters: {
61
+ name: { prefix: "Pump" },
62
+ },
63
+ sort: {
64
+ name: "ascending",
65
+ },
53
66
  limit: 10,
54
67
  });
68
+
69
+ items[0]?.name;
55
70
  ```
56
71
 
57
- ## Examples
72
+ This is the basic contract:
58
73
 
59
- | Topic | Section |
60
- |-------|---------|
61
- | Setup & types | [Shared type definitions](#shared-type-definitions) |
62
- | Basic queries | [Query assets](#query-assets), [Single asset](#query-a-single-asset-by-externalid) |
63
- | 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) |
64
- | Filters | [AND/OR/NOT](#combine-filters-with-and--or--not), [Text search](#search-text-fields), [Nested](#filter-on-related-nodes), [Tags](#filter-assets-by-tags), [Batch IDs](#filter-by-multiple-external-ids) |
65
- | Select & sort | [Select all scalars](#select-all-scalar-fields), [Multi-field sort](#sort-by-multiple-fields) |
66
- | Pagination | [Manual cursor loop](#paginate-through-all-assets), [Fetch all pages](#fetch-all-pages-automatically) |
67
- | Aggregation | [Count by group](#count-assets-by-source-id), [Distinct values](#list-distinct-source-ids), [Numeric aggregates](#average-volume-by-type), [Global count](#count-all-matching-assets) |
68
- | Advanced | [Custom data model](#use-a-custom-data-model), [Full query](#full-example-assets-equipment-and-filters) |
74
+ 1. `viewExternalId` selects the Cognite view.
75
+ 2. The generic type describes the fields you want TypeScript to understand.
76
+ 3. `select` controls the returned shape.
77
+ 4. `filters`, `sort`, `limit`, and `cursor` control the query behavior.
69
78
 
70
- 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`.
79
+ ## Define A Model
71
80
 
72
- ### Shared type definitions
81
+ For scalar-only views, a plain object type is enough:
73
82
 
74
83
  ```ts
75
- import type {
76
- IndustrialModel,
77
- ModelProps,
78
- ModelRelations,
79
- NodeId,
80
- QueryResultItem,
81
- } from "industrial-model";
82
-
83
- type CogniteAssetClass = IndustrialModel<{
84
+ type CogniteAssetClass = {
84
85
  name: string;
85
86
  code: string;
86
- }>;
87
+ };
88
+ ```
89
+
90
+ When a view has expandable relations, use `IndustrialModel<TProps, TRelations>`. Put the raw view properties in `TProps`, and put expandable relation result shapes in `TRelations`.
91
+
92
+ ```ts
93
+ import type { IndustrialModel, ModelProps, ModelRelations, NodeId } from "industrial-model";
87
94
 
88
95
  type CogniteAsset = IndustrialModel<
89
96
  {
90
97
  name: string;
91
98
  description: string;
92
99
  tags: string[];
93
- aliases: string[];
94
100
  sourceId: string;
95
- sourceCreatedTime: string;
96
- sourceUpdatedTime: string;
97
101
  parent?: NodeId;
98
102
  root?: NodeId;
99
103
  path: NodeId[];
100
104
  assetClass?: NodeId;
101
- type?: NodeId;
102
- source?: NodeId;
103
105
  },
104
106
  {
105
107
  parent?: CogniteAsset;
@@ -109,77 +111,27 @@ type CogniteAsset = IndustrialModel<
109
111
  }
110
112
  >;
111
113
 
112
- type CogniteActivity = IndustrialModel<{
113
- name: string;
114
- description: string;
115
- startTime: string;
116
- endTime: string;
117
- scheduledStartTime: string;
118
- scheduledEndTime: string;
119
- assets: NodeId[];
120
- equipment: NodeId[];
121
- timeSeries: NodeId[];
122
- }>;
123
-
124
114
  type CogniteEquipment = IndustrialModel<
125
115
  {
126
116
  name: string;
127
- description: string;
128
117
  manufacturer: string;
129
118
  serialNumber: string;
130
119
  tags: string[];
131
120
  asset?: NodeId;
132
- equipmentType?: NodeId;
133
- source?: NodeId;
134
121
  },
135
122
  {
136
123
  asset?: CogniteAsset;
137
- activities?: CogniteActivity[];
138
- }
139
- >;
140
-
141
- type CogniteUnit = IndustrialModel<{
142
- name: string;
143
- symbol: string;
144
- quantity: string;
145
- source: string;
146
- }>;
147
-
148
- type CogniteTimeSeries = IndustrialModel<
149
- {
150
- name: string;
151
- description: string;
152
- isStep: boolean;
153
- sourceUnit: string;
154
- unit?: NodeId;
155
- assets: NodeId[];
156
- equipment: NodeId[];
157
- },
158
- {
159
- unit?: CogniteUnit;
160
- }
161
- >;
162
-
163
- type Cognite360Image = IndustrialModel<{
164
- takenAt: string;
165
- }>;
166
-
167
- type Cognite3DObject = IndustrialModel<
168
- {
169
- name: string;
170
- description: string;
171
- },
172
- {
173
- images360?: Cognite360Image[];
174
124
  }
175
125
  >;
176
126
  ```
177
127
 
178
- ---
128
+ The relation metadata is type-only. It lets the SDK infer nested `select` trees and nested filters while Cognite remains the source of truth for the actual view and relation definitions.
129
+
130
+ 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`.
179
131
 
180
- ### Query assets
132
+ ## Query Basics
181
133
 
182
- Fetch the first 100 assets whose name starts with `"Pump"`, sorted alphabetically.
134
+ Start with a single view, select the fields the application needs, then layer on filters and sorting.
183
135
 
184
136
  ```ts
185
137
  const { items, cursor } = await model.query<CogniteAsset>()({
@@ -193,32 +145,22 @@ const { items, cursor } = await model.query<CogniteAsset>()({
193
145
  filters: {
194
146
  name: { prefix: "Pump" },
195
147
  },
196
- sort: { name: "ascending" },
148
+ sort: {
149
+ name: "ascending",
150
+ },
197
151
  limit: 100,
198
152
  });
199
153
  ```
200
154
 
201
- ---
202
-
203
- ### Query a single asset by externalId
155
+ The returned item type follows the selection:
204
156
 
205
157
  ```ts
206
- const { items } = await model.query<CogniteAsset>()({
207
- viewExternalId: "CogniteAsset",
208
- select: { name: true, description: true, tags: true },
209
- filters: {
210
- externalId: { eq: "WMT:VAL" },
211
- },
212
- });
213
-
214
- const asset = items[0];
158
+ items[0]?.name; // string
159
+ items[0]?.description; // string
160
+ items[0]?.externalId; // instance metadata is always included
215
161
  ```
216
162
 
217
- ---
218
-
219
- ### Query assets with parent and root relations
220
-
221
- Traverse up the asset hierarchy — fetch each asset alongside its direct parent and the root of the tree.
163
+ To find one known instance, filter by `externalId`:
222
164
 
223
165
  ```ts
224
166
  const { items } = await model.query<CogniteAsset>()({
@@ -226,51 +168,31 @@ const { items } = await model.query<CogniteAsset>()({
226
168
  select: {
227
169
  name: true,
228
170
  description: true,
229
- parent: {
230
- name: true,
231
- description: true,
232
- parent: {
233
- name: true,
234
- },
235
- },
236
- root: {
237
- name: true,
238
- },
171
+ tags: true,
239
172
  },
240
173
  filters: {
241
- name: { prefix: "Pump" },
174
+ externalId: { eq: "WMT:VAL" },
242
175
  },
243
- limit: 50,
244
176
  });
245
177
 
246
- const firstParentName = items[0]?.parent?.name;
178
+ const asset = items[0];
247
179
  ```
248
180
 
249
- ---
250
-
251
- ### Query assets with their full path
252
-
253
- The `path` property is a list of `NodeId` references representing the ancestor chain. Use it to reconstruct breadcrumbs.
181
+ Use `_all` when you want every scalar property from the root view:
254
182
 
255
183
  ```ts
256
184
  const { items } = await model.query<CogniteAsset>()({
257
185
  viewExternalId: "CogniteAsset",
258
- select: {
259
- name: true,
260
- path: {
261
- name: true,
262
- description: true,
263
- },
264
- },
265
- filters: {
266
- externalId: { eq: "WMT:VAL" },
267
- },
186
+ select: { _all: true },
187
+ limit: 50,
268
188
  });
269
189
  ```
270
190
 
271
- ---
191
+ `_all` includes scalar fields and relation IDs. Add nested selections when you want relation objects instead of `NodeId` references.
192
+
193
+ ## Filters And Sorting
272
194
 
273
- ### Query equipment linked to an asset
195
+ Filters are typed from your model. String, number, boolean, date, `NodeId`, and list fields each expose the operators that make sense for that field.
274
196
 
275
197
  ```ts
276
198
  const { items } = await model.query<CogniteEquipment>()({
@@ -286,198 +208,93 @@ const { items } = await model.query<CogniteEquipment>()({
286
208
  asset: { eq: { space: "my-space", externalId: "WMT:VAL" } },
287
209
  manufacturer: { exists: true },
288
210
  },
289
- sort: { name: "ascending" },
211
+ sort: {
212
+ name: "ascending",
213
+ },
290
214
  limit: 50,
291
215
  });
292
216
  ```
293
217
 
294
- ---
295
-
296
- ### Query time series with their unit
218
+ Combine conditions with `AND`, `OR`, and `NOT`:
297
219
 
298
220
  ```ts
299
- const { items } = await model.query<CogniteTimeSeries>()({
300
- viewExternalId: "CogniteTimeSeries",
221
+ const { items } = await model.query<CogniteAsset>()({
222
+ viewExternalId: "CogniteAsset",
301
223
  select: {
302
224
  name: true,
303
- description: true,
304
- isStep: true,
305
- sourceUnit: true,
306
- unit: {
307
- name: true,
308
- symbol: true,
309
- quantity: true,
310
- },
225
+ tags: true,
226
+ sourceId: true,
311
227
  },
312
228
  filters: {
313
- isStep: { eq: false },
314
- sourceUnit: { exists: true },
229
+ OR: [
230
+ { tags: { containsAny: ["critical"] } },
231
+ { name: { prefix: "Compressor" } },
232
+ ],
233
+ NOT: {
234
+ sourceId: { eq: "legacy-system" },
235
+ },
315
236
  },
316
- limit: 200,
237
+ limit: 100,
317
238
  });
318
239
  ```
319
240
 
320
- ---
321
-
322
- ### Query activities in a time window
241
+ List fields support `containsAny` and `containsAll`:
323
242
 
324
243
  ```ts
325
- const { items } = await model.query<CogniteActivity>()({
326
- viewExternalId: "CogniteActivity",
244
+ const critical = await model.query<CogniteAsset>()({
245
+ viewExternalId: "CogniteAsset",
327
246
  select: {
328
247
  name: true,
329
- description: true,
330
- startTime: true,
331
- endTime: true,
332
- scheduledStartTime: true,
333
- scheduledEndTime: true,
248
+ tags: true,
334
249
  },
335
250
  filters: {
336
- startTime: { gte: "2024-01-01T00:00:00Z", lte: "2024-12-31T23:59:59Z" },
251
+ tags: { containsAny: ["critical", "safety"] },
337
252
  },
338
- sort: { startTime: "ascending" },
339
- limit: 500,
253
+ limit: 100,
340
254
  });
341
- ```
342
-
343
- ---
344
255
 
345
- ### Combine filters with AND / OR / NOT
346
-
347
- Fetch assets that are either tagged `"critical"` or have a name starting with `"Compressor"`, but exclude those from source `"legacy-system"`.
348
-
349
- ```ts
350
- const { items } = await model.query<CogniteAsset>()({
256
+ const fullyTagged = await model.query<CogniteAsset>()({
351
257
  viewExternalId: "CogniteAsset",
352
- select: { name: true, tags: true, sourceId: true },
258
+ select: {
259
+ name: true,
260
+ tags: true,
261
+ },
353
262
  filters: {
354
- OR: [
355
- { tags: { containsAny: ["critical"] } },
356
- { name: { prefix: "Compressor" } },
357
- ],
358
- NOT: { sourceId: { eq: "legacy-system" } },
263
+ tags: { containsAll: ["production", "verified"] },
359
264
  },
360
265
  limit: 100,
361
266
  });
362
267
  ```
363
268
 
364
- ---
365
-
366
- ### Paginate through all assets
367
-
368
- ```ts
369
- let cursor: string | null = null;
370
- const allAssets: QueryResultItem<CogniteAsset, { name: true; description: true }>[] = [];
371
-
372
- do {
373
- const result = await model.query<CogniteAsset>()({
374
- viewExternalId: "CogniteAsset",
375
- select: { name: true, description: true },
376
- limit: 1000,
377
- cursor,
378
- });
379
-
380
- allAssets.push(...result.items);
381
- cursor = result.cursor;
382
- } while (cursor !== null);
383
-
384
- console.log(`Total assets: ${allAssets.length}`);
385
- ```
386
-
387
- ---
388
-
389
- ### Fetch all pages automatically
390
-
391
- Pass `limit: -1` to follow root-view cursors until every page is loaded. The SDK issues multiple `instances.query` calls (1000 items per page by default). The returned `cursor` is always `null`.
392
-
393
- ```ts
394
- const { items } = await model.query<CogniteAsset>()({
395
- viewExternalId: "CogniteAsset",
396
- select: { name: true, description: true },
397
- filters: { tags: { containsAny: ["production"] } },
398
- limit: -1,
399
- });
400
-
401
- console.log(`Loaded ${items.length} assets across all pages`);
402
- ```
403
-
404
- ---
405
-
406
- ### Select all scalar fields
407
-
408
- 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.
409
-
410
- ```ts
411
- const { items } = await model.query<CogniteAsset>()({
412
- viewExternalId: "CogniteAsset",
413
- select: { _all: true },
414
- limit: 50,
415
- });
416
-
417
- // items[0] includes name, description, tags, parent (as NodeId), etc.
418
- ```
419
-
420
- Combine `_all` with explicit relation expansion:
269
+ Sort clauses apply to primitive fields on the root view, including node metadata such as `externalId`.
421
270
 
422
271
  ```ts
423
272
  const { items } = await model.query<CogniteAsset>()({
424
273
  viewExternalId: "CogniteAsset",
425
274
  select: {
426
- _all: true,
427
- parent: { name: true },
275
+ name: true,
276
+ sourceId: true,
428
277
  },
429
- limit: 25,
430
- });
431
- ```
432
-
433
- ---
434
-
435
- ### Filter by multiple external IDs
436
-
437
- ```ts
438
- const { items } = await model.query<CogniteAsset>()({
439
- viewExternalId: "CogniteAsset",
440
- select: { name: true, description: true },
441
- filters: {
442
- externalId: {
443
- in: ["WMT:VAL", "WMT:PUMP-01", "WMT:PUMP-02"],
444
- },
278
+ sort: {
279
+ name: "ascending",
280
+ externalId: "descending",
445
281
  },
446
- });
447
- ```
448
-
449
- ---
450
-
451
- ### Filter assets by tags
452
-
453
- ```ts
454
- // Match assets that have at least one of these tags
455
- const critical = await model.query<CogniteAsset>()({
456
- viewExternalId: "CogniteAsset",
457
- select: { name: true, tags: true },
458
- filters: { tags: { containsAny: ["critical", "safety"] } },
459
- limit: 100,
460
- });
461
-
462
- // Match assets that must have every tag
463
- const fullyTagged = await model.query<CogniteAsset>()({
464
- viewExternalId: "CogniteAsset",
465
- select: { name: true, tags: true },
466
- filters: { tags: { containsAll: ["production", "verified"] } },
467
282
  limit: 100,
468
283
  });
469
284
  ```
470
285
 
471
- ---
472
-
473
- ### Search text fields
286
+ ## Text Search
474
287
 
475
- Use `search` on text properties and string-list text properties when you want Cognite full-text matching instead of exact or prefix filters. The optional `operator` is passed to Cognite search and defaults to `"OR"`; use `"AND"` when every term should match.
288
+ Use `search` on Cognite text properties and string-list text properties when you want full-text matching instead of exact or prefix matching. The optional `operator` is passed to Cognite search and defaults to `"OR"`.
476
289
 
477
290
  ```ts
478
291
  const { items } = await model.query<CogniteAsset>()({
479
292
  viewExternalId: "CogniteAsset",
480
- select: { name: true, description: true, tags: true },
293
+ select: {
294
+ name: true,
295
+ description: true,
296
+ tags: true,
297
+ },
481
298
  filters: {
482
299
  name: { search: { query: "root pump", operator: "AND" } },
483
300
  tags: { search: { query: "critical" } },
@@ -486,12 +303,15 @@ const { items } = await model.query<CogniteAsset>()({
486
303
  });
487
304
  ```
488
305
 
489
- Search filters can be combined with normal field operators. The SDK first calls Cognite `instances.search`, then adds the returned node references to the regular query filter.
306
+ Search filters can be combined with regular operators. The SDK first calls Cognite `instances.search`, maps the matched nodes to instance references, and then applies those references to the query or aggregate request.
490
307
 
491
308
  ```ts
492
309
  const pumps = await model.query<CogniteAsset>()({
493
310
  viewExternalId: "CogniteAsset",
494
- select: { name: true, sourceId: true },
311
+ select: {
312
+ name: true,
313
+ sourceId: true,
314
+ },
495
315
  filters: {
496
316
  name: {
497
317
  prefix: "Pump",
@@ -502,72 +322,108 @@ const pumps = await model.query<CogniteAsset>()({
502
322
  });
503
323
  ```
504
324
 
505
- The same filter syntax is also supported by `aggregate`:
325
+ ## Relations
326
+
327
+ The same `select` object that selects scalar fields can expand relations. Direct relations move outward from the current node.
506
328
 
507
329
  ```ts
508
- const { items } = await model.aggregate<CogniteAsset>()({
330
+ const { items } = await model.query<CogniteAsset>()({
509
331
  viewExternalId: "CogniteAsset",
510
- aggregate: { count: {} },
332
+ select: {
333
+ name: true,
334
+ parent: {
335
+ name: true,
336
+ description: true,
337
+ parent: {
338
+ name: true,
339
+ },
340
+ },
341
+ root: {
342
+ name: true,
343
+ },
344
+ },
511
345
  filters: {
512
- name: { search: { query: "compressor seal" } },
346
+ name: { prefix: "Pump" },
513
347
  },
348
+ limit: 50,
514
349
  });
515
- ```
516
350
 
517
- ---
518
-
519
- ### Filter on related nodes
351
+ const firstParentName = items[0]?.parent?.name;
352
+ ```
520
353
 
521
- Filter the root view based on properties of a direct or nested relation. This uses Cognite nested filters under the hood.
354
+ List relations work the same way. For example, `path` can be expanded into asset objects for breadcrumb-style views.
522
355
 
523
356
  ```ts
524
- // Assets whose parent is named "Site Root"
525
357
  const { items } = await model.query<CogniteAsset>()({
526
358
  viewExternalId: "CogniteAsset",
527
- select: { name: true, parent: { name: true } },
359
+ select: {
360
+ name: true,
361
+ path: {
362
+ name: true,
363
+ description: true,
364
+ },
365
+ },
528
366
  filters: {
529
- parent: { name: { eq: "Site Root" } },
367
+ externalId: { eq: "WMT:VAL" },
530
368
  },
531
- limit: 50,
532
369
  });
370
+ ```
533
371
 
534
- // Assets whose parent's asset class code is "PUMP"
535
- const pumpsByClass = await model.query<CogniteAsset>()({
372
+ Nested relation filters let you filter the root view based on related nodes.
373
+
374
+ ```ts
375
+ const { items } = await model.query<CogniteAsset>()({
536
376
  viewExternalId: "CogniteAsset",
537
377
  select: {
538
378
  name: true,
539
- parent: { assetClass: { name: true, code: true } },
379
+ parent: {
380
+ name: true,
381
+ },
540
382
  },
541
383
  filters: {
542
- parent: { assetClass: { code: { eq: "PUMP" } } },
384
+ parent: {
385
+ name: { eq: "Site Root" },
386
+ },
543
387
  },
544
388
  limit: 50,
545
389
  });
390
+ ```
391
+
392
+ You can keep moving through the graph:
546
393
 
547
- // Combine root and nested conditions
548
- const filtered = await model.query<CogniteAsset>()({
394
+ ```ts
395
+ const pumpsByClass = await model.query<CogniteAsset>()({
549
396
  viewExternalId: "CogniteAsset",
550
- select: { name: true, parent: { name: true } },
397
+ select: {
398
+ name: true,
399
+ parent: {
400
+ assetClass: {
401
+ name: true,
402
+ code: true,
403
+ },
404
+ },
405
+ },
551
406
  filters: {
552
- AND: [
553
- { name: { prefix: "Pump" } },
554
- { parent: { name: { exists: true } } },
555
- ],
407
+ parent: {
408
+ assetClass: {
409
+ code: { eq: "PUMP" },
410
+ },
411
+ },
556
412
  },
557
- limit: 100,
413
+ limit: 50,
558
414
  });
559
415
  ```
560
416
 
561
- ---
417
+ ### Reverse Relations
562
418
 
563
- ### Query child assets (reverse relation)
564
-
565
- Declare reverse relations in the `IndustrialModel<TProps, TRelations>` metadata. The library resolves the correct traversal direction from your data model.
419
+ Declare reverse relations in `IndustrialModel<TProps, TRelations>`. The SDK resolves the traversal direction from the Cognite data model.
566
420
 
567
421
  ```ts
568
422
  type AssetWithChildren = IndustrialModel<
569
423
  ModelProps<CogniteAsset>,
570
- ModelRelations<CogniteAsset> & { children?: CogniteAsset[] }
424
+ ModelRelations<CogniteAsset> & {
425
+ children?: CogniteAsset[];
426
+ }
571
427
  >;
572
428
 
573
429
  const { items } = await model.query<AssetWithChildren>()({
@@ -585,18 +441,32 @@ const { items } = await model.query<AssetWithChildren>()({
585
441
  });
586
442
  ```
587
443
 
588
- ---
589
-
590
- ### Traverse edge relations (360 images on 3D objects)
444
+ ### Edge Relations
591
445
 
592
- Some relations are modeled as edges rather than direct node links. Select them the same way — the SDK builds the edge hop automatically.
446
+ Some relations are modeled as edges rather than direct node references. Select them with the same relation syntax.
593
447
 
594
448
  ```ts
449
+ type Cognite360Image = IndustrialModel<{
450
+ takenAt: string;
451
+ }>;
452
+
453
+ type Cognite3DObject = IndustrialModel<
454
+ {
455
+ name: string;
456
+ description: string;
457
+ },
458
+ {
459
+ images360?: Cognite360Image[];
460
+ }
461
+ >;
462
+
595
463
  const { items } = await model.query<Cognite3DObject>()({
596
464
  viewExternalId: "Cognite3DObject",
597
465
  select: {
598
466
  name: true,
599
- images360: { takenAt: true },
467
+ images360: {
468
+ takenAt: true,
469
+ },
600
470
  },
601
471
  filters: {
602
472
  name: { prefix: "Tank" },
@@ -605,120 +475,176 @@ const { items } = await model.query<Cognite3DObject>()({
605
475
  });
606
476
  ```
607
477
 
608
- ---
478
+ ## Pagination
479
+
480
+ `query()` returns a root cursor when more root-view items are available.
481
+
482
+ ```ts
483
+ import type { QueryResultItem } from "industrial-model";
484
+
485
+ let cursor: string | null = null;
486
+ const allAssets: QueryResultItem<CogniteAsset, { name: true; description: true }>[] = [];
487
+
488
+ do {
489
+ const result = await model.query<CogniteAsset>()({
490
+ viewExternalId: "CogniteAsset",
491
+ select: {
492
+ name: true,
493
+ description: true,
494
+ },
495
+ limit: 1000,
496
+ cursor,
497
+ });
609
498
 
610
- ### Sort by multiple fields
499
+ allAssets.push(...result.items);
500
+ cursor = result.cursor;
501
+ } while (cursor !== null);
502
+ ```
611
503
 
612
- Sort clauses apply to primitive fields on the root view, including node-level properties like `externalId`.
504
+ Pass `limit: -1` when you want the SDK to follow all root cursors automatically. The SDK issues multiple `instances.query` calls, using 1000 root items per page by default, and returns `cursor: null`.
613
505
 
614
506
  ```ts
615
507
  const { items } = await model.query<CogniteAsset>()({
616
508
  viewExternalId: "CogniteAsset",
617
- select: { name: true, sourceId: true },
618
- sort: {
619
- name: "ascending",
620
- externalId: "descending",
509
+ select: {
510
+ name: true,
511
+ description: true,
621
512
  },
622
- limit: 100,
513
+ filters: {
514
+ tags: { containsAny: ["production"] },
515
+ },
516
+ limit: -1,
623
517
  });
624
518
  ```
625
519
 
626
- ---
520
+ Expanded relations use internal pagination as well. When a nested relation query reaches the internal page size, the client follows dependency cursors for up to 3 additional rounds.
627
521
 
628
- ### Use a custom data model
522
+ ## Upsert
629
523
 
630
- Point the client at any FDM in your project not only Cognite Core. Scalar fields work with a plain object type; use `IndustrialModel<TProps, TRelations>` when you need relation selects, filters, or inference.
524
+ Use `upsert()` to create or patch nodes with the same model shape you use for queries. Each item must include `space` and `externalId`; all other fields are optional and only the fields you pass are updated.
631
525
 
632
526
  ```ts
633
- const model = new IndustrialModelClient(client, {
634
- space: "my-custom-space",
635
- externalId: "MyPlantModel",
636
- version: "1",
527
+ await model.upsert<CogniteAsset>()({
528
+ viewExternalId: "CogniteAsset",
529
+ items: [
530
+ {
531
+ space: "asset-space",
532
+ externalId: "pump-1",
533
+ name: "Pump 1",
534
+ parent: { space: "asset-space", externalId: "root" },
535
+ },
536
+ ],
637
537
  });
538
+ ```
638
539
 
639
- type PlantArea = IndustrialModel<{
640
- name: string;
641
- code: string;
642
- site?: NodeId;
643
- }>;
540
+ Direct relations are written as `NodeId` values. Reverse direct relations are written by patching the target nodes through the relation field defined in Cognite. For example, writing `children` on an asset updates each child asset's `parent` reference.
644
541
 
645
- const { items } = await model.query<PlantArea>()({
646
- viewExternalId: "PlantArea",
647
- select: { name: true, code: true, site: true },
648
- filters: { code: { prefix: "AREA-" } },
649
- limit: 200,
542
+ ```ts
543
+ await model.upsert<CogniteAsset>()({
544
+ viewExternalId: "CogniteAsset",
545
+ items: [
546
+ {
547
+ space: "asset-space",
548
+ externalId: "parent-asset",
549
+ children: [{ space: "asset-space", externalId: "child-1" }],
550
+ },
551
+ ],
650
552
  });
651
553
  ```
652
554
 
653
- ---
654
-
655
- ### Full example: assets, equipment, and filters
656
-
657
- A single query combining nested selects, nested filters, sorting, and pagination.
555
+ Edge-backed relations need an edge ID. Provide `onEdgeCreation` for every edge connection property you write. The callback receives normalized `startNode`, `endNode`, and `edgeType` values after the SDK has applied the relation direction from the view metadata.
658
556
 
659
557
  ```ts
660
- type AssetWithRelations = IndustrialModel<
661
- ModelProps<CogniteAsset>,
662
- ModelRelations<CogniteAsset> & { children?: CogniteAsset[] }
663
- >;
664
-
665
- const { items, cursor } = await model.query<AssetWithRelations>()({
666
- viewExternalId: "CogniteAsset",
667
- select: {
668
- name: true,
669
- description: true,
670
- tags: true,
671
- parent: {
672
- name: true,
673
- assetClass: { name: true, code: true },
558
+ await model.upsert<Cognite3DObject>()({
559
+ viewExternalId: "Cognite3DObject",
560
+ items: [
561
+ {
562
+ space: "object-space",
563
+ externalId: "object-1",
564
+ images360: [{ space: "image-space", externalId: "image-1" }],
674
565
  },
566
+ ],
567
+ onEdgeCreation: {
568
+ images360: ({ startNode, endNode, edgeType }) => ({
569
+ space: startNode.space,
570
+ externalId: `${startNode.externalId}:${edgeType.externalId}:${endNode.externalId}`,
571
+ }),
675
572
  },
676
- filters: {
677
- name: { prefix: "WMT" },
678
- parent: { name: { exists: true } },
679
- OR: [
680
- { tags: { containsAny: ["critical"] } },
681
- { sourceId: { eq: "sap" } },
682
- ],
683
- },
684
- sort: { name: "ascending" },
685
- limit: 25,
686
- cursor: null,
687
573
  });
574
+ ```
688
575
 
689
- // Follow-up page
690
- if (cursor) {
691
- const next = await model.query<AssetWithRelations>()({
692
- viewExternalId: "CogniteAsset",
693
- select: {
694
- name: true,
695
- description: true,
696
- tags: true,
697
- parent: { name: true, assetClass: { name: true, code: true } },
698
- },
699
- filters: {
700
- name: { prefix: "WMT" },
701
- parent: { name: { exists: true } },
576
+ `edgeMode` controls how edge connection properties are applied:
577
+
578
+ | Mode | Behavior |
579
+ | --- | --- |
580
+ | `"append"` | Default. Creates the generated edges and leaves existing edges untouched. |
581
+ | `"replace"` | Queries existing edges for the provided edge connection fields, deletes edges that were not generated by the current upsert, then applies the new edges. |
582
+
583
+ To clear an edge connection for a node, include the property with an empty array and use `edgeMode: "replace"`:
584
+
585
+ ```ts
586
+ await model.upsert<Cognite3DObject>()({
587
+ viewExternalId: "Cognite3DObject",
588
+ edgeMode: "replace",
589
+ items: [
590
+ {
591
+ space: "object-space",
592
+ externalId: "object-1",
593
+ images360: [],
702
594
  },
703
- sort: { name: "ascending" },
704
- limit: 25,
705
- cursor,
706
- });
707
- }
595
+ ],
596
+ });
708
597
  ```
709
598
 
710
- ---
599
+ This deletes existing `images360` edges for `object-1`. Other edge connection fields on the same node are not touched, and omitting `images360` entirely leaves its existing edges unchanged.
600
+
601
+ Use `replace: true` when you want Cognite apply replace semantics for container-backed node properties.
602
+
603
+ Important constraints:
711
604
 
712
- ### Count assets by source ID
605
+ - Relation fields accept only `NodeId` or `NodeId[]` references. Nested node mutation is intentionally not supported.
606
+ - Unknown fields are rejected before Cognite is called.
607
+ - Edge connection fields require `onEdgeCreation.<property>` only when the submitted array contains edges to create.
608
+ - Cognite apply requests are limited to 1000 writes/deletes per call. You can still pass more than 1000 upsert items or edge references; the SDK follows paginated edge-replacement queries and splits large apply payloads into multiple Cognite calls.
609
+ - `edgeMode: "replace"` only replaces edge connection fields included in the submitted items.
713
610
 
714
- Group assets and count how many share each `sourceId`. Uses the same `filters` syntax as `query`.
611
+ ## Delete
612
+
613
+ Use `delete()` when you only need to delete nodes by identity. The method accepts an array of values with `space` and `externalId`; any extra fields are ignored.
614
+
615
+ ```ts
616
+ await model.delete([
617
+ { space: "asset-space", externalId: "pump-1" },
618
+ { space: "asset-space", externalId: "pump-2", name: "Pump 2" },
619
+ ]);
620
+ ```
621
+
622
+ The delete operation is view-independent, so it does not require `viewExternalId` and is also available directly on `CogniteCoreClient`.
623
+
624
+ ```ts
625
+ await core.delete([{ space: "asset-space", externalId: "pump-1" }]);
626
+ ```
627
+
628
+ Deletes are sent through Cognite apply. When more than 1000 nodes are provided, the SDK splits them into multiple Cognite calls.
629
+
630
+ ## Aggregation
631
+
632
+ Use `aggregate()` when you need grouped counts, distinct values, or numeric summaries without loading every instance.
633
+
634
+ Group and count assets by `sourceId`:
715
635
 
716
636
  ```ts
717
637
  const { items } = await model.aggregate<CogniteAsset>()({
718
638
  viewExternalId: "CogniteAsset",
719
- groupBy: { sourceId: true },
720
- aggregate: { count: {} },
721
- filters: { name: { prefix: "WMT" } },
639
+ groupBy: {
640
+ sourceId: true,
641
+ },
642
+ aggregate: {
643
+ count: {},
644
+ },
645
+ filters: {
646
+ name: { prefix: "WMT" },
647
+ },
722
648
  });
723
649
 
724
650
  for (const row of items) {
@@ -726,37 +652,36 @@ for (const row of items) {
726
652
  }
727
653
  ```
728
654
 
729
- ---
730
-
731
- ### List distinct source IDs
732
-
733
- Omit `aggregate` to return unique combinations of the `groupBy` fields (up to 1000 groups).
655
+ Omit `aggregate` to list distinct values for grouped fields:
734
656
 
735
657
  ```ts
736
658
  const { items } = await model.aggregate<CogniteAsset>()({
737
659
  viewExternalId: "CogniteAsset",
738
- groupBy: { sourceId: true },
660
+ groupBy: {
661
+ sourceId: true,
662
+ },
739
663
  });
740
664
 
741
665
  const sourceIds = items.map((row) => row.group?.sourceId);
742
666
  ```
743
667
 
744
- ---
745
-
746
- ### Average volume by type
747
-
748
- Use `avg`, `min`, `max`, or `sum` on numeric view properties. Only one aggregate operation per call.
668
+ Use `avg`, `min`, `max`, or `sum` on numeric properties:
749
669
 
750
670
  ```ts
751
671
  type PointCloudVolume = IndustrialModel<{
752
672
  volume: number;
753
673
  volumeType: string;
674
+ object3D?: NodeId;
754
675
  }>;
755
676
 
756
677
  const { items } = await model.aggregate<PointCloudVolume>()({
757
678
  viewExternalId: "CognitePointCloudVolume",
758
- groupBy: { volumeType: true },
759
- aggregate: { avg: "volume" },
679
+ groupBy: {
680
+ volumeType: true,
681
+ },
682
+ aggregate: {
683
+ avg: "volume",
684
+ },
760
685
  });
761
686
 
762
687
  items[0]?.group?.volumeType;
@@ -764,219 +689,472 @@ items[0]?.aggregate?.property; // "volume"
764
689
  items[0]?.aggregate?.value;
765
690
  ```
766
691
 
767
- Other numeric aggregates:
692
+ Count all rows matching a filter:
768
693
 
769
694
  ```ts
770
- await model.aggregate<PointCloudVolume>()({
771
- viewExternalId: "CognitePointCloudVolume",
772
- aggregate: { min: "volume" },
695
+ const { items } = await model.aggregate<CogniteAsset>()({
696
+ viewExternalId: "CogniteAsset",
697
+ aggregate: {
698
+ count: {},
699
+ },
700
+ filters: {
701
+ OR: [{ tags: { containsAny: ["critical"] } }, { sourceId: { eq: "sap" } }],
702
+ },
773
703
  });
774
704
 
775
- await model.aggregate<PointCloudVolume>()({
776
- viewExternalId: "CognitePointCloudVolume",
777
- aggregate: { max: "volume" },
778
- });
705
+ items[0]?.aggregate?.value;
706
+ ```
707
+
708
+ Group by a direct relation when you need relation IDs in the result:
779
709
 
780
- await model.aggregate<PointCloudVolume>()({
710
+ ```ts
711
+ const { items } = await model.aggregate<PointCloudVolume>()({
781
712
  viewExternalId: "CognitePointCloudVolume",
782
- aggregate: { sum: "volume" },
713
+ groupBy: {
714
+ object3D: true,
715
+ },
716
+ aggregate: {
717
+ sum: "volume",
718
+ },
783
719
  });
784
- ```
785
720
 
786
- ---
721
+ items[0]?.group?.object3D?.externalId;
722
+ ```
787
723
 
788
- ### Count non-null values for a property
724
+ Text search filters are also supported in aggregations:
789
725
 
790
726
  ```ts
791
727
  const { items } = await model.aggregate<CogniteAsset>()({
792
728
  viewExternalId: "CogniteAsset",
793
- aggregate: { count: "name" },
729
+ aggregate: {
730
+ count: {},
731
+ },
732
+ filters: {
733
+ name: { search: { query: "compressor seal" } },
734
+ },
794
735
  });
795
-
796
- items[0]?.aggregate?.property; // "name"
797
- items[0]?.aggregate?.value;
798
736
  ```
799
737
 
800
- ---
738
+ ## Runtime Validation
801
739
 
802
- ### Count all matching assets
740
+ By default, the SDK validates query inputs against the loaded Cognite view metadata before building the request. Query results are mapped without parsing each returned item.
803
741
 
804
- A global count with no `groupBy`:
742
+ Enable `validateResults` when you also want result parsing through Zod schemas derived from Cognite view metadata:
805
743
 
806
744
  ```ts
807
- const { items } = await model.aggregate<CogniteAsset>()({
808
- viewExternalId: "CogniteAsset",
809
- aggregate: { count: {} },
810
- filters: {
811
- OR: [{ tags: { containsAny: ["critical"] } }, { sourceId: { eq: "sap" } }],
745
+ const model = new IndustrialModelClient(
746
+ client,
747
+ {
748
+ space: "cdf_cdm",
749
+ externalId: "CogniteCore",
750
+ version: "v1",
812
751
  },
813
- });
814
-
815
- items[0]?.aggregate?.value;
752
+ {
753
+ validateResults: true,
754
+ },
755
+ );
816
756
  ```
817
757
 
818
- ---
758
+ When result validation is enabled, Cognite `date` and `timestamp` view properties are converted to JavaScript `Date` objects. Without it, result values are returned as Cognite provides them, usually ISO strings for timestamps.
819
759
 
820
- ### Group by a direct relation
760
+ ## Cognite Core Client
821
761
 
822
- `groupBy` supports direct relations; results are returned as `NodeId` objects.
762
+ For applications working with the Cognite Core Data Model (`cdf_cdm/CogniteCore/v1`), use `CogniteCoreClient` instead of `IndustrialModelClient`. It pre-configures the data model, bundles all view type definitions, and moves the view name to the first positional argument so TypeScript can infer the model type without a generic annotation.
823
763
 
824
764
  ```ts
825
- type PointCloudVolume = IndustrialModel<{
826
- volume: number;
827
- object3D?: NodeId;
828
- }>;
765
+ import { CogniteClient } from "@cognite/sdk";
766
+ import { CogniteCoreClient } from "industrial-model/cognite-core";
829
767
 
830
- const { items } = await model.aggregate<PointCloudVolume>()({
831
- viewExternalId: "CognitePointCloudVolume",
832
- groupBy: { object3D: true },
833
- aggregate: { sum: "volume" },
768
+ const client = new CogniteClient({ ... });
769
+ const core = new CogniteCoreClient(client);
770
+ ```
771
+
772
+ Query any Cognite Core view by passing its name to `query()`:
773
+
774
+ ```ts
775
+ const { items } = await core.query("CogniteAsset")({
776
+ select: {
777
+ name: true,
778
+ description: true,
779
+ parent: { name: true },
780
+ },
781
+ filters: {
782
+ name: { prefix: "Pump" },
783
+ },
784
+ limit: 50,
834
785
  });
835
786
 
836
- items[0]?.group?.object3D?.externalId;
787
+ items[0]?.name; // string | undefined
788
+ items[0]?.parent?.name; // string | undefined
837
789
  ```
838
790
 
839
- ---
791
+ The view name drives TypeScript inference — no generic annotation is needed. All filters, `select` fields, and nested relation selections are type-checked against the bundled view definition. Every feature available on `IndustrialModelClient` — text search, pagination, `limit: -1`, nested filters, and relation traversal — works identically.
840
792
 
841
- ## API
793
+ Aggregations use the same positional-view-name pattern:
842
794
 
843
- ### Exports
795
+ ```ts
796
+ const { items } = await core.aggregate("CogniteEquipment")({
797
+ groupBy: { manufacturer: true },
798
+ aggregate: { count: {} },
799
+ filters: {
800
+ equipmentType: { exists: true },
801
+ },
802
+ });
844
803
 
845
- | Symbol | Description |
846
- |--------|-------------|
847
- | `IndustrialModelClient` | Main client |
848
- | `IndustrialModel`, `ModelProps`, `ModelRelations` | Type helpers for models and relations |
849
- | `NodeId`, `DataModelId` | Instance and data-model identifiers |
850
- | `QueryOptions`, `QuerySelect`, `WhereInput`, `SortInput` | Query input types |
851
- | `QueryResult`, `QueryResultItem`, `QueryResultMetadata` | Query output types |
852
- | `AggregateOptions`, `AggregateGroupBy`, `AggregateDefinition` | Aggregate input types |
853
- | `AggregateResult`, `AggregateResultItem`, `GroupByKey` | Aggregate output types |
854
- | `buildViewSchema`, `nodeIdSchema` | Zod schemas built from Cognite view metadata |
855
- | `SortDirection` | `"ascending"` \| `"descending"` |
804
+ items[0]?.group?.manufacturer;
805
+ items[0]?.aggregate?.value;
806
+ ```
856
807
 
857
- ### `new IndustrialModelClient(client, dataModelId, options?)`
808
+ Upserts use the same pattern and infer the item shape from the view name:
858
809
 
859
- | Parameter | Type | Description |
860
- |-----------|------|-------------|
861
- | `client` | `CogniteClient` | Authenticated Cognite SDK client |
862
- | `dataModelId` | `DataModelId` | Space, externalId, and version of the data model |
863
- | `options.validateResults` | `boolean` | Optional. Validate and parse query results with Zod schemas derived from Cognite view metadata |
810
+ ```ts
811
+ await core.upsert("CogniteAsset")({
812
+ items: [
813
+ {
814
+ space: "asset-space",
815
+ externalId: "pump-1",
816
+ name: "Pump 1",
817
+ parent: { space: "asset-space", externalId: "root" },
818
+ },
819
+ ],
820
+ });
821
+ ```
822
+
823
+ Deletes are view-independent:
864
824
 
865
- On the first query, view definitions are loaded from CDF and cached for the lifetime of the client instance.
825
+ ```ts
826
+ await core.delete([{ space: "asset-space", externalId: "pump-1" }]);
827
+ ```
866
828
 
867
- Query inputs are validated against the loaded view metadata before the Cognite request is built. Result validation is opt-in because it parses every returned item:
829
+ All Cognite Core view types are exported from `industrial-model` and can be imported directly for use with `IndustrialModelClient` if needed:
868
830
 
869
831
  ```ts
870
- const model = new IndustrialModelClient(client, dataModelId, {
871
- validateResults: true,
872
- });
832
+ import type { CogniteAsset, CogniteEquipment, CogniteTimeSeries } from "industrial-model";
873
833
  ```
874
834
 
875
- When `validateResults` is enabled, Cognite `date` and `timestamp` view properties are converted to JavaScript `Date` objects. Without this option, result values are returned as Cognite provides them, usually ISO strings for timestamps.
835
+ ### Inward List-Relation Limitation
876
836
 
877
- ### `model.query<TModel>()(options)`
837
+ Cognite rejects server-side inward traversal of list direct relations. As a result, `timeSeries`, `files`, and `activities` cannot be expanded from `CogniteAsset`. Attempting to select them throws a descriptive error before the Cognite API is called, naming the view to query and the field to filter on.
878
838
 
879
- | Option | Type | Description |
880
- |--------|------|-------------|
881
- | `viewExternalId` | `string` | The view to query |
882
- | `select` | `QuerySelect<TModel>` | Optional. Defaults to `{ _all: true }` (all scalar fields). Use `_all: true` explicitly or list fields; use nested objects for relations |
883
- | `filters` | `WhereInput<TModel>` | Filter conditions (supports nested relation filters) |
884
- | `sort` | `SortInput<TModel>` | Sort by primitive fields on the **root** view only |
885
- | `limit` | `number` | Root page size (default `1000`). Use `-1` to fetch all root pages automatically |
886
- | `cursor` | `string \| null` | Pagination cursor from a previous response |
839
+ The alternative is to query the target view directly and filter by the relation field pointing back to the asset:
887
840
 
888
- `query()` uses a curried form so you can supply the model types first and still get return-type inference from `select`.
841
+ ```ts
842
+ // not supported — throws before calling Cognite
843
+ await core.query("CogniteAsset")({
844
+ select: { timeSeries: { name: true } } as never,
845
+ });
846
+
847
+ // correct alternative: query CogniteTimeSeries and filter by assets
848
+ const { items } = await core.query("CogniteTimeSeries")({
849
+ select: { name: true, type: true },
850
+ filters: {
851
+ assets: { containsAny: [{ space: "my-space", externalId: "WMT:VAL" }] },
852
+ },
853
+ });
854
+ ```
889
855
 
890
- Use `IndustrialModel<TProps, TRelations>` when the model has expandable direct, reverse, or edge relations.
856
+ ## Custom Data Models
891
857
 
892
- Returns `Promise<QueryResult<QueryResultItem<TModel, TSelect>>>`:
858
+ The client can query any FDM in your CDF project. Cognite Core is not required.
893
859
 
894
860
  ```ts
895
- type QueryResult<TItem> = {
896
- items: TItem[];
897
- cursor: string | null; // null when no more root pages
898
- };
861
+ const model = new IndustrialModelClient(client, {
862
+ space: "my-custom-space",
863
+ externalId: "MyPlantModel",
864
+ version: "1",
865
+ });
866
+
867
+ type PlantArea = IndustrialModel<{
868
+ name: string;
869
+ code: string;
870
+ site?: NodeId;
871
+ }>;
872
+
873
+ const { items } = await model.query<PlantArea>()({
874
+ viewExternalId: "PlantArea",
875
+ select: {
876
+ name: true,
877
+ code: true,
878
+ site: true,
879
+ },
880
+ filters: {
881
+ code: { prefix: "AREA-" },
882
+ },
883
+ limit: 200,
884
+ });
899
885
  ```
900
886
 
901
- Each item always includes instance metadata (`space`, `externalId`, `createdTime`, `deletedTime`, `lastUpdatedTime`, `instanceType`) plus the fields you selected. With `{ _all: true }`, scalar view properties are included as well.
887
+ ## Complete Example
902
888
 
903
- Example:
889
+ This example combines typed relations, nested selections, nested filters, sorting, and cursor pagination.
904
890
 
905
891
  ```ts
906
- const { items } = await model.query<CogniteAsset>()({
892
+ type AssetWithRelations = IndustrialModel<
893
+ ModelProps<CogniteAsset>,
894
+ ModelRelations<CogniteAsset> & {
895
+ children?: CogniteAsset[];
896
+ }
897
+ >;
898
+
899
+ const { items, cursor } = await model.query<AssetWithRelations>()({
907
900
  viewExternalId: "CogniteAsset",
908
901
  select: {
902
+ name: true,
903
+ description: true,
904
+ tags: true,
909
905
  parent: {
910
906
  name: true,
907
+ assetClass: {
908
+ name: true,
909
+ code: true,
910
+ },
911
+ },
912
+ children: {
913
+ name: true,
914
+ },
915
+ },
916
+ filters: {
917
+ name: { prefix: "WMT" },
918
+ parent: {
919
+ name: { exists: true },
911
920
  },
921
+ OR: [
922
+ { tags: { containsAny: ["critical"] } },
923
+ { sourceId: { eq: "sap" } },
924
+ ],
925
+ },
926
+ sort: {
927
+ name: "ascending",
912
928
  },
929
+ limit: 25,
930
+ cursor: null,
913
931
  });
914
932
 
915
- items[0]?.parent?.name;
916
- items[0]?.externalId;
933
+ if (cursor) {
934
+ const next = await model.query<AssetWithRelations>()({
935
+ viewExternalId: "CogniteAsset",
936
+ select: {
937
+ name: true,
938
+ description: true,
939
+ tags: true,
940
+ parent: {
941
+ name: true,
942
+ assetClass: {
943
+ name: true,
944
+ code: true,
945
+ },
946
+ },
947
+ },
948
+ filters: {
949
+ name: { prefix: "WMT" },
950
+ parent: {
951
+ name: { exists: true },
952
+ },
953
+ },
954
+ sort: {
955
+ name: "ascending",
956
+ },
957
+ limit: 25,
958
+ cursor,
959
+ });
960
+ }
917
961
  ```
918
962
 
919
- ### `model.aggregate<TModel>()(options)`
963
+ ## API Reference
920
964
 
921
- | Option | Type | Description |
922
- |--------|------|-------------|
923
- | `viewExternalId` | `string` | The view to aggregate |
924
- | `groupBy` | `AggregateGroupBy<TModel>` | Optional. Object of groupable properties set to `true` (max 5) |
925
- | `filters` | `WhereInput<TModel>` | Same filter syntax as `query` |
926
- | `aggregate` | `AggregateDefinition<TModel>` | Optional. One of `avg`, `min`, `max`, `sum`, or `count` per call |
965
+ ### `new CogniteCoreClient(client, options?)`
927
966
 
928
- Provide at least one of `groupBy` or `aggregate`. Omit `aggregate` to fetch distinct values for the grouped fields. The client always requests nodes with `limit: 1000`.
967
+ | Parameter | Type | Description |
968
+ | --- | --- | --- |
969
+ | `client` | `CogniteClient` | Authenticated Cognite SDK client. |
970
+ | `options` | `IndustrialModelClientOptions` | Optional. Same options as `IndustrialModelClient`. |
971
+
972
+ Pre-configured for the Cognite Core Data Model (`cdf_cdm/CogniteCore/v1`). The exported constant `COGNITE_CORE_DATA_MODEL` holds the data model identifier if you need to pass it to other utilities.
973
+
974
+ ### `core.query(viewExternalId)(options)`
975
+
976
+ Same as `model.query<TModel>()(options)` on `IndustrialModelClient`, except the view is provided as the first positional argument and the model type is inferred from it. The `viewExternalId` option is not accepted in the second call. `viewExternalId` must be a valid `CogniteCoreViewExternalId`.
977
+
978
+ ### `core.aggregate(viewExternalId)(options)`
979
+
980
+ Same as `model.aggregate<TModel>()(options)` on `IndustrialModelClient`, with the view name as the first positional argument.
929
981
 
930
- `aggregate()` uses the same curried form as `query`. Each result item has the shape:
982
+ ### `core.upsert(viewExternalId)(options)`
983
+
984
+ Same as `model.upsert<TModel>()(options)` on `IndustrialModelClient`, with the view name as the first positional argument. The model type is inferred from the Cognite Core view name.
985
+
986
+ ### `core.delete(items)`
987
+
988
+ Same as `model.delete(items)` on `IndustrialModelClient`. Deletes nodes by `space` and `externalId`; no view name is required.
989
+
990
+ ### `new IndustrialModelClient(client, dataModelId, options?)`
991
+
992
+ | Parameter | Type | Description |
993
+ | --- | --- | --- |
994
+ | `client` | `CogniteClient` | Authenticated Cognite SDK client. |
995
+ | `dataModelId` | `DataModelId` | Data model `space`, `externalId`, and `version`. |
996
+ | `options.validateResults` | `boolean` | Optional. Parse result items with generated Zod schemas. |
997
+
998
+ On the first query or aggregation, view definitions are loaded from CDF and cached for the lifetime of the client instance.
999
+
1000
+ ### `model.query<TModel>()(options)`
1001
+
1002
+ `query()` uses a curried form so you can provide the model type first and still get return-type inference from `select`.
1003
+
1004
+ | Option | Description |
1005
+ | --- | --- |
1006
+ | `viewExternalId` | View to query. |
1007
+ | `select` | Optional. Defaults to `{ _all: true }`. Use nested objects for relations. |
1008
+ | `filters` | Field, logical, search, and nested relation filters. |
1009
+ | `sort` | Sort by primitive fields on the root view only. |
1010
+ | `limit` | Root page size. Defaults to `1000`. Use `-1` to fetch all root pages. |
1011
+ | `cursor` | Root pagination cursor from a previous response. |
1012
+
1013
+ Returns:
931
1014
 
932
1015
  ```ts
933
- type AggregateResultItem = {
934
- group?: { /* keys from groupBy */ };
935
- aggregate?: { property?: string; value: number };
1016
+ type QueryResult<TItem> = {
1017
+ items: TItem[];
1018
+ cursor: string | null;
936
1019
  };
937
1020
  ```
938
1021
 
939
- When counting all rows, `aggregate` has only `value` (no `property`). When aggregating a field, `property` matches the name you passed in the request.
1022
+ Each item includes instance metadata such as `space`, `externalId`, `version`, `createdTime`, `deletedTime`, and `lastUpdatedTime`, plus the selected fields.
940
1023
 
941
- | Aggregate | Input | Use case |
942
- |-----------|-------|----------|
943
- | `count` | `{ count: {} }` | Row count (optionally filtered) |
944
- | `count` | `{ count: "name" }` | Count non-null values for a property |
945
- | `avg` | `{ avg: "volume" }` | Average of a numeric property |
946
- | `min` | `{ min: "volume" }` | Minimum numeric value |
947
- | `max` | `{ max: "volume" }` | Maximum numeric value |
948
- | `sum` | `{ sum: "volume" }` | Sum of a numeric property |
1024
+ ### `model.upsert<TModel>()(options)`
1025
+
1026
+ `upsert()` uses the same model type as `query()` and accepts partial node patches. It returns the Cognite apply result items.
949
1027
 
950
- See [Count assets by source ID](#count-assets-by-source-id), [List distinct source IDs](#list-distinct-source-ids), and [Average volume by type](#average-volume-by-type) for full examples.
1028
+ | Option | Description |
1029
+ | --- | --- |
1030
+ | `viewExternalId` | View to create or patch. |
1031
+ | `items` | Node patches. Each item must include `space` and `externalId`. Inputs larger than Cognite's 1000-item apply limit are split into multiple calls. |
1032
+ | `replace` | Optional. Enables Cognite apply replace semantics for submitted container-backed properties. |
1033
+ | `edgeMode` | Optional. `"append"` by default; use `"replace"` to remove existing edge connection edges for submitted edge fields before applying the new references. |
1034
+ | `onEdgeCreation` | Optional map of edge connection property names to callbacks that generate edge IDs. Required for every edge connection property that creates one or more edges. |
951
1035
 
952
- ### Automatic query behavior
1036
+ Returns:
953
1037
 
954
- - **`hasData` filter** — every root query includes a `hasData` constraint for the target view.
955
- - **Nested relation limits** — expanded relations use an internal page size of `10000` per sub-query.
956
- - **Dependency pagination** — when a nested sub-query returns `10000` items and a cursor, the client issues follow-up queries (up to 3 rounds) to load additional related data.
1038
+ ```ts
1039
+ type UpsertResult = {
1040
+ items: Array<{
1041
+ instanceType: "node" | "edge";
1042
+ space: string;
1043
+ externalId: string;
1044
+ version?: number;
1045
+ wasModified?: boolean;
1046
+ createdTime?: number;
1047
+ lastUpdatedTime?: number;
1048
+ }>;
1049
+ };
1050
+ ```
1051
+
1052
+ ### `model.delete<TItem extends NodeId>(items)`
1053
+
1054
+ Deletes nodes by identity. Each item must include `space` and `externalId`; extra fields are ignored. Inputs larger than Cognite's 1000-item apply limit are split into multiple calls.
1055
+
1056
+ ```ts
1057
+ await model.delete([{ space: "asset-space", externalId: "pump-1" }]);
1058
+ ```
1059
+
1060
+ Returns:
957
1061
 
958
- ### Filter operators
1062
+ ```ts
1063
+ type DeleteResult = {
1064
+ items: Array<{
1065
+ instanceType: "node";
1066
+ space: string;
1067
+ externalId: string;
1068
+ version?: number;
1069
+ wasModified?: boolean;
1070
+ createdTime?: number;
1071
+ lastUpdatedTime?: number;
1072
+ }>;
1073
+ };
1074
+ ```
959
1075
 
960
- | Type | Operators |
961
- |------|-----------|
1076
+ ### `model.aggregate<TModel>()(options)`
1077
+
1078
+ | Option | Description |
1079
+ | --- | --- |
1080
+ | `viewExternalId` | View to aggregate. |
1081
+ | `groupBy` | Groupable properties set to `true`; max 5 fields. |
1082
+ | `filters` | Same filter syntax as `query()`. |
1083
+ | `aggregate` | One of `avg`, `min`, `max`, `sum`, or `count`. |
1084
+
1085
+ Provide at least one of `groupBy` or `aggregate`. Omit `aggregate` to fetch distinct grouped values. The client requests up to 1000 aggregate rows.
1086
+
1087
+ | Aggregate | Input | Use case |
1088
+ | --- | --- | --- |
1089
+ | `count` | `{ count: {} }` | Row count, optionally filtered. |
1090
+ | `count` | `{ count: "name" }` | Count non-null values for a property. |
1091
+ | `avg` | `{ avg: "volume" }` | Average of a numeric property. |
1092
+ | `min` | `{ min: "volume" }` | Minimum numeric value. |
1093
+ | `max` | `{ max: "volume" }` | Maximum numeric value. |
1094
+ | `sum` | `{ sum: "volume" }` | Sum of a numeric property. |
1095
+
1096
+ ### Filter Operators
1097
+
1098
+ | Field type | Operators |
1099
+ | --- | --- |
962
1100
  | `string` | `eq`, `in`, `prefix`, `search`, `exists` |
963
1101
  | `number` | `eq`, `in`, `gt`, `gte`, `lt`, `lte`, `exists` |
964
1102
  | `boolean` | `eq`, `exists` |
965
- | timestamp / `Date` | `eq`, `in`, `gt`, `gte`, `lt`, `lte`, `exists` — use ISO strings or `Date` values (coerced to ISO) |
1103
+ | timestamp / `Date` | `eq`, `in`, `gt`, `gte`, `lt`, `lte`, `exists` |
966
1104
  | `NodeId` | `eq`, `in`, `exists` |
967
1105
  | `string[]` | `containsAny`, `containsAll`, `search`, `exists` |
968
1106
  | `T[]` | `containsAny`, `containsAll`, `exists` |
969
1107
 
970
- Logical combinators `AND`, `OR`, and `NOT` are supported at any nesting level, including inside nested relation filters (e.g. `parent: { OR: [...] }`).
1108
+ Logical combinators `AND`, `OR`, and `NOT` are supported at any nesting level, including nested relation filters.
971
1109
 
972
- `search` is available for Cognite text properties and string-list text properties. It is not accepted on node metadata fields such as `externalId` or `space`. Each `search` filter uses `instances.search` with `limit: 1000`, maps the matched nodes to `instanceReferences`, and applies those references to the query or aggregate request.
1110
+ ### Relation Traversal
973
1111
 
974
- ### Relation traversal
1112
+ | Relation type | Description |
1113
+ | --- | --- |
1114
+ | Direct relations | Outward node references such as `parent`, `asset`, and `unit`. |
1115
+ | Reverse relations | Relations declared only in `TRelations`, such as `children` on `CogniteAsset`. |
1116
+ | Edge relations | Edge-backed relations declared in `TRelations`, such as `images360` on `Cognite3DObject`. |
1117
+ | Depth | Nested `select` and `filters` are supported up to 3 levels deep. |
975
1118
 
976
- - **Direct relations** — `parent`, `asset`, `unit` (outwards from the current node). List relations such as `path` return arrays when expanded.
977
- - **Reverse relations** — declare in `TRelations` (e.g. `children` on `CogniteAsset`)
978
- - **Edge relations** — declare in `TRelations` (e.g. `images360` on `Cognite3DObject`)
979
- - **Depth** — nested `select` and `filters` up to 3 levels; dependency pages for large nested result sets are fetched automatically (see above)
1119
+ ### Exports
1120
+
1121
+ **Core**
1122
+
1123
+ | Symbol | Description |
1124
+ | --- | --- |
1125
+ | `IndustrialModelClient` | Main client for any FDM data model. |
1126
+ | `IndustrialModel`, `ModelProps`, `ModelRelations` | Type helpers for model properties and relation metadata. |
1127
+ | `NodeId`, `DataModelId` | Instance and data model identifiers. |
1128
+ | `QuerySelect` | Type helper for reusable query selections. |
1129
+ | `QueryResult`, `QueryResultItem` | Query output types. |
1130
+ | `AggregateResult`, `AggregateResultItem` | Aggregate output types. |
1131
+ | `UpsertOptions`, `UpsertNode`, `UpsertProperties` | Upsert input helper types. |
1132
+ | `UpsertResult`, `UpsertResultItem` | Upsert output types. |
1133
+ | `DeleteExecutor`, `DeleteResult`, `DeleteResultItem` | Delete helper and output types. |
1134
+ | `EdgeCreationContext`, `EdgeCreationCallback`, `EdgeCreationCallbacks`, `EdgeMode` | Edge upsert helper types. |
1135
+ | `IndustrialModelClientOptions` | Client configuration options. |
1136
+
1137
+ **Cognite Core**
1138
+
1139
+ | Symbol | Description |
1140
+ | --- | --- |
1141
+ | `CogniteCoreClient` | Convenience client pre-configured for `cdf_cdm/CogniteCore/v1`. |
1142
+ | `COGNITE_CORE_DATA_MODEL` | Data model identifier constant for Cognite Core v1. |
1143
+ | `CogniteCoreViewExternalId` | Union type of all Cognite Core view names. |
1144
+ | `CogniteAsset`, `CogniteAssetClass`, `CogniteAssetType` | Asset hierarchy views. |
1145
+ | `CogniteEquipment`, `CogniteEquipmentType` | Equipment views. |
1146
+ | `CogniteFile`, `CogniteFileCategory` | File views. |
1147
+ | `CogniteActivity` | Activity view. |
1148
+ | `CogniteTimeSeries` | Time series view. |
1149
+ | `CogniteUnit` | Unit of measurement view. |
1150
+ | `CogniteAnnotation`, `CogniteDiagramAnnotation` | Annotation views. |
1151
+ | `CogniteSourceSystem` | Source system view. |
1152
+ | `CogniteDescribable`, `CogniteSourceable`, `CogniteSchedulable`, `CogniteVisualizable` | Mixin views. |
1153
+ | `Cognite3DObject`, `Cognite3DModel`, `Cognite3DRevision`, `Cognite3DTransformation` | 3D object and model views. |
1154
+ | `CogniteCADModel`, `CogniteCADRevision`, `CogniteCADNode` | CAD-specific views. |
1155
+ | `CognitePointCloudModel`, `CognitePointCloudRevision`, `CognitePointCloudVolume` | Point cloud views. |
1156
+ | `Cognite360Image`, `Cognite360ImageModel`, `Cognite360ImageCollection`, `Cognite360ImageStation`, `Cognite360ImageAnnotation` | 360 image views. |
1157
+ | `CogniteCubeMap` | Cube map view. |
980
1158
 
981
1159
  ## Releasing
982
1160