@cap-js-community/common 0.2.1 → 0.2.3

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/CHANGELOG.md ADDED
@@ -0,0 +1,79 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
+ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
+
8
+ ## Version 0.2.3 - 2025-07-01
9
+
10
+ ### Fixed
11
+
12
+ - Improve reference detection
13
+
14
+ ## Version 0.2.2 - 2025-06-26
15
+
16
+ ### Fixed
17
+
18
+ - Improve reference detection for aliases
19
+
20
+ ## Version 0.2.1 - 2025-06-26
21
+
22
+ ### Fixed
23
+
24
+ - Improve reference detection
25
+ - Static non-tenant aware replication via `cds.replicate.static`
26
+
27
+ ## Version 0.2.0 - 2025-06-03
28
+
29
+ ### Fixed
30
+
31
+ - CDS 9 compatibility
32
+
33
+ ## Version 0.1.7 - 2025-05-08
34
+
35
+ ### Fixed
36
+
37
+ - Enabling journal mode and changing entity in same cycle is not allowed
38
+
39
+ ## Version 0.1.6 - 2025-05-07
40
+
41
+ ### Fixed
42
+
43
+ - Redis 4
44
+
45
+ ## Version 0.1.5 - 2025-05-05
46
+
47
+ ### Fixed
48
+
49
+ - Dependencies
50
+
51
+ ## Version 0.1.4 - 2025-04-10
52
+
53
+ ### Fixed
54
+
55
+ - Improvements
56
+
57
+ ## Version 0.1.3 - 2025-04-10
58
+
59
+ ### Fixed
60
+
61
+ - Redis client improvements
62
+
63
+ ## Version 0.1.2 - 2025-04-09
64
+
65
+ ### Fixed
66
+
67
+ - Improvements
68
+
69
+ ## Version 0.1.1 - 2025-04-09
70
+
71
+ ### Fixed
72
+
73
+ - Improvements
74
+
75
+ ## Version 0.1.0 - 2025-04-09
76
+
77
+ ### Added
78
+
79
+ - Internal release
package/README.md CHANGED
@@ -121,6 +121,7 @@ Options can be passed to migration check via CDS environment via `cds.migrationC
121
121
 
122
122
  - Maintain the whitelist extension file `migration-extension-whitelist.json` for compatible changes:
123
123
  - **Whitelist Entity**:
124
+
124
125
  ```json
