industrial-model 0.6.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/dist/index.cjs CHANGED
@@ -43,6 +43,13 @@ var CogniteSdkAdapter = class {
43
43
  items: response.items
44
44
  };
45
45
  }
46
+ async applyInstances(request) {
47
+ const apply = this.client.instances.apply;
48
+ const response = await apply(request);
49
+ return {
50
+ items: response.items
51
+ };
52
+ }
46
53
  };
47
54
 
48
55
  // src/constants.ts
@@ -796,6 +803,85 @@ ${result.error.issues.map((issue) => `- ${issue.path.map(String).join(".")}: ${i
796
803
  return (isList ? zod.z.array(nestedSchema) : nestedSchema).optional();
797
804
  }
798
805
  };
806
+ var strictNodeIdSchema = zod.z.object({
807
+ space: zod.z.string().min(1),
808
+ externalId: zod.z.string().min(1)
809
+ }).strict();
810
+ var nodeIdLikeSchema = zod.z.object({
811
+ space: zod.z.string().min(1),
812
+ externalId: zod.z.string().min(1)
813
+ }).loose();
814
+ var optionsSchema = zod.z.object({
815
+ viewExternalId: zod.z.string().min(1),
816
+ items: zod.z.array(zod.z.record(zod.z.string(), zod.z.unknown())),
817
+ onEdgeCreation: zod.z.record(zod.z.string(), zod.z.function()).optional(),
818
+ replace: zod.z.boolean().optional(),
819
+ edgeMode: zod.z.enum(["append", "replace"]).optional()
820
+ }).strict();
821
+ function issuePath3(path) {
822
+ return path.length === 0 ? "upsert" : path.map(String).join(".");
823
+ }
824
+ function formatZodIssues3(error, path) {
825
+ return error.issues.map((issue) => `${issuePath3([...path, ...issue.path])}: ${issue.message}`);
826
+ }
827
+ function relationValueSchema(property) {
828
+ if (isReverseDirectRelation(property) && property.targetsList === true) {
829
+ return zod.z.never();
830
+ }
831
+ return zod.z.union([nodeIdLikeSchema, zod.z.array(nodeIdLikeSchema)]);
832
+ }
833
+ var UpsertValidator = class {
834
+ validate(options, rootView) {
835
+ const errors = [];
836
+ const optionsResult = optionsSchema.safeParse(options);
837
+ if (!optionsResult.success) {
838
+ errors.push(...formatZodIssues3(optionsResult.error, []));
839
+ }
840
+ if (options.viewExternalId !== rootView.externalId) {
841
+ errors.push(
842
+ `viewExternalId: expected "${rootView.externalId}", received "${options.viewExternalId}"`
843
+ );
844
+ }
845
+ for (const [index, item] of options.items.entries()) {
846
+ errors.push(
847
+ ...this.validateItem(item, rootView, ["items", index])
848
+ );
849
+ }
850
+ if (errors.length > 0) {
851
+ throw new Error(`Invalid upsert options:
852
+ ${errors.map((error) => `- ${error}`).join("\n")}`);
853
+ }
854
+ }
855
+ validateItem(item, view, path) {
856
+ const errors = [];
857
+ const identityResult = strictNodeIdSchema.safeParse({
858
+ space: item.space,
859
+ externalId: item.externalId
860
+ });
861
+ if (!identityResult.success) {
862
+ errors.push(...formatZodIssues3(identityResult.error, path));
863
+ }
864
+ for (const [name, value] of Object.entries(item)) {
865
+ if (name === "space" || name === "externalId") continue;
866
+ const property = view.properties[name];
867
+ if (!property) {
868
+ errors.push(`${issuePath3([...path, name])}: unknown view property`);
869
+ continue;
870
+ }
871
+ if (isViewPropertyDefinition(property)) {
872
+ const schema = property.type.type === "direct" ? property.type.list === true ? zod.z.array(nodeIdLikeSchema) : nodeIdLikeSchema : propertyValueSchema(property);
873
+ const result = schema.safeParse(value);
874
+ if (!result.success) errors.push(...formatZodIssues3(result.error, [...path, name]));
875
+ continue;
876
+ }
877
+ if (isReverseDirectRelation(property) || isEdgeConnection(property)) {
878
+ const result = relationValueSchema(property).safeParse(value);
879
+ if (!result.success) errors.push(...formatZodIssues3(result.error, [...path, name]));
880
+ }
881
+ }
882
+ return errors;
883
+ }
884
+ };
799
885
 
800
886
  // src/mappers/filter-mapper.ts
801
887
  var LEAF_OPS = /* @__PURE__ */ new Set([
@@ -1363,6 +1449,216 @@ var QueryResultMapper = class {
1363
1449
  }
1364
1450
  };
1365
1451
 
1452
+ // src/mappers/upsert-mapper.ts
1453
+ var IDENTITY_KEYS = /* @__PURE__ */ new Set(["space", "externalId"]);
1454
+ var EDGE_QUERY_LIMIT = 1e3;
1455
+ var UpsertMapper = class {
1456
+ constructor(viewMapper, cognite) {
1457
+ this.viewMapper = viewMapper;
1458
+ this.cognite = cognite;
1459
+ this.validator = new UpsertValidator();
1460
+ }
1461
+ async map(options) {
1462
+ const rootView = await this.viewMapper.getView(options.viewExternalId);
1463
+ this.validator.validate(options, rootView);
1464
+ const edgeMode = options.edgeMode ?? "append";
1465
+ const mappedItems = options.items.map(
1466
+ (item) => this.mapItem(item, rootView, options.onEdgeCreation, edgeMode)
1467
+ );
1468
+ const items = mappedItems.flatMap((item) => item.writes);
1469
+ const edgeReplacements = mappedItems.flatMap((item) => item.edgeReplacements);
1470
+ const deleteItems = edgeMode === "replace" ? await this.mapEdgeReplacementDeletes(edgeReplacements) : [];
1471
+ return {
1472
+ items,
1473
+ ...deleteItems.length > 0 ? { delete: deleteItems } : {},
1474
+ ...options.replace === true ? { replace: true } : {}
1475
+ };
1476
+ }
1477
+ mapItem(item, rootView, onEdgeCreation, edgeMode) {
1478
+ const node = { space: item.space, externalId: item.externalId };
1479
+ const nodeProperties = {};
1480
+ const inferredItems = [];
1481
+ const edgeReplacements = [];
1482
+ for (const [name, value] of Object.entries(item)) {
1483
+ if (IDENTITY_KEYS.has(name)) continue;
1484
+ const property = rootView.properties[name];
1485
+ if (!property) continue;
1486
+ if (isViewPropertyDefinition(property)) {
1487
+ nodeProperties[name] = normalizeViewPropertyValue(value, property);
1488
+ } else if (isReverseDirectRelation(property)) {
1489
+ inferredItems.push(...this.mapReverseDirectRelation(node, value, property));
1490
+ } else if (isEdgeConnection(property)) {
1491
+ const desiredEdges = this.mapEdgeConnection(node, name, value, property, onEdgeCreation);
1492
+ inferredItems.push(...desiredEdges);
1493
+ if (edgeMode === "replace") {
1494
+ edgeReplacements.push({ rootNode: node, propertyName: name, property, desiredEdges });
1495
+ }
1496
+ }
1497
+ }
1498
+ const applyNode = {
1499
+ instanceType: "node",
1500
+ ...node
1501
+ };
1502
+ if (Object.keys(nodeProperties).length > 0) {
1503
+ applyNode.sources = [{ source: toViewReference(rootView), properties: nodeProperties }];
1504
+ }
1505
+ return { writes: [applyNode, ...inferredItems], edgeReplacements };
1506
+ }
1507
+ async mapEdgeReplacementDeletes(replacements) {
1508
+ const deletes = await Promise.all(
1509
+ replacements.map(async (replacement) => {
1510
+ const existingEdges = await this.queryExistingEdges(replacement);
1511
+ const desiredEdgeKeys = new Set(replacement.desiredEdges.map((edge) => instanceKey(edge)));
1512
+ return existingEdges.filter((edge) => !desiredEdgeKeys.has(instanceKey(edge))).map((edge) => ({
1513
+ instanceType: "edge",
1514
+ space: edge.space,
1515
+ externalId: edge.externalId
1516
+ }));
1517
+ })
1518
+ );
1519
+ return uniqueDeletes(deletes.flat());
1520
+ }
1521
+ async queryExistingEdges(replacement) {
1522
+ const rootKey = `${replacement.propertyName}Root`;
1523
+ const edgeKey = `${replacement.propertyName}Edges`;
1524
+ const direction = replacement.property.direction ?? "outwards";
1525
+ const query = {
1526
+ with: {
1527
+ [rootKey]: {
1528
+ nodes: {
1529
+ filter: {
1530
+ instanceReferences: [replacement.rootNode]
1531
+ }
1532
+ },
1533
+ limit: 1
1534
+ },
1535
+ [edgeKey]: {
1536
+ edges: {
1537
+ from: rootKey,
1538
+ maxDistance: 1,
1539
+ direction,
1540
+ filter: {
1541
+ equals: { property: ["edge", "type"], value: replacement.property.type }
1542
+ }
1543
+ },
1544
+ limit: EDGE_QUERY_LIMIT
1545
+ }
1546
+ },
1547
+ select: {
1548
+ [rootKey]: {},
1549
+ [edgeKey]: {}
1550
+ }
1551
+ };
1552
+ const edges = [];
1553
+ let cursor;
1554
+ do {
1555
+ const response = await this.cognite.queryInstances({
1556
+ ...query,
1557
+ ...cursor ? { cursors: { [edgeKey]: cursor } } : {}
1558
+ });
1559
+ edges.push(
1560
+ ...(response.items[edgeKey] ?? []).filter(
1561
+ (item) => item.instanceType === "edge"
1562
+ )
1563
+ );
1564
+ cursor = response.nextCursor[edgeKey];
1565
+ } while (cursor);
1566
+ return edges;
1567
+ }
1568
+ mapReverseDirectRelation(node, value, property) {
1569
+ const targets = asNodeIdArray(value);
1570
+ return targets.map((target) => ({
1571
+ instanceType: "node",
1572
+ ...target,
1573
+ sources: [
1574
+ {
1575
+ source: property.through.source,
1576
+ properties: {
1577
+ [property.through.identifier]: normalizeReverseDirectRelationValue(node, property)
1578
+ }
1579
+ }
1580
+ ]
1581
+ }));
1582
+ }
1583
+ mapEdgeConnection(node, propertyName, value, property, onEdgeCreation) {
1584
+ const direction = property.direction ?? "outwards";
1585
+ return asNodeIdArray(value).map((target) => {
1586
+ const startNode = direction === "inwards" ? target : node;
1587
+ const endNode = direction === "inwards" ? node : target;
1588
+ const edgeType = toNodeId(property.type, `edge type for "${propertyName}"`);
1589
+ const createEdgeId = onEdgeCreation?.[propertyName];
1590
+ if (!createEdgeId) {
1591
+ throw new Error(
1592
+ `Invalid upsert options:
1593
+ - onEdgeCreation.${propertyName}: required when ingesting edge connection "${propertyName}"`
1594
+ );
1595
+ }
1596
+ const edgeId = createEdgeId({
1597
+ startNode,
1598
+ endNode,
1599
+ edgeType
1600
+ });
1601
+ assertNodeId(edgeId, `onEdgeCreation(${propertyName})`);
1602
+ return {
1603
+ instanceType: "edge",
1604
+ ...edgeId,
1605
+ type: property.type,
1606
+ startNode,
1607
+ endNode
1608
+ };
1609
+ });
1610
+ }
1611
+ };
1612
+ function normalizeReverseDirectRelationValue(node, property) {
1613
+ return property.targetsList === true ? [node] : node;
1614
+ }
1615
+ function normalizeViewPropertyValue(value, property) {
1616
+ if (property.type.type !== "direct") return normalizePropertyValue(value);
1617
+ if (Array.isArray(value)) return value.map((item) => toNodeId(item));
1618
+ return toNodeId(value);
1619
+ }
1620
+ function normalizePropertyValue(value) {
1621
+ if (value instanceof Date) return value.toISOString();
1622
+ if (Array.isArray(value)) return value.map(normalizePropertyValue);
1623
+ if (isPlainObject(value)) {
1624
+ return Object.fromEntries(
1625
+ Object.entries(value).map(([key, nestedValue]) => [key, normalizePropertyValue(nestedValue)])
1626
+ );
1627
+ }
1628
+ return value;
1629
+ }
1630
+ function asNodeIdArray(value) {
1631
+ return Array.isArray(value) ? value.map((item) => toNodeId(item)) : [toNodeId(value)];
1632
+ }
1633
+ function toNodeId(value, label = "relation reference") {
1634
+ if (!isPlainObject(value) || typeof value.space !== "string" || typeof value.externalId !== "string") {
1635
+ throw new Error(`Invalid upsert options:
1636
+ - ${label}: expected a NodeId`);
1637
+ }
1638
+ return { space: value.space, externalId: value.externalId };
1639
+ }
1640
+ function instanceKey(instance) {
1641
+ return `${instance.space}\0${instance.externalId}`;
1642
+ }
1643
+ function uniqueDeletes(deletes) {
1644
+ const seen = /* @__PURE__ */ new Set();
1645
+ return deletes.filter((item) => {
1646
+ const key = instanceKey(item);
1647
+ if (seen.has(key)) return false;
1648
+ seen.add(key);
1649
+ return true;
1650
+ });
1651
+ }
1652
+ function assertNodeId(value, label) {
1653
+ if (!isPlainObject(value) || typeof value.space !== "string" || typeof value.externalId !== "string") {
1654
+ throw new Error(`Invalid upsert options:
1655
+ - ${label}: expected a NodeId`);
1656
+ }
1657
+ }
1658
+ function isPlainObject(value) {
1659
+ return value != null && typeof value === "object" && !Array.isArray(value);
1660
+ }
1661
+
1366
1662
  // src/mappers/view-mapper.ts
1367
1663
  var ViewMapper = class {
1368
1664
  constructor(cognite, dataModelId) {
@@ -1410,6 +1706,7 @@ var ViewMapper = class {
1410
1706
  };
1411
1707
 
1412
1708
  // src/client.ts
1709
+ var APPLY_ITEM_LIMIT = 1e3;
1413
1710
  var IndustrialModelClient = class {
1414
1711
  constructor(client, dataModelId, options = {}) {
1415
1712
  const cognite = createCogniteAdapter(client);
@@ -1417,6 +1714,7 @@ var IndustrialModelClient = class {
1417
1714
  const viewMapper = new ViewMapper(cognite, dataModelId);
1418
1715
  this.queryMapper = new QueryMapper(viewMapper, cognite);
1419
1716
  this.aggregateMapper = new AggregateMapper(viewMapper, cognite);
1717
+ this.upsertMapper = new UpsertMapper(viewMapper, cognite);
1420
1718
  this.aggregateResultMapper = new AggregateResultMapper();
1421
1719
  this.resultMapper = new QueryResultMapper(viewMapper);
1422
1720
  this.resultValidator = new QueryResultValidator(viewMapper);
@@ -1430,6 +1728,54 @@ var IndustrialModelClient = class {
1430
1728
  const execute = (options) => this.aggregateInternal(options);
1431
1729
  return execute;
1432
1730
  }
1731
+ upsert() {
1732
+ const execute = (options) => this.upsertInternal(options);
1733
+ return execute;
1734
+ }
1735
+ async delete(items) {
1736
+ const deleteItems = items.map((item) => {
1737
+ assertNodeId2(item);
1738
+ return {
1739
+ instanceType: "node",
1740
+ space: item.space,
1741
+ externalId: item.externalId
1742
+ };
1743
+ });
1744
+ const response = await this.applyInstancesInChunks({
1745
+ items: [],
1746
+ delete: deleteItems
1747
+ });
1748
+ return { items: response.items };
1749
+ }
1750
+ async upsertInternal(options) {
1751
+ const cogniteRequest = await this.upsertMapper.map(options);
1752
+ const response = await this.applyInstancesInChunks(cogniteRequest);
1753
+ return { items: response.items };
1754
+ }
1755
+ async applyInstancesInChunks(request) {
1756
+ const deleteItems = request.delete ?? [];
1757
+ const totalItems = request.items.length + deleteItems.length;
1758
+ if (totalItems === 0) return { items: [] };
1759
+ if (totalItems <= APPLY_ITEM_LIMIT) {
1760
+ return this.cognite.applyInstances(request);
1761
+ }
1762
+ const responses = [];
1763
+ for (const deleteChunk of chunks(deleteItems, APPLY_ITEM_LIMIT)) {
1764
+ const response = await this.cognite.applyInstances({
1765
+ items: [],
1766
+ delete: deleteChunk
1767
+ });
1768
+ responses.push(...response.items);
1769
+ }
1770
+ for (const itemChunk of chunks(request.items, APPLY_ITEM_LIMIT)) {
1771
+ const response = await this.cognite.applyInstances({
1772
+ items: itemChunk,
1773
+ ...request.replace === true ? { replace: true } : {}
1774
+ });
1775
+ responses.push(...response.items);
1776
+ }
1777
+ return { items: responses };
1778
+ }
1433
1779
  async aggregateInternal(options) {
1434
1780
  const cogniteRequest = await this.aggregateMapper.map(options);
1435
1781
  const response = await this.cognite.aggregateInstances(cogniteRequest);
@@ -1486,6 +1832,18 @@ var IndustrialModelClient = class {
1486
1832
  return appendNodesAndEdges(result, nestedResults);
1487
1833
  }
1488
1834
  };
1835
+ function chunks(items, size) {
1836
+ const result = [];
1837
+ for (let index = 0; index < items.length; index += size) {
1838
+ result.push(items.slice(index, index + size));
1839
+ }
1840
+ return result;
1841
+ }
1842
+ function assertNodeId2(value) {
1843
+ if (value == null || typeof value !== "object" || Array.isArray(value) || typeof value.space !== "string" || value.space?.length === 0 || typeof value.externalId !== "string" || value.externalId?.length === 0) {
1844
+ throw new Error("Invalid delete options:\n- items: expected NodeId values");
1845
+ }
1846
+ }
1489
1847
 
1490
1848
  exports.IndustrialModelClient = IndustrialModelClient;
1491
1849
  //# sourceMappingURL=index.cjs.map