@cap-js-community/common 0.2.2 → 0.2.4

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,87 @@
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.4 - 2025-07-03
9
+
10
+ ### Fixed
11
+
12
+ - Multi-tenancy fixes
13
+ - Improve reference detection
14
+ - Deactivate TTL for negative value and static entities
15
+
16
+ ## Version 0.2.3 - 2025-07-01
17
+
18
+ ### Fixed
19
+
20
+ - Improve reference detection
21
+
22
+ ## Version 0.2.2 - 2025-06-26
23
+
24
+ ### Fixed
25
+
26
+ - Improve reference detection for aliases
27
+
28
+ ## Version 0.2.1 - 2025-06-26
29
+
30
+ ### Fixed
31
+
32
+ - Improve reference detection
33
+ - Static non-tenant aware replication via `cds.replicate.static`
34
+
35
+ ## Version 0.2.0 - 2025-06-03
36
+
37
+ ### Fixed
38
+
39
+ - CDS 9 compatibility
40
+
41
+ ## Version 0.1.7 - 2025-05-08
42
+
43
+ ### Fixed
44
+
45
+ - Enabling journal mode and changing entity in same cycle is not allowed
46
+
47
+ ## Version 0.1.6 - 2025-05-07
48
+
49
+ ### Fixed
50
+
51
+ - Redis 4
52
+
53
+ ## Version 0.1.5 - 2025-05-05
54
+
55
+ ### Fixed
56
+
57
+ - Dependencies
58
+
59
+ ## Version 0.1.4 - 2025-04-10
60
+
61
+ ### Fixed
62
+
63
+ - Improvements
64
+
65
+ ## Version 0.1.3 - 2025-04-10
66
+
67
+ ### Fixed
68
+
69
+ - Redis client improvements
70
+
71
+ ## Version 0.1.2 - 2025-04-09
72
+
73
+ ### Fixed
74
+
75
+ - Improvements
76
+
77
+ ## Version 0.1.1 - 2025-04-09
78
+
79
+ ### Fixed
80
+
81
+ - Improvements
82
+
83
+ ## Version 0.1.0 - 2025-04-09
84
+
85
+ ### Added
86
+
87
+ - 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.2",
3
+ "version": "0.2.4",
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",
@@ -44,7 +45,7 @@
44
45
  "audit": "npm audit --only=prod"
45
46
  },
46
47
  "dependencies": {
47
- "@cap-js/sqlite": "^2.0.1",
48
+ "@cap-js/sqlite": "^2.0.2",
48
49
  "commander": "^14.0.0",
49
50
  "redis": "^4.7.1",
50
51
  "verror": "^1.10.1"
@@ -52,17 +53,17 @@
52
53
  "devDependencies": {
53
54
  "@cap-js-community/common": "./",
54
55
  "@cap-js/cds-test": "^0.4.0",
55
- "@sap/cds": "^9.0.4",
56
+ "@sap/cds": "^9.1.0",
56
57
  "@sap/cds-common-content": "^3.0.1",
57
- "@sap/cds-dk": "^9.0.5",
58
- "eslint": "9.29.0",
58
+ "@sap/cds-dk": "^9.1.0",
59
+ "eslint": "9.30.1",
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
- "jest": "30.0.3",
63
+ "jest": "30.0.4",
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": {
@@ -73,10 +73,14 @@ class ReplicationCache {
73
73
  this.options?.credentials?.database &&
74
74
  this.options?.credentials?.database !== Constants.InMemory
75
75
  ) {
76
- this.log.debug("Preparing replication cache template database");
77
- this.template = createDB(Tenant.Template, this.model, this.options).catch((err) => {
78
- this.log.error("Preparing replication cache failed", err);
79
- });
76
+ this.log.info("Preparing replication cache template database");
77
+ this.template = createDB(Tenant.Template, this.model, this.options)
78
+ .then(() => {
79
+ this.log.info("Prepared replication cache template database");
80
+ })
81
+ .catch((err) => {
82
+ this.log.error("Preparing replication cache failed", err);
83
+ });
80
84
  }
81
85
  }
82
86
  }