125
126
  {
126
127
  "definitions": {
@@ -128,7 +129,9 @@ Options can be passed to migration check via CDS environment via `cds.migrationC
128
129
  }
129
130
  }
130
131
  ```
132
+
131
133
  - **Whitelist Entity Element**:
134
+
132
135
  ```json
133
136
  {
134
137
  "definitions": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/common",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "CAP Node.js Community Common",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "engines": {
@@ -20,7 +20,8 @@
20
20
  "bin",
21
21
  "src",
22
22
  "LICENSE",
23
- "cds-plugin.js"
23
+ "cds-plugin.js",
24
+ "CHANGELOG.md"
24
25
  ],
25
26
  "main": "index.js",
26
27
  "types": "index.d.ts",
@@ -55,14 +56,14 @@
55
56
  "@sap/cds": "^9.0.4",
56
57
  "@sap/cds-common-content": "^3.0.1",
57
58
  "@sap/cds-dk": "^9.0.5",
58
- "eslint": "9.29.0",
59
+ "eslint": "9.30.0",
59
60
  "eslint-config-prettier": "10.1.5",
60
61
  "eslint-plugin-jest": "29.0.1",
61
62
  "eslint-plugin-n": "^17.20.0",
62
63
  "jest": "30.0.3",
63
64
  "jest-html-reporters": "3.1.7",
64
65
  "jest-junit": "16.0.0",
65
- "prettier": "3.6.1",
66
+ "prettier": "3.6.2",
66
67
  "shelljs": "^0.10.0"
67
68
  },
68
69
  "cds": {
@@ -178,7 +178,7 @@ class ReplicationCache {
178
178
  if (fromRefs.length === 0 || !this.relevant(fromRefs)) {
179
179
  return await next();
180
180
  }
181
- let refs = queryRefs(model, req.query);
181
+ let refs = queryRefs(model, req.query, req.target);
182
182
  if (!this.options.deploy) {
183
183
  if (!this.localized(req.query, refs)) {
184
184
  return await next();
@@ -818,13 +818,18 @@ function baseRefs(model, refs) {
818
818
  const baseRefs = [];
819
819
  let currentRefs = refs;
820
820
  let nextRefs = [];
821
+ const visited = new Set();
821
822
  while (currentRefs.length > 0) {
822
823
  for (const ref of currentRefs) {
824
+ if (visited.has(ref)) {
825
+ continue;
826
+ }
827
+ visited.add(ref);
823
828
  const definition = model.definitions[ref];
824
829
  if (!definition.query) {
825
830
  baseRefs.push(ref);
826
831
  } else {
827
- nextRefs = nextRefs.concat(queryRefs(model, definition.query));
832
+ nextRefs = nextRefs.concat(queryRefs(model, definition.query, definition));
828
833
  }
829
834
  }
830
835
  currentRefs = nextRefs;
@@ -853,49 +858,148 @@ function queryFromRefs(model, query) {
853
858
  return unique(selectFromRefs(model, query));
854
859
  }
855
860
 
856
- function queryRefs(model, query) {
861
+ function queryRefs(model, query, definition) {
857
862
  if (!query.SELECT) {
858
863
  return [];
859
864
  }
860
- return unique(selectRefs(model, query));
865
+ return unique(selectRefs(model, definition, query));
861
866
  }
862
867
 
863
868
  function selectFromRefs(model, query) {
864
869
  let refs = [];
865
- if (query.SELECT.from.SELECT) {
866
- refs = selectFromRefs(model, query.SELECT.from);
867
- } else if (query.SELECT.from.ref) {
870
+ if (query.SELECT.from.ref) {
868
871
  refs = resolveRefs(model, query.SELECT.from.ref);
869
- } else if ((query.SELECT.from.join || query.SELECT.from.SET) && query.SELECT.from.args) {
872
+ } else if (query.SELECT.from.args) {
870
873
  refs = query.SELECT.from.args.reduce((refs, arg) => {
871
- refs = refs.concat(resolveRefs(model, arg.ref || arg));
874
+ if (arg.ref) {
875
+ refs = refs.concat(resolveRefs(model, arg.ref));
876
+ } else if (arg.args) {
877
+ refs = refs.concat(selectFromRefs(model, { SELECT: { from: { args: arg.args } } }));
878
+ }
872
879
  return refs;
873
880
  }, []);
881
+ } else if (query.SELECT.from.SELECT) {
882
+ refs = selectFromRefs(model, query.SELECT.from);
874
883
  }
875
884
  return refs;
876
885
  }
877
886
 
878
- function selectRefs(model, query) {
879
- let refs = selectFromRefs(model, query);
880
- if (query._target) {
881
- const target = model.definitions[query._target.name];
882
- if (query.SELECT.orderBy) {
883
- refs = refs.concat(expressionRefs(model, target, query.SELECT.orderBy, query.SELECT.mixin));
887
+ function selectFromPrimaryRef(model, query) {
888
+ if (query.SELECT.from.ref) {
889
+ return resolveRef(model, query.SELECT.from.ref);
890
+ } else if (query.SELECT.from.args) {
891
+ for (const arg of query.SELECT.from.args) {
892
+ if (arg.ref) {
893
+ return resolveRef(model, arg.ref);
894
+ } else if (arg.args) {
895
+ return selectFromPrimaryRef(model, { SELECT: { from: { args: arg.args } } });
896
+ }
884
897
  }
885
- if (query.SELECT.columns) {
886
- refs = refs.concat(expressionRefs(model, target, query.SELECT.columns, query.SELECT.mixin));
887
- refs = refs.concat(expandRefs(model, target, query.SELECT.columns, query.SELECT.mixin));
898
+ } else if (query.SELECT.from.SELECT) {
899
+ return selectFromPrimaryRef(model, query.SELECT.from);
900
+ }
901
+ }
902
+
903
+ function selectFromAliases(model, definition, query) {
904
+ let aliases = {};
905
+ if (definition?.name) {
906
+ aliases["$self"] = definition.name;
907
+ }
908
+ if (query.SELECT.from.SELECT) {
909
+ // Sub-select aliases are not (yet) supported
910
+ } else if (query.SELECT.from.ref) {
911
+ const ref = resolveRef(model, query.SELECT.from.ref);
912
+ if (query.SELECT.from.as) {
913
+ aliases[query.SELECT.from.as] = ref;
914
+ } else {
915
+ const as = ref.split(".").pop();
916
+ aliases[as] = ref;
888
917
  }
889
- if (query.SELECT.where) {
890
- refs = refs.concat(expressionRefs(model, target, query.SELECT.where, query.SELECT.mixin));
918
+ } else if (query.SELECT.from.args) {
919
+ for (const arg of query.SELECT.from.args) {
920
+ if (arg.ref) {
921
+ const ref = resolveRef(model, arg.ref);
922
+ if (arg.as) {
923
+ aliases[arg.as] = ref;
924
+ } else {
925
+ const as = ref.split(".").pop();
926
+ aliases[as] = ref;
927
+ }
928
+ } else if (arg.args) {
929
+ for (const subArg of arg.args) {
930
+ aliases = {
931
+ ...aliases,
932
+ ...selectFromAliases(model, definition, { SELECT: { from: { args: subArg.args } } }),
933
+ };
934
+ }
935
+ }
891
936
  }
892
- if (query.SELECT.having) {
893
- refs = refs.concat(expressionRefs(model, target, query.SELECT.having, query.SELECT.mixin));
937
+ }
938
+ return aliases;
939
+ }
940
+
941
+ function selectOuterAliases(model, target, columns, mixins, aliases) {
942
+ const outerAliases = {};
943
+ if (columns) {
944
+ for (const column of columns) {
945
+ if (column.ref) {
946
+ const as = column.as || column.ref[column.ref.length - 1];
947
+ let current = target;
948
+ for (const ref of column.ref) {
949
+ const currentTarget = targetEntity(model, current, mixins, aliases, ref);
950
+ if (currentTarget != null) {
951
+ current = currentTarget;
952
+ }
953
+ }
954
+ outerAliases[as] = (current || target).name;
955
+ }
894
956
  }
895
957
  }
958
+ return outerAliases;
959
+ }
960
+
961
+ function selectRefs(model, definition, query, aliases) {
962
+ let targetName = selectFromPrimaryRef(model, query);
963
+ const target = targetName ? model.definitions[targetName] : undefined;
964
+ let refs = selectFromRefs(model, query);
965
+ aliases = {
966
+ ...aliases,
967
+ ...selectFromAliases(model, definition, query),
968
+ };
969
+ if (query.SELECT.columns) {
970
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.columns, query.SELECT.mixin, aliases));
971
+ refs = refs.concat(expandRefs(model, target, query.SELECT.columns, query.SELECT.mixin, aliases));
972
+ }
973
+ if (query.SELECT.where) {
974
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.where, query.SELECT.mixin, aliases));
975
+ }
976
+ const outerAliases = {
977
+ ...aliases,
978
+ ...selectOuterAliases(model, target, query.SELECT.columns, query.SELECT.mixin, aliases),
979
+ };
980
+ if (query.SELECT.having) {
981
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.having, query.SELECT.mixin, outerAliases));
982
+ }
983
+ if (query.SELECT.orderBy) {
984
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.orderBy, query.SELECT.mixin, outerAliases));
985
+ }
896
986
  return refs;
897
987
  }
898
988
 
989
+ function resolveRef(model, refs) {
990
+ let ref = refs[0];
991
+ if (ref.id) {
992
+ ref = ref.id;
993
+ }
994
+ let current = model.definitions[ref];
995
+ for (const ref of refs.slice(1)) {
996
+ if (current.elements[ref].type === "cds.Association" || current.elements[ref].type === "cds.Composition") {
997
+ current = current.elements[ref]._target;
998
+ }
999
+ }
1000
+ return current.name;
1001
+ }
1002
+
899
1003
  function resolveRefs(model, refs) {
900
1004
  let resolvedRefs = [];
901
1005
  let ref = refs[0];
@@ -917,49 +1021,55 @@ function resolveRefs(model, refs) {
917
1021
  return resolvedRefs;
918
1022
  }
919
1023
 
920
- function identifierRefs(model, definition, expressions, mixin) {
1024
+ function identifierRefs(model, definition, expressions, mixins, aliases) {
921
1025
  let refs = [];
922
1026
  for (const expression of expressions) {
923
1027
  if (Array.isArray(expression.ref)) {
924
1028
  let current = definition;
925
- let currentMixin = mixin;
1029
+ let currentMixins = mixins;
926
1030
  for (const ref of expression.ref) {
927
- const element = current.elements[ref] || currentMixin?.[ref];
928
- if (element.type === "cds.Association" || element.type === "cds.Composition") {
929
- current = model.definitions[element.target];
930
- currentMixin = {};
1031
+ if (current?.name !== definition?.name) {
931
1032
  refs.push(current.name);
932
1033
  }
1034
+ if (ref.startsWith("$")) {
1035
+ break;
1036
+ }
1037
+ current = targetEntity(model, current, currentMixins, aliases, ref);
1038
+ currentMixins = {};
933
1039
  }
934
1040
  }
935
1041
  }
936
1042
  return refs;
937
1043
  }
938
1044
 
939
- function expressionRefs(model, definition, expressions, mixin) {
940
- let refs = identifierRefs(model, definition, expressions, mixin);
1045
+ function expressionRefs(model, definition, expressions, mixins, aliases) {
1046
+ let refs = identifierRefs(model, definition, expressions, mixins, aliases);
941
1047
  for (const expression of expressions) {
942
1048
  if (expression.xpr) {
943
- refs = refs.concat(expressionRefs(model, definition, expression.xpr, mixin));
1049
+ refs = refs.concat(expressionRefs(model, definition, expression.xpr, mixins, aliases));
944
1050
  } else if (expression.args) {
945
- refs = refs.concat(expressionRefs(model, definition, expression.args, mixin));
1051
+ refs = refs.concat(expressionRefs(model, definition, expression.args, mixins, aliases));
946
1052
  } else if (expression.SELECT) {
947
- refs = refs.concat(selectRefs(model, expression));
1053
+ refs = refs.concat(
1054
+ selectRefs(model, undefined, expression, {
1055
+ ...aliases,
1056
+ ["$self"]: undefined,
1057
+ }),
1058
+ );
948
1059
  }
949
1060
  }
950
1061
  return refs;
951
1062
  }
952
1063
 
953
- function expandRefs(model, definition, columns, mixin) {
1064
+ function expandRefs(model, definition, columns, mixins, aliases) {
954
1065
  let refs = [];
955
1066
  for (const column of columns) {
956
1067
  if (Array.isArray(column.ref) && column.expand) {
957
1068
  let current = definition;
958
- let currentMixin = mixin;
1069
+ let currentMixins = mixins;
959
1070
  for (const ref of column.ref) {
960
- const element = current.elements[ref] || currentMixin?.[ref];
961
- current = model.definitions[element.target];
962
- currentMixin = {};
1071
+ current = targetEntity(model, current, currentMixins, aliases, ref);
1072
+ currentMixins = {};
963
1073
  refs.push(current.name);
964
1074
  }
965
1075
  refs = refs.concat(expandRefs(model, current, column.expand));
@@ -968,6 +1078,26 @@ function expandRefs(model, definition, columns, mixin) {
968
1078
  return refs;
969
1079
  }
970
1080
 
1081
+ function targetEntity(model, entity, mixins, aliases, ref) {
1082
+ if (aliases?.[ref]) {
1083
+ return typeof aliases[ref] === "string" ? model.definitions[aliases[ref]] : aliases[ref];
1084
+ }
1085
+ if (entity?.name.endsWith(`.${ref}`)) {
1086
+ return entity;
1087
+ }
1088
+ const element = entity?.elements[ref] || mixins?.[ref];
1089
+ if (!element) {
1090
+ cds.log(Component).warn("Reference not found in entity", {
1091
+ entity: entity?.name,
1092
+ ref,
1093
+ });
1094
+ }
1095
+ if (element.type === "cds.Association" || element.type === "cds.Composition") {
1096
+ return model.definitions[element.target];
1097
+ }
1098
+ return null;
1099
+ }
1100
+
971
1101
  function staticRefs(model, refs) {
972
1102
  for (const ref of refs) {
973
1103
  if (!model.definitions[ref][Annotations.ReplicateStatic]) {