@cap-js-community/common 0.1.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.
@@ -0,0 +1,961 @@
1
+ "use strict";
2
+
3
+ const os = require("os");
4
+ const path = require("path");
5
+ const fs = require("fs").promises;
6
+
7
+ const cds = require("@sap/cds");
8
+ const SQLiteService = require("@cap-js/sqlite");
9
+
10
+ require("../common/promise");
11
+
12
+ const Component = "replicationCache";
13
+
14
+ const Constants = {
15
+ InMemory: ":memory:",
16
+ Default: "default",
17
+ };
18
+
19
+ const Status = {
20
+ New: "NEW",
21
+ Initialized: "INITIALIZED",
22
+ NotReady: "NOT_READY",
23
+ Ready: "READY",
24
+ Open: "OPEN",
25
+ Failed: "FAILED",
26
+ Invalid: "INVALID",
27
+ };
28
+
29
+ const Annotations = {
30
+ Replicate: "@cds.replicate",
31
+ ReplicateGroup: "@cds.replicate.group",
32
+ ReplicateAuto: "@cds.replicate.auto",
33
+ ReplicateTTL: "@cds.replicate.ttl",
34
+ ReplicatePreload: "@cds.replicate.preload",
35
+ };
36
+
37
+ const Tenant = {
38
+ Default: "default",
39
+ Template: "template",
40
+ };
41
+
42
+ class ReplicationCache {
43
+ constructor(options) {
44
+ this.options = {
45
+ ...(cds.env.replicationCache || {}),
46
+ ...options,
47
+ tmpDirPath: os.tmpdir(),
48
+ };
49
+ this.name = this.options.name;
50
+ this.group = this.options.group;
51
+ this.log = cds.log(Component);
52
+ this.template = null;
53
+ this.cache = new Map();
54
+ this.initStats();
55
+ this.attach();
56
+ }
57
+
58
+ attach() {
59
+ cds.on("loaded", (model) => {
60
+ this.model = model;
61
+ });
62
+ cds.on("connect", (service) => {
63
+ if (service.name === this.name) {
64
+ const refs = ReplicationCache.replicationRefs(this.model, service);
65
+ if (refs.length > 0) {
66
+ this.setup(service, refs);
67
+ this.log.info("using replication cache", {
68
+ service: service.name,
69
+ });
70
+ if (
71
+ this.options.deploy &&
72
+ this.options?.credentials?.database &&
73
+ this.options?.credentials?.database !== Constants.InMemory
74
+ ) {
75
+ this.log.debug("Preparing replication cache template database");
76
+ this.template = createDB(Tenant.Template, this.model, this.options).catch((err) => {
77
+ this.log.error("Preparing replication cache failed", err);
78
+ });
79
+ }
80
+ }
81
+ }
82
+ });
83
+ }
84
+
85
+ static replicationRefs(model, service) {
86
+ const refs = Object.keys(model.definitions).filter((name) => {
87
+ const definition = model.definitions[name];
88
+ return (
89
+ definition.kind === "entity" &&
90
+ !definition.projection &&
91
+ (service.name === "db" || name.startsWith(`${service.name}.`)) &&
92
+ Object.values(Annotations).find((annotation) => {
93
+ return definition[annotation] !== undefined;
94
+ })
95
+ );
96
+ });
97
+ if (refs) {
98
+ for (const ref of refs) {
99
+ if (model.definitions[`${ref}.texts`]) {
100
+ refs.push(`${ref}.texts`);
101
+ }
102
+ }
103
+ }
104
+ return refs;
105
+ }
106
+
107
+ initStats() {
108
+ this.stats = {
109
+ hits: 0,
110
+ used: 0,
111
+ missed: 0,
112
+ errors: 0,
113
+ ratio: 0,
114
+ measureTotal: 0,
115
+ measureCount: 0,
116
+ measureRatio: 0,
117
+ counts: {}, // <String, Number>
118
+ search: {}, // <String, Number>
119
+ localized: {}, // <String, Number>
120
+ projections: {}, // <String, Number>
121
+ notRelevant: {}, // <String, Number>
122
+ };
123
+ }
124
+
125
+ setup(service, refs) {
126
+ this.service = service;
127
+ this.refs = refs.reduce((result, ref) => {
128
+ result[ref] = true;
129
+ return result;
130
+ }, {});
131
+ this.service.prepend(() => {
132
+ this.service.on("READ", this.read.bind(this));
133
+ });
134
+ if (this.options.check > 0) {
135
+ setInterval(async () => {
136
+ try {
137
+ await this.prune();
138
+ } catch (err) {
139
+ this.log.error("Pruning replication cache failed", err);
140
+ }
141
+ }, this.options.check).unref();
142
+ }
143
+ if (this.options.stats > 0) {
144
+ setInterval(async () => {
145
+ try {
146
+ await this.logStats();
147
+ } catch (err) {
148
+ this.log.error("Logging replication cache statistics failed", err);
149
+ }
150
+ }, this.options.stats).unref();
151
+ }
152
+ }
153
+
154
+ async read(req, next) {
155
+ try {
156
+ if (req.query.replication === true || req.query.replicated === false) {
157
+ return await next();
158
+ }
159
+ if (!(await this.active(req.tenant))) {
160
+ return await next();
161
+ }
162
+ this.stats.hits++;
163
+ const model = req.model ?? cds.model;
164
+ if (this.options.preload) {
165
+ req.on("done", () => {
166
+ this.preloadAnnotated(req.tenant, model);
167
+ });
168
+ }
169
+ if (!this.options.search && !this.search(req.query)) {
170
+ return await next();
171
+ }
172
+ let refs = queryRefs(model, req.query);
173
+ if (!this.options.deploy) {
174
+ if (!this.localized(req.query, refs)) {
175
+ return await next();
176
+ }
177
+ if (!this.projections(model, refs)) {
178
+ return await next();
179
+ }
180
+ }
181
+ if (this.options.deploy) {
182
+ refs = baseRefs(model, refs);
183
+ refs = localizedRefs(model, req.query, refs);
184
+ }
185
+ if (refs.length === 0 || !this.relevant(refs)) {
186
+ return await next();
187
+ }
188
+ for (const ref of refs) {
189
+ this.stats.counts[ref] ??= 0;
190
+ this.stats.counts[ref]++;
191
+ }
192
+ const status = await this.load(
193
+ req.tenant,
194
+ refs,
195
+ {
196
+ auto: this.options.auto,
197
+ wait: this.options.wait,
198
+ thread: true,
199
+ },
200
+ model,
201
+ );
202
+ if (status === Status.Ready) {
203
+ this.stats.used++;
204
+ this.stats.ratio = Math.round(this.stats.used / this.stats.hits);
205
+ this.log.debug("Replication cache was used");
206
+ const db = this.cache.get(req.tenant).db;
207
+ if (this.options.measure) {
208
+ return this.measure(
209
+ async () => {
210
+ return db.tx({ ...req.context }, async (tx) => {
211
+ return tx.run(req.query);
212
+ });
213
+ },
214
+ async () => {
215
+ await next();
216
+ },
217
+ );
218
+ }
219
+ return db.tx(
220
+ {
221
+ tenant: req.context.tenant,
222
+ locale: req.context.locale,
223
+ user: req.context.user,
224
+ http: req.context.http,
225
+ },
226
+ async (tx) => {
227
+ return tx.run(req.query);
228
+ },
229
+ );
230
+ }
231
+ } catch (err) {
232
+ this.stats.errors++;
233
+ this.log.error("Reading from replication cache failed", err);
234
+ }
235
+ this.stats.missed++;
236
+ this.stats.ratio = Math.round(this.stats.used / this.stats.hits);
237
+ this.log.debug("Replication cache was not used");
238
+ return await next();
239
+ }
240
+
241
+ relevant(refs) {
242
+ const notRelevantRefs = refs.filter((ref) => !this.refs[ref]);
243
+ if (notRelevantRefs.length === refs.length) {
244
+ this.log.debug("Replication cache not relevant for query including refs", {
245
+ refs,
246
+ });
247
+ return false;
248
+ }
249
+ if (notRelevantRefs.length > 0) {
250
+ for (const ref of notRelevantRefs) {
251
+ this.stats.notRelevant[ref] ??= 0;
252
+ this.stats.notRelevant[ref]++;
253
+ this.log.debug("Replication cache not relevant for query including ref", {
254
+ ref,
255
+ refs,
256
+ });
257
+ }
258
+ return false;
259
+ }
260
+ return true;
261
+ }
262
+
263
+ async preloadAnnotated(tenant, model, preloadRefs) {
264
+ try {
265
+ const refs = [];
266
+ for (const ref in this.refs) {
267
+ if (preloadRefs && !preloadRefs.includes(ref)) {
268
+ continue;
269
+ }
270
+ const definition = model.definitions[ref];
271
+ if (definition[Annotations.ReplicatePreload]) {
272
+ refs.push(ref);
273
+ }
274
+ }
275
+ await this.preload(tenant, refs, model);
276
+ } catch (err) {
277
+ this.log.error("Preload replication cache failed", err);
278
+ }
279
+ }
280
+
281
+ async preload(tenant, refs, model) {
282
+ if (refs.length === 0) {
283
+ return;
284
+ }
285
+ return await this.load(tenant, refs, { auto: true, wait: true, thread: false }, model ?? cds.model);
286
+ }
287
+
288
+ async load(tenant, refs, options, model = cds.model) {
289
+ refs = Array.isArray(refs) ? refs : [refs];
290
+ refs = refs.filter((ref) => !model.definitions[ref].query);
291
+ if (refs.length === 0) {
292
+ return;
293
+ }
294
+ let tenantCache = cached(this.cache, tenant, async () => {
295
+ return new ReplicationCacheTenant(tenant, model, this.options).prepare();
296
+ });
297
+ return (async () => {
298
+ try {
299
+ const prepared = Promise.resolve(tenantCache).then(async (tenantCache) => {
300
+ const prepares = [];
301
+ for (const ref of refs) {
302
+ const entry = cached(tenantCache.cache, ref, () => {
303
+ return new ReplicationCacheEntry(this, tenantCache, ref);
304
+ });
305
+ entry.touched = Date.now();
306
+ if (
307
+ entry.status !== Status.Ready &&
308
+ !(options?.auto === false) &&
309
+ !(model.definitions[ref]?.[Annotations.ReplicateAuto] === false)
310
+ ) {
311
+ prepares.push(entry.prepare(options?.wait && options?.thread));
312
+ }
313
+ }
314
+ return await Promise.allDone(prepares);
315
+ });
316
+ if (!(options?.wait === false)) {
317
+ await prepared;
318
+ tenantCache = await tenantCache;
319
+ }
320
+ if (!(tenantCache instanceof ReplicationCacheTenant)) {
321
+ return Status.NotReady;
322
+ }
323
+ for (const ref of refs) {
324
+ const entry = tenantCache.cache.get(ref);
325
+ if (!entry || entry.status !== Status.Ready) {
326
+ return Status.NotReady;
327
+ }
328
+ }
329
+ return Status.Ready;
330
+ } catch (err) {
331
+ this.stats.errors++;
332
+ this.log.error("Preparing replication cache entry failed", err);
333
+ return Status.NotReady;
334
+ }
335
+ })();
336
+ }
337
+
338
+ async prepared(tenant, ref) {
339
+ const tenants = tenant ? [tenant] : this.cache.keys();
340
+ for (const id of tenants) {
341
+ const tenant = await this.cache.get(id);
342
+ if (tenant) {
343
+ const refs = ref ? [ref] : tenant.cache.keys();
344
+ for (const ref of refs) {
345
+ const entry = tenant.cache.get(ref);
346
+ if (entry) {
347
+ await entry.prepared;
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }
353
+
354
+ async clear(tenant, ref) {
355
+ const tenants = tenant ? [tenant] : this.cache.keys();
356
+ for (const id of tenants) {
357
+ const tenant = await this.cache.get(id);
358
+ if (tenant) {
359
+ const refs = ref ? [ref] : tenant.cache.keys();
360
+ for (const ref of refs) {
361
+ const entry = tenant.cache.get(ref);
362
+ if (entry) {
363
+ await entry.clear();
364
+ this.log.debug("Replication cache cleared", {
365
+ tenant,
366
+ ref,
367
+ size: entry.size,
368
+ touched: entry.touched,
369
+ });
370
+ }
371
+ }
372
+ }
373
+ }
374
+ }
375
+
376
+ async reset() {
377
+ this.initStats();
378
+ await this.clear();
379
+ }
380
+
381
+ async prune(tenant) {
382
+ const maxSize = this.options.size / this.cache.size;
383
+ const tenants = tenant ? [tenant] : this.cache.keys();
384
+ for (const id of tenants) {
385
+ const tenant = await this.cache.get(id);
386
+ const size = await this.size(tenant.id);
387
+ let diff = size - maxSize;
388
+ if (diff > 0) {
389
+ this.log.debug("Replication cache exceeds limit for tenant", {
390
+ tenant,
391
+ diff,
392
+ });
393
+ const refs = [...tenant.cache.keys()];
394
+ refs.sort((ref1, ref2) => tenant.cache.get(ref1).touched - tenant.cache.get(ref2).touched);
395
+ const pruneRefs = [];
396
+ for (const ref of refs) {
397
+ pruneRefs.push(ref);
398
+ const entry = tenant.cache.get(ref);
399
+ if (entry) {
400
+ diff -= entry.size;
401
+ if (diff <= 0) {
402
+ break;
403
+ }
404
+ }
405
+ }
406
+ for (const ref of pruneRefs) {
407
+ const entry = tenant.cache.get(ref);
408
+ this.log.debug("Replication cache prunes ref for tenant", {
409
+ tenant,
410
+ ref,
411
+ size: entry.size,
412
+ touched: entry.touched,
413
+ });
414
+ await entry.clear(tenant, ref);
415
+ }
416
+ }
417
+ }
418
+ }
419
+
420
+ async size(tenant, ref) {
421
+ let size = 0;
422
+ const tenants = tenant ? [tenant] : this.cache.keys();
423
+ for (const id of tenants) {
424
+ const tenant = await this.cache.get(id);
425
+ if (tenant) {
426
+ const refs = ref ? [ref] : tenant.cache.keys();
427
+ for (const ref of refs) {
428
+ const entry = tenant.cache.get(ref);
429
+ if (entry) {
430
+ size += entry.size;
431
+ }
432
+ }
433
+ }
434
+ }
435
+ return size;
436
+ }
437
+
438
+ async tenantSize(id) {
439
+ const tenant = await this.cache.get(id);
440
+ if (tenant) {
441
+ return await tenant.db.tx(async (tx) => {
442
+ const result = await tx.run(
443
+ "select page_size * page_count as bytes from pragma_page_count(), pragma_page_size()",
444
+ );
445
+ return result[0]?.bytes ?? 0;
446
+ });
447
+ }
448
+ return 0;
449
+ }
450
+
451
+ async measure(fnCache, fnService) {
452
+ let timeCache = 0;
453
+ let timeService = 0;
454
+ const [cacheResult] = await Promise.allDone([
455
+ (async () => {
456
+ const start = performance.now();
457
+ const result = await fnCache();
458
+ const end = performance.now();
459
+ timeCache = end - start;
460
+ return result;
461
+ })(),
462
+ (async () => {
463
+ const start = performance.now();
464
+ const result = await fnService();
465
+ const end = performance.now();
466
+ timeService = end - start;
467
+ return result;
468
+ })(),
469
+ ]);
470
+ const percent = ((timeService - timeCache) / timeService) * 100;
471
+ this.log.info("Replication cache measurement", Math.round(percent), timeCache, timeService);
472
+ this.stats.measureTotal += percent;
473
+ this.stats.measureCount += 1;
474
+ this.stats.measureRatio = Math.round(this.stats.measureTotal / this.stats.measureCount);
475
+ return cacheResult;
476
+ }
477
+
478
+ async active(tenant) {
479
+ if (typeof this.options.active === "function") {
480
+ if (!(await this.options.active(tenant))) {
481
+ this.log.debug("Replication cache not enabled for tenant", {
482
+ tenant,
483
+ });
484
+ return false;
485
+ }
486
+ }
487
+ return true;
488
+ }
489
+
490
+ search(query) {
491
+ let search = true;
492
+ if (query.SELECT.search?.length > 0) {
493
+ const ref = query.target.name;
494
+ this.stats.search[ref] ??= 0;
495
+ this.stats.search[ref]++;
496
+ this.log.debug("Replication cache skipped for search", {
497
+ ref,
498
+ });
499
+ search = false;
500
+ }
501
+ return search;
502
+ }
503
+
504
+ localized(query, refs) {
505
+ let localized = true;
506
+ if (query.SELECT.localized) {
507
+ const ref = query.target.name;
508
+ this.stats.localized[ref] ??= 0;
509
+ this.stats.localized[ref]++;
510
+ this.log.debug("Replication cache not enabled for 'localized' without deploy feature", {
511
+ ref,
512
+ });
513
+ localized = false;
514
+ } else {
515
+ for (const ref of refs) {
516
+ if (ref.startsWith("localized.")) {
517
+ this.stats.localized[ref] ??= 0;
518
+ this.stats.localized[ref]++;
519
+ this.log.debug("Replication cache not enabled for 'localized' without deploy feature", {
520
+ ref,
521
+ });
522
+ localized = false;
523
+ }
524
+ }
525
+ }
526
+ return localized;
527
+ }
528
+
529
+ projections(model, refs) {
530
+ let projections = true;
531
+ for (const ref of refs) {
532
+ const definition = model.definitions[ref];
533
+ if (definition.query) {
534
+ this.stats.projections[ref] ??= 0;
535
+ this.stats.projections[ref]++;
536
+ this.log.debug("Replication cache not enabled for 'projections' without deploy feature", {
537
+ ref,
538
+ });
539
+ projections = false;
540
+ }
541
+ }
542
+ return projections;
543
+ }
544
+
545
+ async logStats() {
546
+ this.log.info("Replication cache statistics", this.stats);
547
+ this.log.info("Replication cache size", await this.size());
548
+ }
549
+ }
550
+
551
+ class ReplicationCacheTenant {
552
+ constructor(tenant, model, options) {
553
+ this.id = tenant;
554
+ this.model = model;
555
+ this.options = options;
556
+ this.csn = model.definitions;
557
+ this.cache = new Map();
558
+ }
559
+
560
+ async prepare() {
561
+ this.db = await createDB(this.id, this.model, this.options);
562
+ return this;
563
+ }
564
+ }
565
+
566
+ class ReplicationCacheEntry {
567
+ constructor(cache, tenant, ref) {
568
+ this.cache = cache;
569
+ this.service = cache.service;
570
+ this.tenant = tenant;
571
+ this.csn = tenant.csn;
572
+ this.db = tenant.db;
573
+ this.ref = ref;
574
+ this.definition = this.csn[ref];
575
+ this.preload = this.cache.options.preload && this.definition[Annotations.ReplicatePreload];
576
+ this.name = this.definition.name.replace(/\./gi, "_");
577
+ this.status = Status.New;
578
+ this.failures = 0;
579
+ this.touched = Date.now();
580
+ this.timestamp = Date.now();
581
+ this.timeout = null;
582
+ this.ttl = this.definition[Annotations.ReplicateTTL] || this.cache.options.ttl;
583
+ this.size = 0; // bytes
584
+ this.preparing = null;
585
+ this.prepared = null;
586
+ }
587
+
588
+ async prepare(thread) {
589
+ if (!this.preparing) {
590
+ this.prepared = this.preparing = (async () => {
591
+ this.cache.log.debug("Preparing replication cache ref started", {
592
+ tenant: this.tenant.id,
593
+ ref: this.ref,
594
+ });
595
+ try {
596
+ await this.cache.template;
597
+ if ([Status.New].includes(this.status)) {
598
+ await this.initialize();
599
+ }
600
+ if ([Status.Initialized, Status.Open, Status.Failed].includes(this.status)) {
601
+ await this.load(thread);
602
+ this.status = Status.Ready;
603
+ this.failures = 0;
604
+ this.timeout = setTimeout(async () => {
605
+ this.cache.log.debug("Replication cache ref TTL reached", {
606
+ tenant: this.tenant.id,
607
+ ref: this.ref,
608
+ });
609
+ await this.clear(true);
610
+ }, this.ttl).unref();
611
+ }
612
+ this.cache.log.debug("Preparing replication cache ref finished", {
613
+ tenant: this.tenant.id,
614
+ ref: this.ref,
615
+ });
616
+ if (this.cache.options.prune) {
617
+ this.cache.prune(this.tenant.id).catch((err) => {
618
+ this.cache.log.error("Pruning replication cache failed", err);
619
+ });
620
+ }
621
+ } catch (err) {
622
+ this.status = Status.Failed;
623
+ this.failures++;
624
+ if (this.failures > this.cache.options.retries) {
625
+ this.status = Status.Invalid;
626
+ }
627
+ throw err;
628
+ }
629
+ this.preparing = null;
630
+ })();
631
+ }
632
+ return this.preparing;
633
+ }
634
+
635
+ async initialize() {
636
+ if (!this.cache.options.deploy) {
637
+ const csn = {
638
+ definitions: {
639
+ [this.definition.name]: {
640
+ name: this.definition.name,
641
+ kind: "entity",
642
+ elements: Object.keys(this.definition.elements).reduce((result, name) => {
643
+ const element = this.definition.elements[name];
644
+ if (element.type !== "cds.Association" && element.type !== "cds.Composition") {
645
+ result[name] = element;
646
+ }
647
+ return result;
648
+ }, {}),
649
+ },
650
+ },
651
+ };
652
+ const ddl = cds.compile(csn).to.sql({ dialect: "sqlite" })?.[0]?.replace(/\n/g, "");
653
+ this.name = /CREATE (TABLE|VIEW) ([^ ]*?) /.exec(ddl)?.[2];
654
+ await this.db.tx(async (tx) => {
655
+ let result = await tx.run("SELECT name FROM sqlite_schema WHERE type = 'table' and name = ?", [this.name]);
656
+ if (result.length === 0) {
657
+ await tx.run(ddl);
658
+ }
659
+ });
660
+ }
661
+ this.status = Status.Initialized;
662
+ this.timestamp = Date.now();
663
+ }
664
+
665
+ async load(thread) {
666
+ this.timestamp = Date.now();
667
+ await this.clear();
668
+ if (thread && cds.context && this.service instanceof SQLiteService) {
669
+ const srcTx = this.service.tx(cds.context);
670
+ await this.db.tx({ tenant: this.tenant.id }, async (destTx) => {
671
+ await this.loadChunked(srcTx, destTx);
672
+ await this.checkRecords(srcTx, destTx);
673
+ await this.calcSize(destTx);
674
+ });
675
+ } else {
676
+ await this.service.tx({ tenant: this.tenant.id }, async (srcTx) => {
677
+ await this.db.tx({ tenant: this.tenant.id }, async (destTx) => {
678
+ await this.loadChunked(srcTx, destTx);
679
+ await this.checkRecords(srcTx, destTx);
680
+ await this.calcSize(destTx);
681
+ });
682
+ });
683
+ }
684
+ this.timestamp = Date.now();
685
+ }
686
+
687
+ async loadChunked(srcTx, destTx) {
688
+ const keys = Object.keys(this.definition.keys);
689
+ const selectQuery = SELECT.from(this.definition).orderBy(keys);
690
+ selectQuery.replication = true;
691
+ const chunkSize = this.cache.options.chunks;
692
+ let offset = 0;
693
+ let entries = [];
694
+ do {
695
+ entries = await srcTx.run(selectQuery.limit(chunkSize, offset));
696
+ if (entries.length > 0) {
697
+ const insertQuery = INSERT.into(this.definition).entries(entries);
698
+ const result = await destTx.run(insertQuery);
699
+ if (this.cache.options.validate) {
700
+ if (isNaN(Number(result)) || isNaN(entries?.length) || Number(result) !== entries.length) {
701
+ this.cache.log.debug("Loading replication cache failed. Number of inserted entries does not match.", {
702
+ ref: this.ref,
703
+ entries: entries.length,
704
+ result: result.valueOf(),
705
+ });
706
+ throw new Error("Loading replication cache failed. Number of inserted entries does not match.");
707
+ }
708
+ }
709
+ offset += chunkSize;
710
+ }
711
+ } while (entries.length > 0);
712
+ }
713
+
714
+ async checkRecords(srcTx, destTx) {
715
+ if (this.cache.options.validate) {
716
+ const countQuery = SELECT.one.from(this.definition).columns("count(*) as count");
717
+ countQuery.replication = true;
718
+ const srcCount = (await srcTx.run(countQuery))?.count;
719
+ const destCount = (await destTx.run(countQuery))?.count;
720
+ if (isNaN(srcCount) || isNaN(destCount) || srcCount !== destCount) {
721
+ this.cache.log.debug("Loading replication cache failed. Number of inserted entries does not match.", {
722
+ ref: this.ref,
723
+ entries: srcCount,
724
+ result: destCount,
725
+ });
726
+ throw new Error("Loading replication cache failed. Number of inserted entries does not match.");
727
+ }
728
+ }
729
+ }
730
+
731
+ async calcSize(tx) {
732
+ const result = await tx.run("select sum(pgsize) as bytes from dbstat where name = ?", [this.name]);
733
+ const bytes = result[0]?.bytes;
734
+ this.size = bytes <= 4096 ? 0 : bytes;
735
+ }
736
+
737
+ async clear(ttl) {
738
+ this.status = Status.Open;
739
+ if (this.timeout) {
740
+ clearTimeout(this.timeout);
741
+ this.timeout = null;
742
+ }
743
+ await this.db.tx(async (tx) => {
744
+ await tx.run("DELETE from " + this.name);
745
+ this.size = this.calcSize(tx);
746
+ });
747
+ this.timestamp = Date.now();
748
+ if (ttl && this.preload) {
749
+ this.cache.preloadAnnotated(this.tenant.id, this.cache.model, [this.ref]);
750
+ }
751
+ }
752
+ }
753
+
754
+ module.exports = ReplicationCache;
755
+
756
+ async function createDB(tenant, model, options) {
757
+ const filePath = await dbPath(tenant, options);
758
+ cds.log(Component).debug("Preparing replication cache database", {
759
+ tenant,
760
+ file: filePath,
761
+ });
762
+ if (options.deploy && filePath !== Constants.InMemory && tenant !== Tenant.Template) {
763
+ const templateDatabase = await dbPath(Tenant.Template, options);
764
+ await fs.copyFile(templateDatabase, filePath);
765
+ }
766
+ const db = new SQLiteService(tenant ?? Tenant.Default, model, {
767
+ kind: "sqlite",
768
+ impl: "@cap-js/sqlite",
769
+ credentials: { ...options.credentials, database: filePath },
770
+ });
771
+ await db.init();
772
+ if (options.deploy && (filePath === Constants.InMemory || tenant === Tenant.Template)) {
773
+ await db.tx(async () => {
774
+ await cds.deploy(filePath === Constants.InMemory ? "*" : model, undefined, []).to(db);
775
+ });
776
+ if (tenant === Tenant.Template) {
777
+ await db.disconnect(); // Close to finalize template file before copying
778
+ }
779
+ }
780
+ return db;
781
+ }
782
+
783
+ async function dbPath(tenant, options) {
784
+ let filePath = options.credentials?.database ?? Constants.InMemory;
785
+ if (filePath !== Constants.InMemory) {
786
+ const dir = path.join(
787
+ options.tmpDir ? options.tmpDirPath : process.cwd(),
788
+ options.baseDir ?? "",
789
+ options.name ?? "",
790
+ options.group ?? "",
791
+ );
792
+ await fs.mkdir(dir, { recursive: true });
793
+ if (tenant) {
794
+ const parts = filePath.split(".");
795
+ const extension = parts.pop();
796
+ filePath = path.join(dir, `${parts.join(".")}-${tenant}.${extension}`);
797
+ } else {
798
+ filePath = path.join(dir, filePath);
799
+ }
800
+ }
801
+ return filePath;
802
+ }
803
+
804
+ function baseRefs(model, refs) {
805
+ const baseRefs = [];
806
+ let currentRefs = refs;
807
+ let nextRefs = [];
808
+ while (currentRefs.length > 0) {
809
+ for (const ref of currentRefs) {
810
+ const definition = model.definitions[ref];
811
+ if (!definition.query) {
812
+ baseRefs.push(ref);
813
+ } else {
814
+ nextRefs = nextRefs.concat(queryRefs(model, definition.query));
815
+ }
816
+ }
817
+ currentRefs = nextRefs;
818
+ nextRefs = [];
819
+ }
820
+ return unique(baseRefs);
821
+ }
822
+
823
+ function localizedRefs(model, query, refs) {
824
+ if (!query.SELECT.localized) {
825
+ return refs;
826
+ }
827
+ const localizedRefs = [];
828
+ for (const ref of refs) {
829
+ if (model.definitions[`${ref}.texts`]) {
830
+ localizedRefs.push(`${ref}.texts`);
831
+ }
832
+ }
833
+ return unique(refs.concat(localizedRefs));
834
+ }
835
+
836
+ function queryRefs(model, query) {
837
+ if (!query.SELECT) {
838
+ return [];
839
+ }
840
+ return unique(fromRefs(model, query));
841
+ }
842
+
843
+ function fromRefs(model, query) {
844
+ let refs = [];
845
+ if (query.SELECT.from.SELECT) {
846
+ refs = fromRefs(model, query.SELECT.from);
847
+ } else if (query.SELECT.from.ref) {
848
+ refs = resolveRefs(model, query.SELECT.from.ref);
849
+ } else if ((query.SELECT.from.join || query.SELECT.from.SET) && query.SELECT.from.args) {
850
+ refs = query.SELECT.from.args.reduce((refs, arg) => {
851
+ refs = refs.concat(resolveRefs(model, arg.ref || arg));
852
+ return refs;
853
+ }, []);
854
+ }
855
+ if (query.target) {
856
+ if (query.SELECT.orderBy) {
857
+ refs = refs.concat(expressionRefs(model, query.target, query.SELECT.orderBy));
858
+ }
859
+ if (query.SELECT.columns) {
860
+ refs = refs.concat(expressionRefs(model, query.target, query.SELECT.columns));
861
+ refs = refs.concat(expandRefs(model, query.target, query.SELECT.columns));
862
+ }
863
+ if (query.SELECT.where) {
864
+ refs = refs.concat(expressionRefs(model, query.target, query.SELECT.where));
865
+ }
866
+ if (query.SELECT.having) {
867
+ refs = refs.concat(expressionRefs(model, query.target, query.SELECT.having));
868
+ }
869
+ }
870
+ return refs;
871
+ }
872
+
873
+ function resolveRefs(model, refs) {
874
+ let resolvedRefs = [];
875
+ let ref = refs[0];
876
+ if (ref.id) {
877
+ if (ref.where) {
878
+ const definition = model.definitions[ref.id];
879
+ resolvedRefs = resolvedRefs.concat(expressionRefs(model, definition, ref.where));
880
+ }
881
+ ref = ref.id;
882
+ }
883
+ resolvedRefs.push(ref);
884
+ let current = model.definitions[ref];
885
+ for (const ref of refs.slice(1)) {
886
+ if (current.elements[ref].type === "cds.Association" || current.elements[ref].type === "cds.Composition") {
887
+ current = current.elements[ref]._target;
888
+ resolvedRefs.push(current.name);
889
+ }
890
+ }
891
+ return resolvedRefs;
892
+ }
893
+
894
+ function identifierRefs(model, definition, array) {
895
+ let refs = [];
896
+ for (const entry of array) {
897
+ if (Array.isArray(entry.ref)) {
898
+ let current = definition;
899
+ for (const ref of entry.ref) {
900
+ if (current.elements[ref].type === "cds.Association" || current.elements[ref].type === "cds.Composition") {
901
+ current = current.elements[ref]._target;
902
+ refs.push(current.name);
903
+ }
904
+ }
905
+ }
906
+ }
907
+ return refs;
908
+ }
909
+
910
+ function expressionRefs(model, definition, array) {
911
+ let refs = identifierRefs(model, definition, array);
912
+ for (const entry of array) {
913
+ if (entry.xpr) {
914
+ refs = refs.concat(expressionRefs(model, definition, entry.xpr));
915
+ } else if (entry.args) {
916
+ refs = refs.concat(expressionRefs(model, definition, entry.args));
917
+ } else if (entry.SELECT) {
918
+ refs = refs.concat(fromRefs(model, entry));
919
+ }
920
+ }
921
+ return refs;
922
+ }
923
+
924
+ function expandRefs(model, definition, columns) {
925
+ let refs = [];
926
+ for (const column of columns) {
927
+ if (Array.isArray(column.ref) && column.expand) {
928
+ let current = definition;
929
+ for (const ref of column.ref) {
930
+ current = current.elements[ref]._target;
931
+ refs.push(current.name);
932
+ }
933
+ refs = refs.concat(expandRefs(model, current, column.expand));
934
+ }
935
+ }
936
+ return refs;
937
+ }
938
+
939
+ function cached(cache, field, init) {
940
+ try {
941
+ if (init && !cache.get(field)) {
942
+ cache.set(field, init());
943
+ }
944
+ const result = cache.get(field);
945
+ (async () => {
946
+ try {
947
+ cache.set(field, await result);
948
+ } catch {
949
+ cache.delete(field);
950
+ }
951
+ })();
952
+ return cache.get(field);
953
+ } catch (err) {
954
+ cache.delete(field);
955
+ throw err;
956
+ }
957
+ }
958
+
959
+ function unique(array) {
960
+ return [...new Set(array)].sort();
961
+ }