@@ -178,7 +182,7 @@ class ReplicationCache {
178
182
  if (fromRefs.length === 0 || !this.relevant(fromRefs)) {
179
183
  return await next();
180
184
  }
181
- let refs = queryRefs(model, req.query);
185
+ let refs = queryRefs(model, req.query, req.target);
182
186
  if (!this.options.deploy) {
183
187
  if (!this.localized(req.query, refs)) {
184
188
  return await next();
@@ -220,9 +224,17 @@ class ReplicationCache {
220
224
  if (this.options.measure) {
221
225
  return this.measure(
222
226
  async () => {
223
- return db.tx({ ...req.context }, async (tx) => {
224
- return tx.run(req.query);
225
- });
227
+ return db.tx(
228
+ {
229
+ tenant: req.context.tenant,
230
+ locale: req.context.locale,
231
+ user: req.context.user,
232
+ http: req.context.http,
233
+ },
234
+ async (tx) => {
235
+ return tx.run(req.query);
236
+ },
237
+ );
226
238
  },
227
239
  async () => {
228
240
  await next();
@@ -585,6 +597,7 @@ class ReplicationCacheEntry {
585
597
  this.db = tenant.db;
586
598
  this.ref = ref;
587
599
  this.definition = this.csn[ref];
600
+ this.static = !!this.definition[Annotations.ReplicateStatic];
588
601
  this.preload = this.cache.options.preload && this.definition[Annotations.ReplicatePreload];
589
602
  this.name = this.definition.name.replace(/\./gi, "_");
590
603
  this.status = Status.New;
@@ -614,13 +627,15 @@ class ReplicationCacheEntry {
614
627
  await this.load(thread);
615
628
  this.status = Status.Ready;
616
629
  this.failures = 0;
617
- this.timeout = setTimeout(async () => {
618
- this.cache.log.debug("Replication cache ref TTL reached", {
619
- tenant: this.tenant.id,
620
- ref: this.ref,
621
- });
622
- await this.clear(true);
623
- }, this.ttl).unref();
630
+ if (this.ttl > 0 && !this.static) {
631
+ this.timeout = setTimeout(async () => {
632
+ this.cache.log.debug("Replication cache ref TTL reached", {
633
+ tenant: this.tenant.id,
634
+ ref: this.ref,
635
+ });
636
+ await this.clear(true);
637
+ }, this.ttl).unref();
638
+ }
624
639
  }
625
640
  this.cache.log.debug("Preparing replication cache ref finished", {
626
641
  tenant: this.tenant.id,
@@ -779,7 +794,11 @@ async function createDB(tenant, model, options) {
779
794
  const db = new SQLiteService(tenant ?? Tenant.Default, model, {
780
795
  kind: "sqlite",
781
796
  impl: "@cap-js/sqlite",
782
- credentials: { ...options.credentials, database: filePath },
797
+ multiTenant: false,
798
+ credentials: {
799
+ ...options.credentials,
800
+ database: filePath,
801
+ },
783
802
  });
784
803
  await db.init();
785
804
  if (options.deploy && (filePath === Constants.InMemory || tenant === Tenant.Template)) {
@@ -818,13 +837,18 @@ function baseRefs(model, refs) {
818
837
  const baseRefs = [];
819
838
  let currentRefs = refs;
820
839
  let nextRefs = [];
840
+ const visited = new Set();
821
841
  while (currentRefs.length > 0) {
822
842
  for (const ref of currentRefs) {
843
+ if (visited.has(ref)) {
844
+ continue;
845
+ }
846
+ visited.add(ref);
823
847
  const definition = model.definitions[ref];
824
848
  if (!definition.query) {
825
849
  baseRefs.push(ref);
826
850
  } else {
827
- nextRefs = nextRefs.concat(queryRefs(model, definition.query));
851
+ nextRefs = nextRefs.concat(queryRefs(model, definition.query, definition));
828
852
  }
829
853
  }
830
854
  currentRefs = nextRefs;
@@ -853,33 +877,61 @@ function queryFromRefs(model, query) {
853
877
  return unique(selectFromRefs(model, query));
854
878
  }
855
879
 
856
- function queryRefs(model, query) {
880
+ function queryRefs(model, query, definition) {
857
881
  if (!query.SELECT) {
858
882
  return [];
859
883
  }
860
- return unique(selectRefs(model, query));
884
+ return unique(selectRefs(model, definition, query));
861
885
  }
862
886
 
863
887
  function selectFromRefs(model, query) {
864
888
  let refs = [];
865
- if (query.SELECT.from.SELECT) {
866
- refs = selectFromRefs(model, query.SELECT.from);
867
- } else if (query.SELECT.from.ref) {
889
+ if (query.SELECT.from.ref) {
868
890
  refs = resolveRefs(model, query.SELECT.from.ref);
869
- } else if ((query.SELECT.from.join || query.SELECT.from.SET) && query.SELECT.from.args) {
870
- refs = query.SELECT.from.args.reduce((refs, arg) => {
871
- refs = refs.concat(resolveRefs(model, arg.ref || arg));
872
- return refs;
873
- }, []);
891
+ } else if (query.SELECT.from.args) {
892
+ for (const arg of query.SELECT.from.args) {
893
+ if (arg.ref) {
894
+ refs = refs.concat(resolveRefs(model, arg.ref));
895
+ } else if (arg.args) {
896
+ refs = refs.concat(selectFromRefs(model, { SELECT: { from: { args: arg.args } } }));
897
+ }
898
+ }
899
+ } else if (query.SELECT.from.SET?.args) {
900
+ for (const arg of query.SELECT.from.SET.args) {
901
+ refs = refs.concat(selectFromRefs(model, arg));
902
+ }
903
+ } else if (query.SELECT.from.SELECT) {
904
+ refs = selectFromRefs(model, query.SELECT.from);
874
905
  }
875
906
  return refs;
876
907
  }
877
908
 
878
- function selectFromAliases(model, query) {
909
+ function selectFromPrimaryRef(model, query) {
910
+ if (query.SELECT.from.ref) {
911
+ return resolveRef(model, query.SELECT.from.ref);
912
+ } else if (query.SELECT.from.args) {
913
+ for (const arg of query.SELECT.from.args) {
914
+ if (arg.ref) {
915
+ return resolveRef(model, arg.ref);
916
+ } else if (arg.args) {
917
+ return selectFromPrimaryRef(model, { SELECT: { from: { args: arg.args } } });
918
+ }
919
+ }
920
+ } else if (query.SELECT.from.SET?.args) {
921
+ for (const arg of query.SELECT.from.SET.args) {
922
+ return selectFromPrimaryRef(model, arg);
923
+ }
924
+ } else if (query.SELECT.from.SELECT) {
925
+ return selectFromPrimaryRef(model, query.SELECT.from);
926
+ }
927
+ }
928
+
929
+ function selectFromAliases(model, definition, query) {
879
930
  let aliases = {};
880
- if (query.SELECT.from.SELECT) {
881
- // Sub-select aliases are not (yet) supported
882
- } else if (query.SELECT.from.ref) {
931
+ if (definition?.name) {
932
+ aliases["$self"] = definition.name;
933
+ }
934
+ if (query.SELECT.from.ref) {
883
935
  const ref = resolveRef(model, query.SELECT.from.ref);
884
936
  if (query.SELECT.from.as) {
885
937
  aliases[query.SELECT.from.as] = ref;
@@ -887,39 +939,84 @@ function selectFromAliases(model, query) {
887
939
  const as = ref.split(".").pop();
888
940
  aliases[as] = ref;
889
941
  }
890
- } else if ((query.SELECT.from.join || query.SELECT.from.SET) && query.SELECT.from.args) {
942
+ } else if (query.SELECT.from.args) {
891
943
  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;
944
+ if (arg.ref) {
945
+ const ref = resolveRef(model, arg.ref);
946
+ if (arg.as) {
947
+ aliases[arg.as] = ref;
948
+ } else {
949
+ const as = ref.split(".").pop();
950
+ aliases[as] = ref;
951
+ }
952
+ } else if (arg.args) {
953
+ aliases = {
954
+ ...aliases,
955
+ ...selectFromAliases(model, definition, { SELECT: { from: { args: arg.args } } }),
956
+ };
898
957
  }
899
958
  }
959
+ } else if (query.SELECT.from.SET?.args) {
960
+ for (const arg of query.SELECT.from.SET.args) {
961
+ aliases = {
962
+ ...aliases,
963
+ ...selectFromAliases(model, definition, arg),
964
+ };
965
+ }
966
+ } else if (query.SELECT.from.SELECT) {
967
+ aliases = {
968
+ ...aliases,
969
+ ...selectFromAliases(model, definition, query.SELECT.from),
970
+ };
900
971
  }
901
972
  return aliases;
902
973
  }
903
974
 
904
- function selectRefs(model, query) {
905
- let refs = selectFromRefs(model, query);
906
- const aliases = selectFromAliases(model, query);
907
- if (query._target) {
908
- const target = model.definitions[query._target.name];
909
- if (query.SELECT.orderBy) {
910
- refs = refs.concat(expressionRefs(model, target, query.SELECT.orderBy, query.SELECT.mixin, aliases));
911
- }
912
- if (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));
915
- }
916
- if (query.SELECT.where) {
917
- refs = refs.concat(expressionRefs(model, target, query.SELECT.where, query.SELECT.mixin, aliases));
918
- }
919
- if (query.SELECT.having) {
920
- refs = refs.concat(expressionRefs(model, target, query.SELECT.having, query.SELECT.mixin, aliases));
975
+ function selectOuterAliases(model, target, columns, mixins, aliases) {
976
+ const outerAliases = {};
977
+ if (columns) {
978
+ for (const column of columns) {
979
+ if (column.ref) {
980
+ const as = column.as || column.ref[column.ref.length - 1];
981
+ let current = target;
982
+ for (const ref of column.ref) {
983
+ const currentTarget = targetEntity(model, current, mixins, aliases, ref);
984
+ if (currentTarget != null) {
985
+ current = currentTarget;
986
+ }
987
+ }
988
+ outerAliases[as] = (current || target).name;
989
+ }
921
990
  }
922
991
  }
992
+ return outerAliases;
993
+ }
994
+
995
+ function selectRefs(model, definition, query, aliases) {
996
+ let targetName = selectFromPrimaryRef(model, query);
997
+ const target = targetName ? model.definitions[targetName] : undefined;
998
+ let refs = selectFromRefs(model, query);
999
+ aliases = {
1000
+ ...aliases,
1001
+ ...selectFromAliases(model, definition, query),
1002
+ };
1003
+ if (query.SELECT.columns) {
1004
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.columns, query.SELECT.mixin, aliases));
1005
+ refs = refs.concat(expandRefs(model, target, query.SELECT.columns, query.SELECT.mixin, aliases));
1006
+ }
1007
+ if (query.SELECT.where) {
1008
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.where, query.SELECT.mixin, aliases));
1009
+ }
1010
+ const outerAliases = {
1011
+ ...aliases,
1012
+ ...selectOuterAliases(model, target, query.SELECT.columns, query.SELECT.mixin, aliases),
1013
+ };
1014
+ if (query.SELECT.having) {
1015
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.having, query.SELECT.mixin, outerAliases));
1016
+ }
1017
+ if (query.SELECT.orderBy) {
1018
+ refs = refs.concat(expressionRefs(model, target, query.SELECT.orderBy, query.SELECT.mixin, outerAliases));
1019
+ }
923
1020
  return refs;
924
1021
  }
925
1022
 
@@ -965,11 +1062,14 @@ function identifierRefs(model, definition, expressions, mixins, aliases) {
965
1062
  let current = definition;
966
1063
  let currentMixins = mixins;
967
1064
  for (const ref of expression.ref) {
968
- current = target(model, current, currentMixins, aliases, ref);
969
- if (current !== null) {
970
- currentMixins = {};
1065
+ if (current?.name !== definition?.name) {
971
1066
  refs.push(current.name);
972
1067
  }
1068
+ if (ref.startsWith("$")) {
1069
+ break;
1070
+ }
1071
+ current = targetEntity(model, current, currentMixins, aliases, ref);
1072
+ currentMixins = {};
973
1073
  }
974
1074
  }
975
1075
  }
@@ -984,7 +1084,12 @@ function expressionRefs(model, definition, expressions, mixins, aliases) {
984
1084
  } else if (expression.args) {
985
1085
  refs = refs.concat(expressionRefs(model, definition, expression.args, mixins, aliases));
986
1086
  } else if (expression.SELECT) {
987
- refs = refs.concat(selectRefs(model, expression));
1087
+ refs = refs.concat(
1088
+ selectRefs(model, undefined, expression, {
1089
+ ...aliases,
1090
+ ["$self"]: undefined,
1091
+ }),
1092
+ );
988
1093
  }
989
1094
  }
990
1095
  return refs;
@@ -997,7 +1102,7 @@ function expandRefs(model, definition, columns, mixins, aliases) {
997
1102
  let current = definition;
998
1103
  let currentMixins = mixins;
999
1104
  for (const ref of column.ref) {
1000
- current = target(model, current, currentMixins, aliases, ref);
1105
+ current = targetEntity(model, current, currentMixins, aliases, ref);
1001
1106
  currentMixins = {};
1002
1107
  refs.push(current.name);
1003
1108
  }
@@ -1007,17 +1112,17 @@ function expandRefs(model, definition, columns, mixins, aliases) {
1007
1112
  return refs;
1008
1113
  }
1009
1114
 
1010
- function target(model, entity, mixins, aliases, ref) {
1115
+ function targetEntity(model, entity, mixins, aliases, ref) {
1011
1116
  if (aliases?.[ref]) {
1012
1117
  return typeof aliases[ref] === "string" ? model.definitions[aliases[ref]] : aliases[ref];
1013
1118
  }
1014
- if (entity.name.endsWith(`.${ref}`)) {
1119
+ if (entity?.name.endsWith(`.${ref}`)) {
1015
1120
  return entity;
1016
1121
  }
1017
- const element = entity.elements[ref] || mixins?.[ref];
1122
+ const element = entity?.elements[ref] || mixins?.[ref];
1018
1123
  if (!element) {
1019
1124
  cds.log(Component).warn("Reference not found in entity", {
1020
- entity: entity.name,
1125
+ entity: entity?.name,
1021
1126
  ref,
1022
1127
  });
1023
1128
  }