webspresso 0.0.64 → 0.0.66
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 +97 -2
- package/core/auth/middleware.js +3 -3
- package/core/orm/cache/fingerprint.js +73 -0
- package/core/orm/cache/index.js +73 -0
- package/core/orm/cache/layer.js +314 -0
- package/core/orm/cache/listeners.js +67 -0
- package/core/orm/cache/memory-provider.js +109 -0
- package/core/orm/cache/types.js +27 -0
- package/core/orm/index.js +19 -6
- package/core/orm/model.js +2 -0
- package/core/orm/query-builder.js +206 -59
- package/core/orm/repository.js +134 -75
- package/core/orm/types.js +21 -0
- package/index.d.ts +46 -1
- package/index.js +2 -1
- package/package.json +1 -1
- package/plugins/index.js +2 -0
- package/plugins/orm-cache-admin/admin-component.js +146 -0
- package/plugins/orm-cache-admin/api-handlers.js +78 -0
- package/plugins/orm-cache-admin/index.js +72 -0
- package/plugins/site-analytics/admin-component.js +34 -4
- package/plugins/site-analytics/api-handlers.js +74 -1
- package/plugins/site-analytics/index.js +1 -0
- package/src/file-router.js +61 -12
- package/templates/skills/webspresso-usage/SKILL.md +3 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory CacheProvider (process-local, tag-based invalidation)
|
|
3
|
+
* @module core/orm/cache/memory-provider
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} MemoryCacheProviderOptions
|
|
8
|
+
* @property {number} [maxEntries=10000] - Max stored keys; FIFO evict on overflow
|
|
9
|
+
* @property {number} [defaultTtlMs=0] - Default TTL (0 = no expiry)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {MemoryCacheProviderOptions} [options]
|
|
14
|
+
* @returns {import('./types').CacheProvider}
|
|
15
|
+
*/
|
|
16
|
+
function createMemoryCacheProvider(options = {}) {
|
|
17
|
+
const maxEntries = options.maxEntries ?? 10000;
|
|
18
|
+
const defaultTtlMs = options.defaultTtlMs ?? 0;
|
|
19
|
+
|
|
20
|
+
/** @type {Map<string, { value: *, expiresAt: number, tags: Set<string> }>} */
|
|
21
|
+
const store = new Map();
|
|
22
|
+
/** @type {Map<string, Set<string>>} */
|
|
23
|
+
const tagToKeys = new Map();
|
|
24
|
+
/** @type {Map<string, Set<string>>} */
|
|
25
|
+
const keyToTags = new Map();
|
|
26
|
+
|
|
27
|
+
function removeKey(key) {
|
|
28
|
+
const tags = keyToTags.get(key);
|
|
29
|
+
if (tags) {
|
|
30
|
+
for (const t of tags) {
|
|
31
|
+
const set = tagToKeys.get(t);
|
|
32
|
+
if (set) {
|
|
33
|
+
set.delete(key);
|
|
34
|
+
if (set.size === 0) tagToKeys.delete(t);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
keyToTags.delete(key);
|
|
39
|
+
store.delete(key);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function get(key) {
|
|
43
|
+
const entry = store.get(key);
|
|
44
|
+
if (!entry) return undefined;
|
|
45
|
+
if (entry.expiresAt > 0 && Date.now() > entry.expiresAt) {
|
|
46
|
+
removeKey(key);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return entry.value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function set(key, value, opts = {}) {
|
|
53
|
+
const tags = Array.isArray(opts.tags) ? opts.tags : [];
|
|
54
|
+
const ttlMs = opts.ttlMs ?? defaultTtlMs;
|
|
55
|
+
|
|
56
|
+
if (store.has(key)) removeKey(key);
|
|
57
|
+
|
|
58
|
+
if (maxEntries > 0 && store.size >= maxEntries) {
|
|
59
|
+
const first = store.keys().next().value;
|
|
60
|
+
if (first !== undefined) removeKey(first);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const expiresAt = ttlMs > 0 ? Date.now() + ttlMs : 0;
|
|
64
|
+
const tagSet = new Set(tags);
|
|
65
|
+
store.set(key, { value, expiresAt, tags: tagSet });
|
|
66
|
+
keyToTags.set(key, new Set(tags));
|
|
67
|
+
|
|
68
|
+
for (const t of tags) {
|
|
69
|
+
if (!tagToKeys.has(t)) tagToKeys.set(t, new Set());
|
|
70
|
+
tagToKeys.get(t).add(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function invalidateTags(tags) {
|
|
75
|
+
const toDelete = new Set();
|
|
76
|
+
for (const t of tags) {
|
|
77
|
+
const keys = tagToKeys.get(t);
|
|
78
|
+
if (!keys) continue;
|
|
79
|
+
for (const k of keys) toDelete.add(k);
|
|
80
|
+
}
|
|
81
|
+
for (const k of toDelete) removeKey(k);
|
|
82
|
+
for (const t of tags) tagToKeys.delete(t);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function clear() {
|
|
86
|
+
store.clear();
|
|
87
|
+
tagToKeys.clear();
|
|
88
|
+
keyToTags.clear();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getSizeStats() {
|
|
92
|
+
return {
|
|
93
|
+
entries: store.size,
|
|
94
|
+
tags: tagToKeys.size,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
get,
|
|
100
|
+
set,
|
|
101
|
+
invalidateTags,
|
|
102
|
+
clear,
|
|
103
|
+
getSizeStats,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
createMemoryCacheProvider,
|
|
109
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} CacheSetOptions
|
|
3
|
+
* @property {string[]} [tags]
|
|
4
|
+
* @property {number} [ttlMs]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} CacheProvider
|
|
9
|
+
* @property {(key: string) => *|undefined} get
|
|
10
|
+
* @property {(key: string, value: *, opts?: CacheSetOptions) => void} set
|
|
11
|
+
* @property {(tags: string[]) => void} invalidateTags
|
|
12
|
+
* @property {() => void} clear
|
|
13
|
+
* @property {() => { entries: number, tags: number }} getSizeStats
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {'auto'|'smart'} CacheStrategy
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} OrmCacheConfigResolved
|
|
22
|
+
* @property {boolean} enabled
|
|
23
|
+
* @property {CacheStrategy} defaultStrategy
|
|
24
|
+
* @property {import('./memory-provider').MemoryCacheProviderOptions} [memoryOptions]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
module.exports = {};
|
package/core/orm/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const { createScopeContext } = require('./scopes');
|
|
|
15
15
|
const { ModelEvents, Hooks, HookCancellationError, createEventContext } = require('./events');
|
|
16
16
|
const { omitHiddenColumns, sanitizeForOutput } = require('./utils');
|
|
17
17
|
const { generateNanoid, zodNanoid, extendZ } = require('./utils/nanoid');
|
|
18
|
+
const { createOrmCacheFromConfig, unregisterOrmCacheListeners } = require('./cache');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Create a database instance
|
|
@@ -93,6 +94,10 @@ function createDatabase(config) {
|
|
|
93
94
|
const migrationConfig = config.migrations || {};
|
|
94
95
|
const migrate = createMigrationManager(knexInstance, migrationConfig);
|
|
95
96
|
|
|
97
|
+
const { layer: ormCacheLayer, publicApi: ormCachePublicApi } = createOrmCacheFromConfig(
|
|
98
|
+
config.cache
|
|
99
|
+
);
|
|
100
|
+
|
|
96
101
|
// Auto-load models from models directory
|
|
97
102
|
const modelsDir = config.models || './models';
|
|
98
103
|
const absoluteModelsDir = path.resolve(process.cwd(), modelsDir);
|
|
@@ -180,7 +185,7 @@ function createDatabase(config) {
|
|
|
180
185
|
const model = getModelInstance(modelName);
|
|
181
186
|
// Always create fresh scope context if not provided to avoid shared state
|
|
182
187
|
const ctx = scopeContext || createScopeContext();
|
|
183
|
-
return createRepository(model, knexInstance, ctx);
|
|
188
|
+
return createRepository(model, knexInstance, ctx, ormCacheLayer);
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
/**
|
|
@@ -192,7 +197,7 @@ function createDatabase(config) {
|
|
|
192
197
|
function query(modelName, scopeContext) {
|
|
193
198
|
const model = getModelInstance(modelName);
|
|
194
199
|
const ctx = scopeContext || createScopeContext();
|
|
195
|
-
const repo = createRepository(model, knexInstance, ctx);
|
|
200
|
+
const repo = createRepository(model, knexInstance, ctx, ormCacheLayer);
|
|
196
201
|
return repo.query();
|
|
197
202
|
}
|
|
198
203
|
|
|
@@ -212,7 +217,7 @@ function createDatabase(config) {
|
|
|
212
217
|
*/
|
|
213
218
|
function createRepositoryFromModel(model, scopeContext) {
|
|
214
219
|
const ctx = scopeContext || createScopeContext();
|
|
215
|
-
return createRepository(model, knexInstance, ctx);
|
|
220
|
+
return createRepository(model, knexInstance, ctx, ormCacheLayer);
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
/**
|
|
@@ -228,11 +233,11 @@ function createDatabase(config) {
|
|
|
228
233
|
getRepository(modelName, scopeContext) {
|
|
229
234
|
const model = getModelInstance(modelName);
|
|
230
235
|
const ctx = scopeContext || createScopeContext();
|
|
231
|
-
return createRepository(model, trx, ctx);
|
|
236
|
+
return createRepository(model, trx, ctx, ormCacheLayer);
|
|
232
237
|
},
|
|
233
238
|
createRepository(model, scopeContext) {
|
|
234
239
|
const ctx = scopeContext || createScopeContext();
|
|
235
|
-
return createRepository(model, trx, ctx);
|
|
240
|
+
return createRepository(model, trx, ctx, ormCacheLayer);
|
|
236
241
|
},
|
|
237
242
|
};
|
|
238
243
|
return callback(trxContext);
|
|
@@ -251,7 +256,11 @@ function createDatabase(config) {
|
|
|
251
256
|
query,
|
|
252
257
|
transaction,
|
|
253
258
|
createSeeder: createSeederInstance,
|
|
254
|
-
|
|
259
|
+
cache: ormCachePublicApi,
|
|
260
|
+
destroy: () => {
|
|
261
|
+
if (ormCacheLayer) unregisterOrmCacheListeners(ormCacheLayer);
|
|
262
|
+
return knexInstance.destroy();
|
|
263
|
+
},
|
|
255
264
|
};
|
|
256
265
|
}
|
|
257
266
|
|
|
@@ -262,6 +271,10 @@ const zdb = z ? createSchemaHelpers(z) : null;
|
|
|
262
271
|
module.exports = {
|
|
263
272
|
// Main factory
|
|
264
273
|
createDatabase,
|
|
274
|
+
// ORM cache (provider + layer utilities)
|
|
275
|
+
createMemoryCacheProvider: require('./cache/memory-provider').createMemoryCacheProvider,
|
|
276
|
+
OrmCacheLayer: require('./cache/layer').OrmCacheLayer,
|
|
277
|
+
createOrmCacheFromConfig: require('./cache').createOrmCacheFromConfig,
|
|
265
278
|
// Schema helpers
|
|
266
279
|
zdb,
|
|
267
280
|
createSchemaHelpers,
|
package/core/orm/model.js
CHANGED
|
@@ -30,6 +30,7 @@ function defineModel(options) {
|
|
|
30
30
|
rest = {},
|
|
31
31
|
hooks = {},
|
|
32
32
|
hidden = [],
|
|
33
|
+
cache: cacheOption,
|
|
33
34
|
} = options;
|
|
34
35
|
|
|
35
36
|
// Validate required fields
|
|
@@ -97,6 +98,7 @@ function defineModel(options) {
|
|
|
97
98
|
},
|
|
98
99
|
hidden: Array.isArray(hidden) ? hidden : [],
|
|
99
100
|
hooks: {},
|
|
101
|
+
cache: cacheOption,
|
|
100
102
|
};
|
|
101
103
|
|
|
102
104
|
// Register model
|
|
@@ -21,8 +21,8 @@ const {
|
|
|
21
21
|
* @param {import('./types').ScopeContext} [initialContext] - Initial scope context
|
|
22
22
|
* @returns {QueryBuilder}
|
|
23
23
|
*/
|
|
24
|
-
function createQueryBuilder(model, knex, initialContext) {
|
|
25
|
-
return new QueryBuilder(model, knex, initialContext);
|
|
24
|
+
function createQueryBuilder(model, knex, initialContext, cacheLayer = null) {
|
|
25
|
+
return new QueryBuilder(model, knex, initialContext, cacheLayer);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -34,11 +34,13 @@ class QueryBuilder {
|
|
|
34
34
|
* @param {import('./types').ModelDefinition} model
|
|
35
35
|
* @param {import('knex').Knex|import('knex').Knex.Transaction} knex
|
|
36
36
|
* @param {import('./types').ScopeContext} [initialContext]
|
|
37
|
+
* @param {import('./cache/layer').OrmCacheLayer|null} [cacheLayer]
|
|
37
38
|
*/
|
|
38
|
-
constructor(model, knex, initialContext) {
|
|
39
|
+
constructor(model, knex, initialContext, cacheLayer = null) {
|
|
39
40
|
this.model = model;
|
|
40
41
|
this.knex = knex;
|
|
41
42
|
this.scopeContext = initialContext || createScopeContext();
|
|
43
|
+
this.cacheLayer = cacheLayer;
|
|
42
44
|
this.jsonColumns = getJsonColumns(model);
|
|
43
45
|
|
|
44
46
|
/** @type {import('./types').QueryState} */
|
|
@@ -376,24 +378,64 @@ class QueryBuilder {
|
|
|
376
378
|
*/
|
|
377
379
|
async first() {
|
|
378
380
|
const ctx = createEventContext(this.model.name, 'find', this._hookTrx());
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
381
|
+
const self = this;
|
|
382
|
+
|
|
383
|
+
async function loadFromDb() {
|
|
384
|
+
await ModelEvents.emitAsync(self.model.name, Hooks.BEFORE_FIND, {}, ctx);
|
|
385
|
+
if (ctx.isCancelled) {
|
|
386
|
+
throw new HookCancellationError(ctx.cancelReason, self.model.name, Hooks.BEFORE_FIND);
|
|
387
|
+
}
|
|
383
388
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
389
|
+
const qb = self.toKnex().first();
|
|
390
|
+
const result = await qb;
|
|
391
|
+
if (!result) return null;
|
|
387
392
|
|
|
388
|
-
|
|
393
|
+
deserializeJsonFields(result, self.jsonColumns);
|
|
394
|
+
|
|
395
|
+
const withs = self.getWiths();
|
|
396
|
+
if (withs.length > 0) {
|
|
397
|
+
await loadRelations([result], ensureArray(withs), self.model, self.knex, self.scopeContext);
|
|
398
|
+
}
|
|
389
399
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
await loadRelations([result], ensureArray(withs), this.model, this.knex, this.scopeContext);
|
|
400
|
+
ModelEvents.emit(self.model.name, Hooks.AFTER_FIND, result, ctx);
|
|
401
|
+
return result;
|
|
393
402
|
}
|
|
394
403
|
|
|
395
|
-
|
|
396
|
-
|
|
404
|
+
if (
|
|
405
|
+
self.cacheLayer &&
|
|
406
|
+
self.cacheLayer.strategyFor(self.model)
|
|
407
|
+
) {
|
|
408
|
+
const strat = self.cacheLayer.strategyFor(self.model);
|
|
409
|
+
const classification = self.cacheLayer.classifyQueryBuilder(self.model, self.state, 'first');
|
|
410
|
+
if (!classification.cacheable) {
|
|
411
|
+
self.cacheLayer.metrics.bypassed += 1;
|
|
412
|
+
return loadFromDb();
|
|
413
|
+
}
|
|
414
|
+
const kind = classification.kind === 'pk' ? 'pk' : 'collection';
|
|
415
|
+
const fingerprint = self.cacheLayer.queryBuilderFingerprint(
|
|
416
|
+
self.model,
|
|
417
|
+
self.scopeContext,
|
|
418
|
+
self.state,
|
|
419
|
+
'first'
|
|
420
|
+
);
|
|
421
|
+
const tags = self.cacheLayer.buildReadTags(
|
|
422
|
+
self.model,
|
|
423
|
+
strat,
|
|
424
|
+
kind,
|
|
425
|
+
classification.kind === 'pk' ? classification.pkValue : null
|
|
426
|
+
);
|
|
427
|
+
return self.cacheLayer.wrapRead(
|
|
428
|
+
self.model,
|
|
429
|
+
self.knex,
|
|
430
|
+
self.scopeContext,
|
|
431
|
+
fingerprint,
|
|
432
|
+
tags,
|
|
433
|
+
loadFromDb,
|
|
434
|
+
(r) => r != null
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return loadFromDb();
|
|
397
439
|
}
|
|
398
440
|
|
|
399
441
|
/**
|
|
@@ -402,26 +444,57 @@ class QueryBuilder {
|
|
|
402
444
|
*/
|
|
403
445
|
async list() {
|
|
404
446
|
const ctx = createEventContext(this.model.name, 'find', this._hookTrx());
|
|
405
|
-
|
|
406
|
-
if (ctx.isCancelled) {
|
|
407
|
-
throw new HookCancellationError(ctx.cancelReason, this.model.name, Hooks.BEFORE_FIND);
|
|
408
|
-
}
|
|
447
|
+
const self = this;
|
|
409
448
|
|
|
410
|
-
|
|
449
|
+
async function loadFromDb() {
|
|
450
|
+
await ModelEvents.emitAsync(self.model.name, Hooks.BEFORE_FIND, {}, ctx);
|
|
451
|
+
if (ctx.isCancelled) {
|
|
452
|
+
throw new HookCancellationError(ctx.cancelReason, self.model.name, Hooks.BEFORE_FIND);
|
|
453
|
+
}
|
|
411
454
|
|
|
412
|
-
|
|
413
|
-
deserializeJsonFields(record, this.jsonColumns);
|
|
414
|
-
}
|
|
455
|
+
const results = await self.toKnex();
|
|
415
456
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
457
|
+
for (const record of results) {
|
|
458
|
+
deserializeJsonFields(record, self.jsonColumns);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const withs = self.getWiths();
|
|
462
|
+
if (withs.length > 0 && results.length > 0) {
|
|
463
|
+
await loadRelations(results, ensureArray(withs), self.model, self.knex, self.scopeContext);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
for (const record of results) {
|
|
467
|
+
ModelEvents.emit(self.model.name, Hooks.AFTER_FIND, record, ctx);
|
|
468
|
+
}
|
|
469
|
+
return results;
|
|
419
470
|
}
|
|
420
471
|
|
|
421
|
-
|
|
422
|
-
|
|
472
|
+
if (self.cacheLayer && self.cacheLayer.strategyFor(self.model)) {
|
|
473
|
+
const strat = self.cacheLayer.strategyFor(self.model);
|
|
474
|
+
const classification = self.cacheLayer.classifyQueryBuilder(self.model, self.state, 'list');
|
|
475
|
+
if (!classification.cacheable) {
|
|
476
|
+
self.cacheLayer.metrics.bypassed += 1;
|
|
477
|
+
return loadFromDb();
|
|
478
|
+
}
|
|
479
|
+
const fingerprint = self.cacheLayer.queryBuilderFingerprint(
|
|
480
|
+
self.model,
|
|
481
|
+
self.scopeContext,
|
|
482
|
+
self.state,
|
|
483
|
+
'list'
|
|
484
|
+
);
|
|
485
|
+
const tags = self.cacheLayer.buildReadTags(self.model, strat, 'collection', null);
|
|
486
|
+
return self.cacheLayer.wrapRead(
|
|
487
|
+
self.model,
|
|
488
|
+
self.knex,
|
|
489
|
+
self.scopeContext,
|
|
490
|
+
fingerprint,
|
|
491
|
+
tags,
|
|
492
|
+
loadFromDb,
|
|
493
|
+
() => true
|
|
494
|
+
);
|
|
423
495
|
}
|
|
424
|
-
|
|
496
|
+
|
|
497
|
+
return loadFromDb();
|
|
425
498
|
}
|
|
426
499
|
|
|
427
500
|
/**
|
|
@@ -437,8 +510,42 @@ class QueryBuilder {
|
|
|
437
510
|
* @returns {Promise<number>}
|
|
438
511
|
*/
|
|
439
512
|
async count() {
|
|
440
|
-
const
|
|
441
|
-
|
|
513
|
+
const self = this;
|
|
514
|
+
|
|
515
|
+
async function loadFromDb() {
|
|
516
|
+
const result = await self
|
|
517
|
+
.toKnex({ includeLimitOffset: false })
|
|
518
|
+
.count('* as count')
|
|
519
|
+
.first();
|
|
520
|
+
return parseInt(result?.count || 0, 10);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (self.cacheLayer && self.cacheLayer.strategyFor(self.model)) {
|
|
524
|
+
const strat = self.cacheLayer.strategyFor(self.model);
|
|
525
|
+
const classification = self.cacheLayer.classifyQueryBuilder(self.model, self.state, 'count');
|
|
526
|
+
if (!classification.cacheable) {
|
|
527
|
+
self.cacheLayer.metrics.bypassed += 1;
|
|
528
|
+
return loadFromDb();
|
|
529
|
+
}
|
|
530
|
+
const fingerprint = self.cacheLayer.queryBuilderFingerprint(
|
|
531
|
+
self.model,
|
|
532
|
+
self.scopeContext,
|
|
533
|
+
self.state,
|
|
534
|
+
'count'
|
|
535
|
+
);
|
|
536
|
+
const tags = self.cacheLayer.buildReadTags(self.model, strat, 'collection', null);
|
|
537
|
+
return self.cacheLayer.wrapRead(
|
|
538
|
+
self.model,
|
|
539
|
+
self.knex,
|
|
540
|
+
self.scopeContext,
|
|
541
|
+
fingerprint,
|
|
542
|
+
tags,
|
|
543
|
+
loadFromDb,
|
|
544
|
+
() => true
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return loadFromDb();
|
|
442
549
|
}
|
|
443
550
|
|
|
444
551
|
/**
|
|
@@ -458,39 +565,71 @@ class QueryBuilder {
|
|
|
458
565
|
*/
|
|
459
566
|
async paginate(page = 1, perPage = 15) {
|
|
460
567
|
const ctx = createEventContext(this.model.name, 'find', this._hookTrx());
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
568
|
+
const self = this;
|
|
569
|
+
|
|
570
|
+
async function loadFromDb() {
|
|
571
|
+
await ModelEvents.emitAsync(self.model.name, Hooks.BEFORE_FIND, {}, ctx);
|
|
572
|
+
if (ctx.isCancelled) {
|
|
573
|
+
throw new HookCancellationError(ctx.cancelReason, self.model.name, Hooks.BEFORE_FIND);
|
|
574
|
+
}
|
|
465
575
|
|
|
466
|
-
|
|
576
|
+
const base = self.toKnex({ includeLimitOffset: false });
|
|
467
577
|
|
|
468
|
-
|
|
469
|
-
|
|
578
|
+
const countResult = await base.clone().count('* as count').first();
|
|
579
|
+
const total = parseInt(countResult?.count || 0, 10);
|
|
470
580
|
|
|
471
|
-
|
|
472
|
-
|
|
581
|
+
const offset = (page - 1) * perPage;
|
|
582
|
+
const data = await base.clone().limit(perPage).offset(offset);
|
|
473
583
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
584
|
+
for (const record of data) {
|
|
585
|
+
deserializeJsonFields(record, self.jsonColumns);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const withs = self.getWiths();
|
|
589
|
+
if (withs.length > 0 && data.length > 0) {
|
|
590
|
+
await loadRelations(data, ensureArray(withs), self.model, self.knex, self.scopeContext);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
for (const record of data) {
|
|
594
|
+
ModelEvents.emit(self.model.name, Hooks.AFTER_FIND, record, ctx);
|
|
595
|
+
}
|
|
477
596
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
597
|
+
return {
|
|
598
|
+
data,
|
|
599
|
+
total,
|
|
600
|
+
page,
|
|
601
|
+
perPage,
|
|
602
|
+
totalPages: Math.ceil(total / perPage),
|
|
603
|
+
};
|
|
481
604
|
}
|
|
482
605
|
|
|
483
|
-
|
|
484
|
-
|
|
606
|
+
if (self.cacheLayer && self.cacheLayer.strategyFor(self.model)) {
|
|
607
|
+
const strat = self.cacheLayer.strategyFor(self.model);
|
|
608
|
+
const classification = self.cacheLayer.classifyQueryBuilder(self.model, self.state, 'paginate');
|
|
609
|
+
if (!classification.cacheable) {
|
|
610
|
+
self.cacheLayer.metrics.bypassed += 1;
|
|
611
|
+
return loadFromDb();
|
|
612
|
+
}
|
|
613
|
+
const fingerprint = self.cacheLayer.queryBuilderFingerprint(
|
|
614
|
+
self.model,
|
|
615
|
+
self.scopeContext,
|
|
616
|
+
self.state,
|
|
617
|
+
'paginate',
|
|
618
|
+
{ page, perPage }
|
|
619
|
+
);
|
|
620
|
+
const tags = self.cacheLayer.buildReadTags(self.model, strat, 'collection', null);
|
|
621
|
+
return self.cacheLayer.wrapRead(
|
|
622
|
+
self.model,
|
|
623
|
+
self.knex,
|
|
624
|
+
self.scopeContext,
|
|
625
|
+
fingerprint,
|
|
626
|
+
tags,
|
|
627
|
+
loadFromDb,
|
|
628
|
+
() => true
|
|
629
|
+
);
|
|
485
630
|
}
|
|
486
631
|
|
|
487
|
-
return
|
|
488
|
-
data,
|
|
489
|
-
total,
|
|
490
|
-
page,
|
|
491
|
-
perPage,
|
|
492
|
-
totalPages: Math.ceil(total / perPage),
|
|
493
|
-
};
|
|
632
|
+
return loadFromDb();
|
|
494
633
|
}
|
|
495
634
|
|
|
496
635
|
/**
|
|
@@ -498,7 +637,11 @@ class QueryBuilder {
|
|
|
498
637
|
* @returns {Promise<number>} Number of deleted records
|
|
499
638
|
*/
|
|
500
639
|
async delete() {
|
|
501
|
-
|
|
640
|
+
const n = await this.toKnex().delete();
|
|
641
|
+
if (this.cacheLayer && n > 0) {
|
|
642
|
+
this.cacheLayer.invalidateModelAll(this.model);
|
|
643
|
+
}
|
|
644
|
+
return n;
|
|
502
645
|
}
|
|
503
646
|
|
|
504
647
|
/**
|
|
@@ -508,7 +651,11 @@ class QueryBuilder {
|
|
|
508
651
|
*/
|
|
509
652
|
async update(data) {
|
|
510
653
|
const serialized = serializeJsonFields(data, this.jsonColumns);
|
|
511
|
-
|
|
654
|
+
const n = await this.toKnex().update(serialized);
|
|
655
|
+
if (this.cacheLayer && n > 0) {
|
|
656
|
+
this.cacheLayer.invalidateModelAll(this.model);
|
|
657
|
+
}
|
|
658
|
+
return n;
|
|
512
659
|
}
|
|
513
660
|
|
|
514
661
|
/**
|
|
@@ -532,7 +679,7 @@ class QueryBuilder {
|
|
|
532
679
|
* @returns {QueryBuilder}
|
|
533
680
|
*/
|
|
534
681
|
clone() {
|
|
535
|
-
const cloned = new QueryBuilder(this.model, this.knex, { ...this.scopeContext });
|
|
682
|
+
const cloned = new QueryBuilder(this.model, this.knex, { ...this.scopeContext }, this.cacheLayer);
|
|
536
683
|
cloned.jsonColumns = this.jsonColumns;
|
|
537
684
|
cloned.state = {
|
|
538
685
|
wheres: [...this.state.wheres],
|