industrial-model 0.1.0 → 0.3.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/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  // src/cognite/adapter.ts
2
4
  function createCogniteAdapter(client) {
3
5
  return new CogniteSdkAdapter(client);
@@ -202,11 +204,350 @@ var FilterMapper = class {
202
204
  return value;
203
205
  }
204
206
  };
207
+ var nodeIdSchema = z.object({
208
+ space: z.string().min(1),
209
+ externalId: z.string().min(1)
210
+ });
211
+ function dateSchema(dateMode) {
212
+ if (dateMode === "coerce") {
213
+ return z.preprocess(
214
+ (value) => typeof value === "string" || typeof value === "number" ? new Date(value) : value,
215
+ z.date()
216
+ );
217
+ }
218
+ return z.union([z.string(), z.date()]);
219
+ }
220
+ function propertyValueSchema(property, options = {}) {
221
+ const type = property.type;
222
+ let schema;
223
+ switch (type.type) {
224
+ case "text":
225
+ case "enum":
226
+ schema = z.string();
227
+ break;
228
+ case "int32":
229
+ case "int64":
230
+ schema = z.number().int();
231
+ break;
232
+ case "float32":
233
+ case "float64":
234
+ schema = z.number();
235
+ break;
236
+ case "boolean":
237
+ schema = z.boolean();
238
+ break;
239
+ case "date":
240
+ case "timestamp":
241
+ schema = dateSchema(options.dateMode);
242
+ break;
243
+ case "direct":
244
+ schema = nodeIdSchema;
245
+ break;
246
+ case "json":
247
+ schema = z.unknown();
248
+ break;
249
+ default:
250
+ schema = z.unknown();
251
+ break;
252
+ }
253
+ return type.list === true ? z.array(schema) : schema;
254
+ }
255
+ function buildViewSchema(view, options = {}) {
256
+ const shape = {};
257
+ for (const [name, property] of Object.entries(view.properties)) {
258
+ if (isViewPropertyDefinition(property)) {
259
+ shape[name] = propertyValueSchema(property, options).optional();
260
+ }
261
+ }
262
+ return z.object(shape).strict();
263
+ }
264
+
265
+ // src/mappers/query-validator.ts
266
+ var NODE_STRING_PROPERTIES = ["externalId", "space"];
267
+ var NODE_NUMBER_PROPERTIES = ["createdTime", "deletedTime", "lastUpdatedTime"];
268
+ var NODE_PROPERTIES2 = /* @__PURE__ */ new Set([...NODE_STRING_PROPERTIES, ...NODE_NUMBER_PROPERTIES]);
269
+ var SORT_DIRECTION_SCHEMA = z.enum(["ascending", "descending"]);
270
+ var recordSchema = z.record(z.string(), z.unknown());
271
+ var leafOps = /* @__PURE__ */ new Set([
272
+ "eq",
273
+ "in",
274
+ "gt",
275
+ "gte",
276
+ "lt",
277
+ "lte",
278
+ "exists",
279
+ "prefix",
280
+ "containsAny",
281
+ "containsAll"
282
+ ]);
283
+ function isRecord(value) {
284
+ return value != null && typeof value === "object" && !Array.isArray(value);
285
+ }
286
+ function isLeafFilter2(value) {
287
+ return Object.keys(value).some((key) => leafOps.has(key));
288
+ }
289
+ function issuePath(path) {
290
+ return path.length === 0 ? "query" : path.map(String).join(".");
291
+ }
292
+ function formatZodIssues(error, path) {
293
+ return error.issues.map((issue) => `${issuePath([...path, ...issue.path])}: ${issue.message}`);
294
+ }
295
+ function getRelationTarget(property) {
296
+ if (isViewPropertyDefinition(property)) {
297
+ return getDirectRelationSource(property)?.externalId ?? null;
298
+ }
299
+ if (isReverseDirectRelation(property) || isEdgeConnection(property)) {
300
+ return property.source.externalId;
301
+ }
302
+ return null;
303
+ }
304
+ function baseValueSchema(property) {
305
+ if (property === "node-string") return z.string();
306
+ if (property === "node-number") return z.number();
307
+ switch (property.type.type) {
308
+ case "text":
309
+ case "enum":
310
+ return z.string();
311
+ case "int32":
312
+ case "int64":
313
+ return z.number().int();
314
+ case "float32":
315
+ case "float64":
316
+ return z.number();
317
+ case "boolean":
318
+ return z.boolean();
319
+ case "date":
320
+ case "timestamp":
321
+ return z.union([z.string(), z.date()]);
322
+ case "direct":
323
+ return nodeIdSchema;
324
+ default:
325
+ return z.union([z.string(), z.number(), z.boolean()]);
326
+ }
327
+ }
328
+ function leafFilterSchema(property) {
329
+ const value = baseValueSchema(property);
330
+ const isList = typeof property !== "string" && property.type.list === true;
331
+ if (isList) {
332
+ return z.object({
333
+ containsAny: z.array(value).optional(),
334
+ containsAll: z.array(value).optional(),
335
+ exists: z.boolean().optional()
336
+ }).strict();
337
+ }
338
+ if (property === "node-string" || typeof property !== "string" && property.type.type === "text") {
339
+ return z.object({
340
+ eq: z.string().optional(),
341
+ in: z.array(z.string()).optional(),
342
+ prefix: z.string().optional(),
343
+ exists: z.boolean().optional()
344
+ }).strict();
345
+ }
346
+ if (typeof property !== "string" && property.type.type === "enum") {
347
+ return z.object({
348
+ eq: z.string().optional(),
349
+ in: z.array(z.string()).optional(),
350
+ prefix: z.string().optional(),
351
+ exists: z.boolean().optional()
352
+ }).strict();
353
+ }
354
+ if (property === "node-number" || typeof property !== "string" && ["int32", "int64", "float32", "float64"].includes(property.type.type ?? "")) {
355
+ return z.object({
356
+ eq: value.optional(),
357
+ in: z.array(value).optional(),
358
+ gt: value.optional(),
359
+ gte: value.optional(),
360
+ lt: value.optional(),
361
+ lte: value.optional(),
362
+ exists: z.boolean().optional()
363
+ }).strict();
364
+ }
365
+ if (typeof property !== "string" && ["date", "timestamp"].includes(property.type.type ?? "")) {
366
+ return z.object({
367
+ eq: value.optional(),
368
+ in: z.array(value).optional(),
369
+ gt: value.optional(),
370
+ gte: value.optional(),
371
+ lt: value.optional(),
372
+ lte: value.optional(),
373
+ exists: z.boolean().optional()
374
+ }).strict();
375
+ }
376
+ if (typeof property !== "string" && property.type.type === "boolean") {
377
+ return z.object({
378
+ eq: z.boolean().optional(),
379
+ exists: z.boolean().optional()
380
+ }).strict();
381
+ }
382
+ if (typeof property !== "string" && property.type.type === "direct") {
383
+ return z.object({
384
+ eq: nodeIdSchema.optional(),
385
+ in: z.array(nodeIdSchema).optional(),
386
+ exists: z.boolean().optional()
387
+ }).strict();
388
+ }
389
+ return z.object({
390
+ eq: value.optional(),
391
+ in: z.array(value).optional(),
392
+ exists: z.boolean().optional()
393
+ }).strict();
394
+ }
395
+ var QueryValidator = class {
396
+ constructor(viewMapper) {
397
+ this.viewMapper = viewMapper;
398
+ }
399
+ async validate(options, rootView) {
400
+ const errors = [];
401
+ errors.push(...this.validateOptionsShape(options, rootView));
402
+ if (options.select !== void 0) {
403
+ errors.push(...await this.validateSelect(options.select, rootView, ["select"]));
404
+ }
405
+ if (options.filters !== void 0) {
406
+ errors.push(...await this.validateFilters(options.filters, rootView, ["filters"]));
407
+ }
408
+ if (options.sort !== void 0) {
409
+ errors.push(...this.validateSort(options.sort, rootView, ["sort"]));
410
+ }
411
+ if (errors.length > 0) {
412
+ throw new Error(`Invalid query options:
413
+ ${errors.map((error) => `- ${error}`).join("\n")}`);
414
+ }
415
+ }
416
+ validateOptionsShape(options, rootView) {
417
+ const schema = z.object({
418
+ viewExternalId: z.literal(rootView.externalId),
419
+ select: z.unknown().optional(),
420
+ filters: z.unknown().optional(),
421
+ sort: z.unknown().optional(),
422
+ limit: z.union([z.literal(-1), z.number().int().positive().max(MAX_LIMIT)]).optional(),
423
+ cursor: z.string().nullable().optional()
424
+ }).strict();
425
+ const result = schema.safeParse(options);
426
+ return result.success ? [] : formatZodIssues(result.error, []);
427
+ }
428
+ async validateSelect(select, view, path) {
429
+ const shape = {
430
+ _all: z.literal(true).optional()
431
+ };
432
+ for (const property of NODE_PROPERTIES2) {
433
+ shape[property] = z.boolean().optional();
434
+ }
435
+ for (const [name, property] of Object.entries(view.properties)) {
436
+ const target = getRelationTarget(property);
437
+ if (target != null) {
438
+ const nestedSelect = recordSchema;
439
+ shape[name] = isViewPropertyDefinition(property) ? z.union([z.boolean(), nestedSelect]).optional() : nestedSelect.optional();
440
+ } else {
441
+ shape[name] = z.boolean().optional();
442
+ }
443
+ }
444
+ const result = z.object(shape).strict().safeParse(select);
445
+ if (!result.success) return formatZodIssues(result.error, path);
446
+ if (!isRecord(select)) return [];
447
+ const errors = [];
448
+ for (const [name, value] of Object.entries(select)) {
449
+ if (name === "_all" || value == null || typeof value !== "object" || Array.isArray(value)) {
450
+ continue;
451
+ }
452
+ const property = view.properties[name];
453
+ if (!property) continue;
454
+ const target = getRelationTarget(property);
455
+ if (target == null) {
456
+ errors.push(
457
+ `${issuePath([...path, name])}: property "${name}" does not support nested select`
458
+ );
459
+ continue;
460
+ }
461
+ const targetView = await this.viewMapper.getView(target);
462
+ errors.push(...await this.validateSelect(value, targetView, [...path, name]));
463
+ }
464
+ return errors;
465
+ }
466
+ async validateFilters(filters, view, path) {
467
+ const shape = {
468
+ AND: z.union([recordSchema, z.array(recordSchema)]).optional(),
469
+ OR: z.array(recordSchema).optional(),
470
+ NOT: z.union([recordSchema, z.array(recordSchema)]).optional()
471
+ };
472
+ for (const property of NODE_STRING_PROPERTIES) {
473
+ shape[property] = z.unknown().optional();
474
+ }
475
+ for (const property of NODE_NUMBER_PROPERTIES) {
476
+ shape[property] = z.unknown().optional();
477
+ }
478
+ for (const property of Object.keys(view.properties)) {
479
+ shape[property] = z.unknown().optional();
480
+ }
481
+ const result = z.object(shape).strict().safeParse(filters);
482
+ if (!result.success) return formatZodIssues(result.error, path);
483
+ if (!isRecord(filters)) return [];
484
+ const errors = [];
485
+ for (const [name, value] of Object.entries(filters)) {
486
+ if (value == null) continue;
487
+ if (name === "AND" || name === "OR" || name === "NOT") {
488
+ const clauses = Array.isArray(value) ? value : [value];
489
+ for (const [index, clause] of clauses.entries()) {
490
+ errors.push(...await this.validateFilters(clause, view, [...path, name, index]));
491
+ }
492
+ continue;
493
+ }
494
+ if (!isRecord(value)) {
495
+ errors.push(`${issuePath([...path, name])}: Expected object`);
496
+ continue;
497
+ }
498
+ const nodePropertyType = NODE_STRING_PROPERTIES.includes(
499
+ name
500
+ ) ? "node-string" : NODE_NUMBER_PROPERTIES.includes(name) ? "node-number" : null;
501
+ if (nodePropertyType != null) {
502
+ errors.push(...this.validateLeafFilter(value, nodePropertyType, [...path, name]));
503
+ continue;
504
+ }
505
+ const property = view.properties[name];
506
+ if (!property) continue;
507
+ if (isViewPropertyDefinition(property)) {
508
+ const target2 = getDirectRelationSource(property);
509
+ if (target2 != null && !isLeafFilter2(value)) {
510
+ const targetView = await this.viewMapper.getView(target2.externalId);
511
+ errors.push(...await this.validateFilters(value, targetView, [...path, name]));
512
+ } else {
513
+ errors.push(...this.validateLeafFilter(value, property, [...path, name]));
514
+ }
515
+ continue;
516
+ }
517
+ const target = getRelationTarget(property);
518
+ if (target == null) {
519
+ errors.push(`${issuePath([...path, name])}: property "${name}" does not support filters`);
520
+ continue;
521
+ }
522
+ errors.push(
523
+ `${issuePath([...path, name])}: filtering through "${name}" is not supported by the query mapper`
524
+ );
525
+ }
526
+ return errors;
527
+ }
528
+ validateLeafFilter(value, property, path) {
529
+ const result = leafFilterSchema(property).safeParse(value);
530
+ return result.success ? [] : formatZodIssues(result.error, path);
531
+ }
532
+ validateSort(sort, view, path) {
533
+ const shape = {};
534
+ for (const property of NODE_PROPERTIES2) {
535
+ shape[property] = SORT_DIRECTION_SCHEMA.optional();
536
+ }
537
+ for (const [name, property] of Object.entries(view.properties)) {
538
+ if (isViewPropertyDefinition(property) && property.type.list !== true) {
539
+ shape[name] = SORT_DIRECTION_SCHEMA.optional();
540
+ }
541
+ }
542
+ const result = z.object(shape).strict().safeParse(sort);
543
+ return result.success ? [] : formatZodIssues(result.error, path);
544
+ }
545
+ };
205
546
 
