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