@cap-js-community/common 0.2.0 → 0.2.2

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
@@ -41,13 +41,14 @@ entity Books {
41
41
 
42
42
  ### Annotations
43
43
 
44
- Annotations can be used to enable replication cache for a service:
44
+ Annotations can be used to enable replication cache for a service on entity level:
45
45
 
46
46
  - `@cds.replicate: Boolean | Object`: Enable replication cache for entity
47
47
  - `@cds.replicate.ttl: Number`: Time-To-Live (TTL) of cache entry in milliseconds
48
48
  - `@cds.replicate.auto: Boolean`: Replication is managed automatically
49
49
  - `@cds.replicate.preload: Boolean`: Preload replication for entity
50
50
  - `@cds.replicate.group: String`: Replication group name
51
+ - `@cds.replicate.static: Boolean`: Statically replicate non-tenant aware
51
52
 
52
53
  Defaults are taken from CDS environment.
53
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js-community/common",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "CAP Node.js Community Common",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "engines": {
@@ -51,18 +51,18 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@cap-js-community/common": "./",
54
- "@cap-js/cds-test": "^0.3.0",
55
- "@sap/cds": "^9.0.2",
54
+ "@cap-js/cds-test": "^0.4.0",
55
+ "@sap/cds": "^9.0.4",
56
56
  "@sap/cds-common-content": "^3.0.1",
57
- "@sap/cds-dk": "^9.0.4",
58
- "eslint": "9.28.0",
57
+ "@sap/cds-dk": "^9.0.5",
58
+ "eslint": "9.29.0",
59
59
  "eslint-config-prettier": "10.1.5",
60
- "eslint-plugin-jest": "28.12.0",
61
- "eslint-plugin-n": "^17.19.0",
62
- "jest": "29.7.0",
60
+ "eslint-plugin-jest": "29.0.1",
61
+ "eslint-plugin-n": "^17.20.0",
62
+ "jest": "30.0.3",
63
63
  "jest-html-reporters": "3.1.7",
64
64
  "jest-junit": "16.0.0",
65
- "prettier": "3.5.3",
65
+ "prettier": "3.6.1",
66
66
  "shelljs": "^0.10.0"
67
67
  },