206
547
  // src/mappers/sort-mapper.ts
207
548
  var SortMapper = class {
208
- map(sortClauses, rootView) {
209
- return Object.entries(sortClauses).map(([property, direction]) => ({
549
+ map(sort, rootView) {
550
+ return Object.entries(sort).map(([property, direction]) => ({
210
551
  property: getPropertyRef(property, rootView),
211
552
  direction,
212
553
  nullsFirst: this.isNullsFirst(property, rootView, direction)
@@ -227,18 +568,20 @@ var QueryMapper = class {
227
568
  this.viewMapper = viewMapper;
228
569
  this.filterMapper = new FilterMapper(viewMapper);
229
570
  this.sortMapper = new SortMapper();
571
+ this.validator = new QueryValidator(viewMapper);
230
572
  }
231
573
  async map(options) {
232
574
  const {
233
575
  viewExternalId,
234
576
  select = { _all: true },
235
577
  filters,
236
- sortClauses = {},
578
+ sort = {},
237
579
  limit: requestedLimit = DEFAULT_LIMIT,
238
580
  cursor = null
239
581
  } = options;
240
582
  const limit = requestedLimit === -1 ? DEFAULT_LIMIT : requestedLimit;
241
583
  const rootView = await this.viewMapper.getView(viewExternalId);
584
+ await this.validator.validate(options, rootView);
242
585
  const rootViewRef = toViewReference(rootView);
243
586
  const whereFilters = filters ? await this.filterMapper.map(filters, rootView) : [];
244
587
  const baseFilters = [{ hasData: [rootViewRef] }, ...whereFilters];
@@ -247,7 +590,7 @@ var QueryMapper = class {
247
590
  nodes: {
248
591
  filter: { and: baseFilters }
249
592
  },
250
- sort: this.sortMapper.map(sortClauses, rootView),
593
+ sort: this.sortMapper.map(sort, rootView),
251
594
  limit
252
595
  }
253
596
  };
@@ -529,6 +872,92 @@ var QueryResultMapper = class {
529
872
  return entry;
530
873
  }
531
874
  };
875
+ var nodeMetadataSchema = {
876
+ instanceType: z.literal("node").optional(),
877
+ space: z.string(),
878
+ externalId: z.string(),
879
+ version: z.number().optional(),
880
+ createdTime: z.number().optional(),
881
+ deletedTime: z.number().optional(),
882
+ lastUpdatedTime: z.number().optional(),
883
+ _edges: z.record(z.string(), z.unknown()).optional()
884
+ };
885
+ function isListRelation(property) {
886
+ if (isViewPropertyDefinition(property)) {
887
+ return isListDirectRelation(property);
888
+ }
889
+ if (isReverseDirectRelation(property)) {
890
+ return property.connectionType === "multi_reverse_direct_relation" || property.targetsList === true;
891
+ }
892
+ return isEdgeConnection(property);
893
+ }
894
+ function isRecord2(value) {
895
+ return value != null && typeof value === "object" && !Array.isArray(value);
896
+ }
897
+ var QueryResultValidator = class {
898
+ constructor(viewMapper) {
899
+ this.viewMapper = viewMapper;
900
+ }
901
+ async parseItems(rootViewExternalId, items, select) {
902
+ const rootView = await this.viewMapper.getView(rootViewExternalId);
903
+ const schema = await this.buildResultSchema(rootView, MAX_DEPENDENCY_DEPTH, select);
904
+ const result = z.array(schema).safeParse(items);
905
+ if (!result.success) {
906
+ throw new Error(
907
+ `Invalid query result:
908
+ ${result.error.issues.map((issue) => `- ${issue.path.map(String).join(".")}: ${issue.message}`).join("\n")}`
909
+ );
910
+ }
911
+ return result.data;
912
+ }
913
+ async buildResultSchema(view, remainingDepth, select) {
914
+ const shape = { ...nodeMetadataSchema };
915
+ const includeAllProperties = select == null || select._all === true;
916
+ for (const [name, property] of Object.entries(view.properties)) {
917
+ const isSelected = includeAllProperties || name in select;
918
+ if (!isSelected) continue;
919
+ const nestedSelect = isRecord2(select?.[name]) ? select[name] : void 0;
920
+ if (isViewPropertyDefinition(property)) {
921
+ const relationSource = getDirectRelationSource(property);
922
+ if (relationSource) {
923
+ shape[name] = await this.buildRelationSchema(
924
+ property,
925
+ relationSource.externalId,
926
+ remainingDepth,
927
+ nestedSelect
928
+ );
929
+ } else {
930
+ shape[name] = propertyValueSchema(property, { dateMode: "coerce" }).optional();
931
+ }
932
+ continue;
933
+ }
934
+ if (isReverseDirectRelation(property) || isEdgeConnection(property)) {
935
+ shape[name] = await this.buildRelationSchema(
936
+ property,
937
+ property.source.externalId,
938
+ remainingDepth,
939
+ nestedSelect
940
+ );
941
+ }
942
+ }
943
+ const schema = z.object(shape);
944
+ return includeAllProperties ? schema.strict() : schema;
945
+ }
946
+ async buildRelationSchema(property, targetViewExternalId, remainingDepth, select) {
947
+ const isList = isListRelation(property);
948
+ const fallbackSchema = isViewPropertyDefinition(property) ? propertyValueSchema(property, { dateMode: "coerce" }) : z.unknown();
949
+ if (remainingDepth <= 0 || select == null) {
950
+ return fallbackSchema.optional();
951
+ }
952
+ const targetView = await this.viewMapper.getView(targetViewExternalId);
953
+ const nestedSchema = await this.buildResultSchema(targetView, remainingDepth - 1, select);
954
+ if (isViewPropertyDefinition(property)) {
955
+ const nestedRelationSchema = isList ? z.array(nestedSchema) : nestedSchema;
956
+ return z.union([nestedRelationSchema, fallbackSchema]).optional();
957
+ }
958
+ return (isList ? z.array(nestedSchema) : nestedSchema).optional();
959
+ }
960
+ };
532
961
 
533
962
  // src/mappers/view-mapper.ts
534
963
  var ViewMapper = class {
@@ -678,15 +1107,21 @@ function buildDependenciesQuery(previousQuery, nodesParent, nodesChildren, leafC
678
1107
  }
679
1108
 
680
1109
  // src/client.ts
681
- var IndustrialModel = class {
682
- constructor(client, dataModelId) {
1110
+ var IndustrialModelClient = class {
1111
+ constructor(client, dataModelId, options = {}) {
683
1112
  const cognite = createCogniteAdapter(client);
684
1113
  this.cognite = cognite;
685
1114
  const viewMapper = new ViewMapper(cognite, dataModelId);
686
1115
  this.queryMapper = new QueryMapper(viewMapper);
687
1116
  this.resultMapper = new QueryResultMapper(viewMapper);
1117
+ this.resultValidator = new QueryResultValidator(viewMapper);
1118
+ this.validateResults = options.validateResults ?? false;
688
1119
  }
689
- async query(options) {
1120
+ query() {
1121
+ const execute = (options) => this.queryInternal(options);
1122
+ return execute;
1123
+ }
1124
+ async queryInternal(options) {
690
1125
  const { viewExternalId, limit = DEFAULT_LIMIT } = options;
691
1126
  const allPages = options.limit === -1;
692
1127
  const cogniteQuery = await this.queryMapper.map(options);
@@ -702,11 +1137,12 @@ var IndustrialModel = class {
702
1137
  mapNodesAndEdges(queryResult),
703
1138
  dependenciesData
704
1139
  );
705
- const pageResult = await this.resultMapper.mapNodes(viewExternalId, queryResultData);
1140
+ const mappedPageResult = await this.resultMapper.mapNodes(viewExternalId, queryResultData);
1141
+ const pageResult = this.validateResults ? await this.resultValidator.parseItems(viewExternalId, mappedPageResult, options.select) : mappedPageResult;
706
1142
  const nextCursor = queryResult.nextCursor[viewExternalId] ?? null;
707
- data.push(...pageResult);
708
1143
  const isLastPage = pageResult.length < limit || !nextCursor;
709
1144
  const resolvedCursor = isLastPage ? null : nextCursor;
1145
+ data.push(...pageResult);
710
1146
  if (!isLastPage && resolvedCursor !== null) {
711
1147
  cogniteQuery.cursors = { [viewExternalId]: resolvedCursor };
712
1148
  }
@@ -733,6 +1169,6 @@ var IndustrialModel = class {
733
1169
  }
734
1170
  };
735
1171
 
736
- export { IndustrialModel };
1172
+ export { IndustrialModelClient, buildViewSchema, nodeIdSchema };
737
1173
  //# sourceMappingURL=index.js.map
738
1174
  //# sourceMappingURL=index.js.map