industrial-model 0.4.0 → 0.5.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
@@ -6,6 +6,7 @@ TypeScript SDK for querying [Cognite Flexible Data Models (FDM)](https://docs.co
6
6
 
7
7
  - **Type-safe queries** — define your data model types once, get compile-time validation on filters, selects, and sorts
8
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
9
10
  - **Dual CJS/ESM** — works in Node.js and bundlers out of the box
10
11
  - **Cursor-based pagination** — built-in support for iterating large result sets
11
12
 
@@ -60,7 +61,7 @@ const { items } = await model.query<{ name: string; description: string }>()({
60
61
  | Setup & types | [Shared type definitions](#shared-type-definitions) |
61
62
  | Basic queries | [Query assets](#query-assets), [Single asset](#query-a-single-asset-by-externalid) |
62
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) |
63
- | 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) |
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) |
64
65
  | Select & sort | [Select all scalars](#select-all-scalar-fields), [Multi-field sort](#sort-by-multiple-fields) |
65
66
  | Pagination | [Manual cursor loop](#paginate-through-all-assets), [Fetch all pages](#fetch-all-pages-automatically) |
66
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) |
@@ -469,6 +470,52 @@ const fullyTagged = await model.query<CogniteAsset>()({
469
470
 
470
471
  ---
471
472
 
473
+ ### Search text fields
474
+
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.
476
+
477
+ ```ts
478
+ const { items } = await model.query<CogniteAsset>()({
479
+ viewExternalId: "CogniteAsset",
480
+ select: { name: true, description: true, tags: true },
481
+ filters: {
482
+ name: { search: { query: "root pump", operator: "AND" } },
483
+ tags: { search: { query: "critical" } },
484
+ },
485
+ limit: 100,
486
+ });
487
+ ```
488
+
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.
490
+
491
+ ```ts
492
+ const pumps = await model.query<CogniteAsset>()({
493
+ viewExternalId: "CogniteAsset",
494
+ select: { name: true, sourceId: true },
495
+ filters: {
496
+ name: {
497
+ prefix: "Pump",
498
+ search: { query: "motor" },
499
+ },
500
+ sourceId: { exists: true },
501
+ },
502
+ });
503
+ ```
504
+
505
+ The same filter syntax is also supported by `aggregate`:
506
+
507
+ ```ts
508
+ const { items } = await model.aggregate<CogniteAsset>()({
509
+ viewExternalId: "CogniteAsset",
510
+ aggregate: { count: {} },
511
+ filters: {
512
+ name: { search: { query: "compressor seal" } },
513
+ },
514
+ });
515
+ ```
516
+
517
+ ---
518
+
472
519
  ### Filter on related nodes
473
520
 
474
521
  Filter the root view based on properties of a direct or nested relation. This uses Cognite nested filters under the hood.
@@ -912,15 +959,18 @@ See [Count assets by source ID](#count-assets-by-source-id), [List distinct sour
912
959
 
913
960
  | Type | Operators |
914
961
  |------|-----------|
915
- | `string` | `eq`, `in`, `prefix`, `exists` |
962
+ | `string` | `eq`, `in`, `prefix`, `search`, `exists` |
916
963
  | `number` | `eq`, `in`, `gt`, `gte`, `lt`, `lte`, `exists` |
917
964
  | `boolean` | `eq`, `exists` |
918
965
  | timestamp / `Date` | `eq`, `in`, `gt`, `gte`, `lt`, `lte`, `exists` — use ISO strings or `Date` values (coerced to ISO) |
919
966
  | `NodeId` | `eq`, `in`, `exists` |
967
+ | `string[]` | `containsAny`, `containsAll`, `search`, `exists` |
920
968
  | `T[]` | `containsAny`, `containsAll`, `exists` |
921
969
 
922
970
  Logical combinators `AND`, `OR`, and `NOT` are supported at any nesting level, including inside nested relation filters (e.g. `parent: { OR: [...] }`).
923
971
 
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.
973
+
924
974
  ### Relation traversal
925
975
 
926
976
  - **Direct relations** — `parent`, `asset`, `unit` (outwards from the current node). List relations such as `path` return arrays when expanded.
package/dist/index.cjs CHANGED
@@ -28,6 +28,13 @@ var CogniteSdkAdapter = class {
28
28
  nextCursor: response.nextCursor
29
29
  };
30
30
  }
31
+ async searchInstances(request) {
32
+ const search = this.client.instances.search;
33
+ const response = await search(request);
34
+ return {
35
+ items: response.items
36
+ };
37
+ }
31
38
  async aggregateInstances(request) {
32
39
  const response = await this.client.instances.aggregate(
33
40
  request
@@ -186,6 +193,7 @@ var leafOps = /* @__PURE__ */ new Set([
186
193
  "lte",
187
194
  "exists",
188
195
  "prefix",
196
+ "search",
189
197
  "containsAny",
190
198
  "containsAll"
191
199
  ]);
@@ -238,19 +246,33 @@ function leafFilterSchema(property) {
238
246
  const value = baseValueSchema(property);
239
247
  const isList = typeof property !== "string" && property.type.list === true;
240
248
  if (isList) {
241
- return zod.z.object({
249
+ const shape = {
242
250
  containsAny: zod.z.array(value).optional(),
243
251
  containsAll: zod.z.array(value).optional(),
244
252
  exists: zod.z.boolean().optional()
245
- }).strict();
253
+ };
254
+ if (property.type.type === "text") {
255
+ shape.search = zod.z.object({
256
+ query: zod.z.string(),
257
+ operator: zod.z.enum(["OR", "AND"]).optional()
258
+ }).strict().optional();
259
+ }
260
+ return zod.z.object(shape).strict();
246
261
  }
247
262
  if (property === "node-string" || typeof property !== "string" && property.type.type === "text") {
248
- return zod.z.object({
263
+ const shape = {
249
264
  eq: zod.z.string().optional(),
250
265
  in: zod.z.array(zod.z.string()).optional(),
251
266
  prefix: zod.z.string().optional(),
252
267
  exists: zod.z.boolean().optional()
253
- }).strict();
268
+ };
269
+ if (property !== "node-string") {
270
+ shape.search = zod.z.object({
271
+ query: zod.z.string(),
272
+ operator: zod.z.enum(["OR", "AND"]).optional()
273
+ }).strict().optional();
274
+ }
275
+ return zod.z.object(shape).strict();
254
276
  }
255
277
  if (typeof property !== "string" && property.type.type === "enum") {
256
278
  return zod.z.object({
@@ -610,11 +632,13 @@ function isLeafFilter2(value) {
610
632
  return Object.keys(value).some((k) => LEAF_OPS.has(k));
611
633
  }
612
634
  var FilterMapper = class {
613
- constructor(viewMapper) {
635
+ constructor(viewMapper, cognite) {
614
636
  this.viewMapper = viewMapper;
637
+ this.cognite = cognite;
615
638
  }
616
639
  async map(input, rootView) {
617
640
  const result = [];
641
+ const searchQueries = {};
618
642
  for (const [key, value] of Object.entries(input)) {
619
643
  if (value == null) continue;
620
644
  if (key === "AND") {
@@ -635,17 +659,44 @@ var FilterMapper = class {
635
659
  } else {
636
660
  const filterValue = value;
637
661
  const property = getPropertyRef(key, rootView);
638
- if (isLeafFilter2(filterValue)) {
662
+ const hasLeafFilter = isLeafFilter2(filterValue);
663
+ if (hasLeafFilter) {
639
664
  result.push(...this.leafToFilterDefs(property, filterValue));
640
- } else {
665
+ }
666
+ if ("search" in filterValue && filterValue.search != null) {
667
+ searchQueries[key] = filterValue.search;
668
+ } else if (!hasLeafFilter) {
641
669
  const targetView = await this.getNestedTargetView(key, rootView);
642
670
  const innerFilter = await this.whereInputToSingle(filterValue, targetView);
643
671
  result.push({ nested: { scope: property, filter: innerFilter } });
644
672
  }
645
673
  }
646
674
  }
675
+ if (Object.keys(searchQueries).length > 0) {
676
+ const searchFilters = await Promise.all(
677
+ Object.entries(searchQueries).map(
678
+ ([key, filterValue]) => this.searchToFilterDef(key, filterValue, rootView)
679
+ )
680
+ );
681
+ result.push(...searchFilters);
682
+ }
647
683
  return result;
648
684
  }
685
+ async searchToFilterDef(propertyName, search, rootView) {
686
+ const response = await this.cognite.searchInstances({
687
+ view: toViewReference(rootView),
688
+ query: search.query,
689
+ instanceType: "node",
690
+ properties: [propertyName],
691
+ operator: search.operator ?? "OR",
692
+ limit: 1e3
693
+ });
694
+ const instanceRefs = response.items.map((item) => ({
695
+ space: item.space,
696
+ externalId: item.externalId
697
+ }));
698
+ return { instanceReferences: instanceRefs };
699
+ }
649
700
  async whereInputToSingle(input, rootView) {
650
701
  const filters = await this.map(input, rootView);
651
702
  const [firstFilter, ...restFilters] = filters;
@@ -724,9 +775,9 @@ var FilterMapper = class {
724
775
 
725
776
  // src/mappers/aggregate-mapper.ts
726
777
  var AggregateMapper = class {
727
- constructor(viewMapper) {
778
+ constructor(viewMapper, cognite) {
728
779
  this.viewMapper = viewMapper;
729
- this.filterMapper = new FilterMapper(viewMapper);
780
+ this.filterMapper = new FilterMapper(viewMapper, cognite);
730
781
  this.validator = new AggregateValidator(viewMapper);
731
782
  }
732
783
  async map(options) {
@@ -821,9 +872,9 @@ var SortMapper = class {
821
872
 
822
873
  // src/mappers/query-mapper.ts
823
874
  var QueryMapper = class {
824
- constructor(viewMapper) {
875
+ constructor(viewMapper, cognite) {
825
876
  this.viewMapper = viewMapper;
826
- this.filterMapper = new FilterMapper(viewMapper);
877
+ this.filterMapper = new FilterMapper(viewMapper, cognite);
827
878
  this.sortMapper = new SortMapper();
828
879
  this.validator = new QueryValidator(viewMapper);
829
880
  }
@@ -1369,8 +1420,8 @@ var IndustrialModelClient = class {
1369
1420
  const cognite = createCogniteAdapter(client);
1370
1421
  this.cognite = cognite;
1371
1422
  const viewMapper = new ViewMapper(cognite, dataModelId);
1372
- this.queryMapper = new QueryMapper(viewMapper);
1373
- this.aggregateMapper = new AggregateMapper(viewMapper);
1423
+ this.queryMapper = new QueryMapper(viewMapper, cognite);
1424
+ this.aggregateMapper = new AggregateMapper(viewMapper, cognite);
1374
1425
  this.aggregateResultMapper = new AggregateResultMapper();
1375
1426
  this.resultMapper = new QueryResultMapper(viewMapper);
1376
1427
  this.resultValidator = new QueryResultValidator(viewMapper);