68
68
  "cds": {
@@ -28,6 +28,7 @@ const Status = {
28
28
 
29
29
  const Annotations = {
30
30
  Replicate: "@cds.replicate",
31
+ ReplicateStatic: "@cds.replicate.static",
31
32
  ReplicateGroup: "@cds.replicate.group",
32
33
  ReplicateAuto: "@cds.replicate.auto",
33
34
  ReplicateTTL: "@cds.replicate.ttl",
@@ -61,7 +62,7 @@ class ReplicationCache {
61
62
  });
62
63
  cds.on("connect", (service) => {
63
64
  if (service.name === this.name) {
64
- const refs = ReplicationCache.replicationRefs(this.model, service);
65
+ const refs = ReplicationCache.replicationRefs(this.model, service, this.options.deploy);
65
66
  if (refs.length > 0) {
66
67
  this.setup(service, refs);
67
68
  this.log.info("using replication cache", {
@@ -82,12 +83,12 @@ class ReplicationCache {
82
83
  });
83
84
  }
84
85
 
85
- static replicationRefs(model, service) {
86
+ static replicationRefs(model, service, deploy) {
86
87
  const refs = Object.keys(model.definitions).filter((name) => {
87
88
  const definition = model.definitions[name];
88
89
  return (
89
90
  definition.kind === "entity" &&
90
- !definition.projection &&
91
+ (!(definition.query || definition.projection) || !deploy) &&
91
92
  (service.name === "db" || name.startsWith(`${service.name}.`)) &&
92
93
  Object.values(Annotations).find((annotation) => {
93
94
  return definition[annotation] !== undefined;
@@ -169,6 +170,14 @@ class ReplicationCache {
169
170
  if (!this.options.search && !this.search(req.query)) {
170
171
  return await next();
171
172
  }
173
+ let fromRefs = queryFromRefs(model, req.query);
174
+ if (this.options.deploy) {
175
+ fromRefs = baseRefs(model, fromRefs);
176
+ fromRefs = localizedRefs(model, req.query, fromRefs);
177
+ }
178
+ if (fromRefs.length === 0 || !this.relevant(fromRefs)) {
179
+ return await next();
180
+ }
172
181
  let refs = queryRefs(model, req.query);
173
182
  if (!this.options.deploy) {
174
183
  if (!this.localized(req.query, refs)) {
@@ -189,8 +198,12 @@ class ReplicationCache {
189
198
  this.stats.counts[ref] ??= 0;
190
199
  this.stats.counts[ref]++;
191
200
  }
201
+ let tenant = req.tenant;
202
+ if (staticRefs(model, refs)) {
203
+ tenant = undefined; // non-tenant cache
204
+ }
192
205
  const status = await this.load(
193
- req.tenant,
206
+ tenant,
194
207
  refs,
195
208
  {
196
209
  auto: this.options.auto,
@@ -203,7 +216,7 @@ class ReplicationCache {
203
216
  this.stats.used++;
204
217
  this.stats.ratio = Math.round(this.stats.used / this.stats.hits);
205
218
  this.log.debug("Replication cache was used");
206
- const db = this.cache.get(req.tenant).db;
219
+ const db = this.cache.get(tenant).db;
207
220
  if (this.options.measure) {
208
221
  return this.measure(
209
222
  async () => {
@@ -530,7 +543,7 @@ class ReplicationCache {
530
543
  let projections = true;
531
544
  for (const ref of refs) {
532
545
  const definition = model.definitions[ref];
533
- if (definition.query) {
546
+ if (definition.query || definition.projection) {
534
547
  this.stats.projections[ref] ??= 0;
535
548
  this.stats.projections[ref]++;
536
549
  this.log.debug("Replication cache not enabled for 'projections' without deploy feature", {
@@ -833,17 +846,24 @@ function localizedRefs(model, query, refs) {
833
846
  return unique(refs.concat(localizedRefs));
834
847
  }
835
848
 
849
+ function queryFromRefs(model, query) {
850
+ if (!query.SELECT) {
851
+ return [];
852
+ }
853
+ return unique(selectFromRefs(model, query));
854
+ }
855
+
836
856
  function queryRefs(model, query) {
837
857
  if (!query.SELECT) {
838
858
  return [];
839
859
  }
840
- return unique(fromRefs(model, query));
860
+ return unique(selectRefs(model, query));
841
861
  }
842
862
 
843
- function fromRefs(model, query) {
863
+ function selectFromRefs(model, query) {
844
864
  let refs = [];
845
865
  if (query.SELECT.from.SELECT) {
846
- refs = fromRefs(model, query.SELECT.from);
866
+ refs = selectFromRefs(model, query.SELECT.from);
847
867
  } else if (query.SELECT.from.ref) {
848
868
  refs = resolveRefs(model, query.SELECT.from.ref);
849
869
  } else if ((query.SELECT.from.join || query.SELECT.from.SET) && query.SELECT.from.args) {
@@ -852,25 +872,71 @@ function fromRefs(model, query) {
852
872
  return refs;
853
873
  }, []);
854
874
  }
875
+ return refs;
876
+ }
877
+
878
+ function selectFromAliases(model, query) {
879
+ let aliases = {};
880
+ if (query.SELECT.from.SELECT) {
881
+ // Sub-select aliases are not (yet) supported
882
+ } else if (query.SELECT.from.ref) {
883
+ const ref = resolveRef(model, query.SELECT.from.ref);
884
+ if (query.SELECT.from.as) {
885
+ aliases[query.SELECT.from.as] = ref;
886
+ } else {
887
+ const as = ref.split(".").pop();
888
+ aliases[as] = ref;
889
+ }
890
+ } else if ((query.SELECT.from.join || query.SELECT.from.SET) && query.SELECT.from.args) {
891
+ for (const arg of query.SELECT.from.args) {
892
+ const ref = resolveRef(model, arg.ref);
893
+ if (arg.as) {
894
+ aliases[arg.as] = ref;
895
+ } else {
896
+ const as = ref.split(".").pop();
897
+ aliases[as] = ref;
898
+ }
899
+ }
900
+ }
901
+ return aliases;
902
+ }
903
+
904
+ function selectRefs(model, query) {
905
+ let refs = selectFromRefs(model, query);
906
+ const aliases = selectFromAliases(model, query);
855
907
  if (query._target) {
856
908
  const target = model.definitions[query._target.name];
857
909
  if (query.SELECT.orderBy) {
858
- refs = refs.concat(expressionRefs(model, target, query.SELECT.orderBy));
910
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.orderBy, query.SELECT.mixin, aliases));
859
911
  }
860
912
  if (query.SELECT.columns) {
861
- refs = refs.concat(expressionRefs(model, target, query.SELECT.columns));
862
- refs = refs.concat(expandRefs(model, target, query.SELECT.columns));
913
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.columns, query.SELECT.mixin, aliases));
914
+ refs = refs.concat(expandRefs(model, target, query.SELECT.columns, query.SELECT.mixin, aliases));
863
915
  }
864
916
  if (query.SELECT.where) {
865
- refs = refs.concat(expressionRefs(model, target, query.SELECT.where));
917
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.where, query.SELECT.mixin, aliases));
866
918
  }
867
919
  if (query.SELECT.having) {
868
- refs = refs.concat(expressionRefs(model, target, query.SELECT.having));
920
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.having, query.SELECT.mixin, aliases));
869
921
  }
870
922
  }
871
923
  return refs;
872
924
  }
873
925
 
926
+ function resolveRef(model, refs) {
927
+ let ref = refs[0];
928
+ if (ref.id) {
929
+ ref = ref.id;
930
+ }
931
+ let current = model.definitions[ref];
932
+ for (const ref of refs.slice(1)) {
933
+ if (current.elements[ref].type === "cds.Association" || current.elements[ref].type === "cds.Composition") {
934
+ current = current.elements[ref]._target;
935
+ }
936
+ }
937
+ return current.name;
938
+ }
939
+
874
940
  function resolveRefs(model, refs) {
875
941
  let resolvedRefs = [];
876
942
  let ref = refs[0];
@@ -892,14 +958,16 @@ function resolveRefs(model, refs) {
892
958
  return resolvedRefs;
893
959
  }
894
960
 
895
- function identifierRefs(model, definition, array) {
961
+ function identifierRefs(model, definition, expressions, mixins, aliases) {
896
962
  let refs = [];
897
- for (const entry of array) {
898
- if (Array.isArray(entry.ref)) {
963
+ for (const expression of expressions) {
964
+ if (Array.isArray(expression.ref)) {
899
965
  let current = definition;
900
- for (const ref of entry.ref) {
901
- if (current.elements[ref].type === "cds.Association" || current.elements[ref].type === "cds.Composition") {
902
- current = current.elements[ref]._target;
966
+ let currentMixins = mixins;
967
+ for (const ref of expression.ref) {
968
+ current = target(model, current, currentMixins, aliases, ref);
969
+ if (current !== null) {
970
+ currentMixins = {};
903
971
  refs.push(current.name);
904
972
  }
905
973
  }
@@ -908,27 +976,29 @@ function identifierRefs(model, definition, array) {
908
976
  return refs;
909
977
  }
910
978
 
911
- function expressionRefs(model, definition, array) {
912
- let refs = identifierRefs(model, definition, array);
913
- for (const entry of array) {
914
- if (entry.xpr) {
915
- refs = refs.concat(expressionRefs(model, definition, entry.xpr));
916
- } else if (entry.args) {
917
- refs = refs.concat(expressionRefs(model, definition, entry.args));
918
- } else if (entry.SELECT) {
919
- refs = refs.concat(fromRefs(model, entry));
979
+ function expressionRefs(model, definition, expressions, mixins, aliases) {
980
+ let refs = identifierRefs(model, definition, expressions, mixins, aliases);
981
+ for (const expression of expressions) {
982
+ if (expression.xpr) {
983
+ refs = refs.concat(expressionRefs(model, definition, expression.xpr, mixins, aliases));
984
+ } else if (expression.args) {
985
+ refs = refs.concat(expressionRefs(model, definition, expression.args, mixins, aliases));
986
+ } else if (expression.SELECT) {
987
+ refs = refs.concat(selectRefs(model, expression));
920
988
  }
921
989
  }
922
990
  return refs;
923
991
  }
924
992
 
925
- function expandRefs(model, definition, columns) {
993
+ function expandRefs(model, definition, columns, mixins, aliases) {
926
994
  let refs = [];
927
995
  for (const column of columns) {
928
996
  if (Array.isArray(column.ref) && column.expand) {
929
997
  let current = definition;
998
+ let currentMixins = mixins;
930
999
  for (const ref of column.ref) {
931
- current = current.elements[ref]._target;
1000
+ current = target(model, current, currentMixins, aliases, ref);
1001
+ currentMixins = {};
932
1002
  refs.push(current.name);
933
1003
  }
934
1004
  refs = refs.concat(expandRefs(model, current, column.expand));
@@ -937,6 +1007,35 @@ function expandRefs(model, definition, columns) {
937
1007
  return refs;
938
1008
  }
939
1009
 
1010
+ function target(model, entity, mixins, aliases, ref) {
1011
+ if (aliases?.[ref]) {
1012
+ return typeof aliases[ref] === "string" ? model.definitions[aliases[ref]] : aliases[ref];
1013
+ }
1014
+ if (entity.name.endsWith(`.${ref}`)) {
1015
+ return entity;
1016
+ }
1017
+ const element = entity.elements[ref] || mixins?.[ref];
1018
+ if (!element) {
1019
+ cds.log(Component).warn("Reference not found in entity", {
1020
+ entity: entity.name,
1021
+ ref,
1022
+ });
1023
+ }
1024
+ if (element.type === "cds.Association" || element.type === "cds.Composition") {
1025
+ return model.definitions[element.target];
1026
+ }
1027
+ return null;
1028
+ }
1029
+
1030
+ function staticRefs(model, refs) {
1031
+ for (const ref of refs) {
1032
+ if (!model.definitions[ref][Annotations.ReplicateStatic]) {
1033
+ return false;
1034
+ }
1035
+ }
1036
+ return true;
1037
+ }
1038
+
940
1039
  function cached(cache, field, init) {
941
1040
  try {
942
1041
  if (init && !cache.get(field)) {