webspresso 0.0.65 → 0.0.67
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 +120 -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 +52 -1
- package/index.js +6 -1
- package/package.json +9 -4
- 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/src/client-runtime/bootstrap-alpine-swup.js +34 -0
- package/src/client-runtime/bootstrap-swup.js +26 -0
- package/src/client-runtime/mount.js +65 -0
- package/src/client-runtime/resolve.js +40 -0
- package/src/file-router.js +77 -14
- package/src/server.js +11 -2
- package/templates/skills/webspresso-usage/SKILL.md +5 -1
- package/views/partials/webspresso-client-runtime.njk +15 -0
package/core/orm/repository.js
CHANGED
|
@@ -22,9 +22,10 @@ const { getJsonColumns, serializeJsonFields, deserializeJsonFields } = require('
|
|
|
22
22
|
* @param {import('./types').ModelDefinition} model - Model definition
|
|
23
23
|
* @param {import('knex').Knex|import('knex').Knex.Transaction} knex - Knex instance
|
|
24
24
|
* @param {import('./types').ScopeContext} [initialContext] - Initial scope context
|
|
25
|
+
* @param {import('./cache/layer').OrmCacheLayer|null} [cacheLayer] - Optional ORM cache layer
|
|
25
26
|
* @returns {import('./types').Repository}
|
|
26
27
|
*/
|
|
27
|
-
function createRepository(model, knex, initialContext) {
|
|
28
|
+
function createRepository(model, knex, initialContext, cacheLayer = null) {
|
|
28
29
|
const scopeContext = initialContext || createScopeContext();
|
|
29
30
|
const jsonColumns = getJsonColumns(model);
|
|
30
31
|
|
|
@@ -49,40 +50,59 @@ function createRepository(model, knex, initialContext) {
|
|
|
49
50
|
const ctx = createEventContext(model.name, 'find');
|
|
50
51
|
const query = { [model.primaryKey]: id };
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
async function loadFromDb() {
|
|
54
|
+
if (emitEvents) {
|
|
55
|
+
await ModelEvents.emitAsync(model.name, Hooks.BEFORE_FIND, query, ctx);
|
|
56
|
+
if (ctx.isCancelled) {
|
|
57
|
+
throw new HookCancellationError(ctx.cancelReason, model.name, Hooks.BEFORE_FIND);
|
|
58
|
+
}
|
|
57
59
|
}
|
|
58
|
-
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (select && select.length > 0) {
|
|
63
|
-
qb = qb.select(select);
|
|
64
|
-
}
|
|
61
|
+
let qb = baseQuery().where(model.primaryKey, id);
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
if (select && select.length > 0) {
|
|
64
|
+
qb = qb.select(select);
|
|
65
|
+
}
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
67
|
+
const record = await qb.first();
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
if (!record) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
if (withs.length > 0) {
|
|
77
|
-
await loadRelations([record], ensureArray(withs), model, knex, scopeContext);
|
|
78
|
-
}
|
|
73
|
+
deserializeJsonFields(record, jsonColumns);
|
|
79
74
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
75
|
+
if (withs.length > 0) {
|
|
76
|
+
await loadRelations([record], ensureArray(withs), model, knex, scopeContext);
|
|
77
|
+
}
|
|
84
78
|
|
|
85
|
-
|
|
79
|
+
if (emitEvents) {
|
|
80
|
+
ModelEvents.emit(model.name, Hooks.AFTER_FIND, record, ctx);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return record;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (cacheLayer && emitEvents && cacheLayer.strategyFor(model)) {
|
|
87
|
+
const strat = cacheLayer.strategyFor(model);
|
|
88
|
+
const kind = withs.length > 0 ? 'collection' : 'pk';
|
|
89
|
+
const fingerprint = cacheLayer.findByIdFingerprint(
|
|
90
|
+
model,
|
|
91
|
+
scopeContext,
|
|
92
|
+
id,
|
|
93
|
+
select || [],
|
|
94
|
+
withs
|
|
95
|
+
);
|
|
96
|
+
const tags = cacheLayer.buildReadTags(
|
|
97
|
+
model,
|
|
98
|
+
strat,
|
|
99
|
+
kind,
|
|
100
|
+
kind === 'pk' ? id : null
|
|
101
|
+
);
|
|
102
|
+
return cacheLayer.wrapRead(model, knex, scopeContext, fingerprint, tags, loadFromDb, (r) => r != null);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return loadFromDb();
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
/**
|
|
@@ -95,40 +115,65 @@ function createRepository(model, knex, initialContext) {
|
|
|
95
115
|
const { with: withs = [], select } = options;
|
|
96
116
|
const ctx = createEventContext(model.name, 'find');
|
|
97
117
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
const condKeys = Object.keys(conditions);
|
|
119
|
+
const isPkOnly =
|
|
120
|
+
withs.length === 0 &&
|
|
121
|
+
condKeys.length === 1 &&
|
|
122
|
+
condKeys[0] === model.primaryKey;
|
|
103
123
|
|
|
104
|
-
|
|
124
|
+
async function loadFromDb() {
|
|
125
|
+
await ModelEvents.emitAsync(model.name, Hooks.BEFORE_FIND, conditions, ctx);
|
|
126
|
+
if (ctx.isCancelled) {
|
|
127
|
+
throw new HookCancellationError(ctx.cancelReason, model.name, Hooks.BEFORE_FIND);
|
|
128
|
+
}
|
|
105
129
|
|
|
106
|
-
|
|
107
|
-
qb = qb.where(key, value);
|
|
108
|
-
}
|
|
130
|
+
let qb = baseQuery();
|
|
109
131
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
133
|
+
qb = qb.where(key, value);
|
|
134
|
+
}
|
|
113
135
|
|
|
114
|
-
|
|
136
|
+
if (select && select.length > 0) {
|
|
137
|
+
qb = qb.select(select);
|
|
138
|
+
}
|
|
115
139
|
|
|
116
|
-
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
140
|
+
const record = await qb.first();
|
|
119
141
|
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
if (!record) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
122
145
|
|
|
123
|
-
|
|
124
|
-
if (withs.length > 0) {
|
|
125
|
-
await loadRelations([record], ensureArray(withs), model, knex, scopeContext);
|
|
126
|
-
}
|
|
146
|
+
deserializeJsonFields(record, jsonColumns);
|
|
127
147
|
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
if (withs.length > 0) {
|
|
149
|
+
await loadRelations([record], ensureArray(withs), model, knex, scopeContext);
|
|
150
|
+
}
|
|
130
151
|
|
|
131
|
-
|
|
152
|
+
ModelEvents.emit(model.name, Hooks.AFTER_FIND, record, ctx);
|
|
153
|
+
|
|
154
|
+
return record;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (cacheLayer && cacheLayer.strategyFor(model)) {
|
|
158
|
+
const strat = cacheLayer.strategyFor(model);
|
|
159
|
+
const kind = isPkOnly ? 'pk' : 'collection';
|
|
160
|
+
const fingerprint = cacheLayer.findOneFingerprint(
|
|
161
|
+
model,
|
|
162
|
+
scopeContext,
|
|
163
|
+
conditions,
|
|
164
|
+
select || [],
|
|
165
|
+
withs
|
|
166
|
+
);
|
|
167
|
+
const tags = cacheLayer.buildReadTags(
|
|
168
|
+
model,
|
|
169
|
+
strat,
|
|
170
|
+
kind,
|
|
171
|
+
kind === 'pk' ? conditions[model.primaryKey] : null
|
|
172
|
+
);
|
|
173
|
+
return cacheLayer.wrapRead(model, knex, scopeContext, fingerprint, tags, loadFromDb, (r) => r != null);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return loadFromDb();
|
|
132
177
|
}
|
|
133
178
|
|
|
134
179
|
/**
|
|
@@ -140,36 +185,43 @@ function createRepository(model, knex, initialContext) {
|
|
|
140
185
|
const { with: withs = [], select } = options;
|
|
141
186
|
const ctx = createEventContext(model.name, 'find');
|
|
142
187
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
188
|
+
async function loadFromDb() {
|
|
189
|
+
await ModelEvents.emitAsync(model.name, Hooks.BEFORE_FIND, {}, ctx);
|
|
190
|
+
if (ctx.isCancelled) {
|
|
191
|
+
throw new HookCancellationError(ctx.cancelReason, model.name, Hooks.BEFORE_FIND);
|
|
192
|
+
}
|
|
148
193
|
|
|
149
|
-
|
|
194
|
+
let qb = baseQuery();
|
|
150
195
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
196
|
+
if (select && select.length > 0) {
|
|
197
|
+
qb = qb.select(select);
|
|
198
|
+
}
|
|
154
199
|
|
|
155
|
-
|
|
200
|
+
const records = await qb;
|
|
156
201
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
202
|
+
for (const record of records) {
|
|
203
|
+
deserializeJsonFields(record, jsonColumns);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (withs.length > 0 && records.length > 0) {
|
|
207
|
+
await loadRelations(records, ensureArray(withs), model, knex, scopeContext);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const record of records) {
|
|
211
|
+
ModelEvents.emit(model.name, Hooks.AFTER_FIND, record, ctx);
|
|
212
|
+
}
|
|
161
213
|
|
|
162
|
-
|
|
163
|
-
if (withs.length > 0 && records.length > 0) {
|
|
164
|
-
await loadRelations(records, ensureArray(withs), model, knex, scopeContext);
|
|
214
|
+
return records;
|
|
165
215
|
}
|
|
166
216
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
217
|
+
if (cacheLayer && cacheLayer.strategyFor(model)) {
|
|
218
|
+
const strat = cacheLayer.strategyFor(model);
|
|
219
|
+
const fingerprint = cacheLayer.findAllFingerprint(model, scopeContext, select || []);
|
|
220
|
+
const tags = cacheLayer.buildReadTags(model, strat, 'collection', null);
|
|
221
|
+
return cacheLayer.wrapRead(model, knex, scopeContext, fingerprint, tags, loadFromDb, () => true);
|
|
170
222
|
}
|
|
171
223
|
|
|
172
|
-
return
|
|
224
|
+
return loadFromDb();
|
|
173
225
|
}
|
|
174
226
|
|
|
175
227
|
/**
|
|
@@ -334,7 +386,11 @@ function createRepository(model, knex, initialContext) {
|
|
|
334
386
|
qb = qb.where(key, value);
|
|
335
387
|
}
|
|
336
388
|
|
|
337
|
-
|
|
389
|
+
const updated = await qb.update(updateData);
|
|
390
|
+
if (cacheLayer && updated > 0) {
|
|
391
|
+
cacheLayer.invalidateModelAll(model);
|
|
392
|
+
}
|
|
393
|
+
return updated;
|
|
338
394
|
}
|
|
339
395
|
|
|
340
396
|
/**
|
|
@@ -391,6 +447,9 @@ function createRepository(model, knex, initialContext) {
|
|
|
391
447
|
const deleted = await knex(model.table)
|
|
392
448
|
.where(model.primaryKey, id)
|
|
393
449
|
.delete();
|
|
450
|
+
if (cacheLayer && deleted > 0) {
|
|
451
|
+
cacheLayer.invalidateModelAll(model);
|
|
452
|
+
}
|
|
394
453
|
return deleted > 0;
|
|
395
454
|
}
|
|
396
455
|
|
|
@@ -445,7 +504,7 @@ function createRepository(model, knex, initialContext) {
|
|
|
445
504
|
* @returns {import('./query-builder').QueryBuilder}
|
|
446
505
|
*/
|
|
447
506
|
function query() {
|
|
448
|
-
return createQueryBuilder(model, knex, scopeContext);
|
|
507
|
+
return createQueryBuilder(model, knex, scopeContext, cacheLayer);
|
|
449
508
|
}
|
|
450
509
|
|
|
451
510
|
/**
|
package/core/orm/types.js
CHANGED
|
@@ -156,6 +156,7 @@
|
|
|
156
156
|
* @property {AdminMetadata} [admin] - Admin panel metadata
|
|
157
157
|
* @property {RestMetadata} [rest] - REST resource plugin metadata
|
|
158
158
|
* @property {string[]} [hidden=[]] - Column names to never expose in API/templates (e.g. password_hash, api_token)
|
|
159
|
+
* @property {boolean|'auto'|'smart'|{strategy:'auto'|'smart'}} [cache] - Per-model query cache when DB cache is enabled (`true` uses DB defaultStrategy)
|
|
159
160
|
*/
|
|
160
161
|
|
|
161
162
|
/**
|
|
@@ -170,6 +171,7 @@
|
|
|
170
171
|
* @property {AdminMetadata} [admin] - Admin panel metadata
|
|
171
172
|
* @property {RestMetadata} rest - REST resource plugin metadata
|
|
172
173
|
* @property {string[]} hidden - Column names never exposed in API/templates
|
|
174
|
+
* @property {boolean|'auto'|'smart'|{strategy:'auto'|'smart'}|undefined} [cache] - Query cache strategy for this model
|
|
173
175
|
*/
|
|
174
176
|
|
|
175
177
|
// ============================================================================
|
|
@@ -265,6 +267,15 @@
|
|
|
265
267
|
* @property {string|Object} connection - Connection string or config object
|
|
266
268
|
* @property {MigrationConfig} [migrations] - Migration configuration
|
|
267
269
|
* @property {Object} [pool] - Connection pool settings
|
|
270
|
+
* @property {boolean|OrmDatabaseCacheConfig} [cache] - Enable ORM read cache (memory provider by default)
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @typedef {Object} OrmDatabaseCacheConfig
|
|
275
|
+
* @property {boolean} [enabled=true]
|
|
276
|
+
* @property {'auto'|'smart'} [defaultStrategy='auto'] - Default when model.cache is unspecified
|
|
277
|
+
* @property {import('./cache/types').CacheProvider} [provider] - Custom provider (e.g. Redis)
|
|
278
|
+
* @property {Object} [memory] - Options for built-in memory provider (maxEntries, defaultTtlMs)
|
|
268
279
|
*/
|
|
269
280
|
|
|
270
281
|
/**
|
|
@@ -274,6 +285,16 @@
|
|
|
274
285
|
* @property {function(function(TransactionContext): Promise): Promise} transaction - Run in transaction
|
|
275
286
|
* @property {MigrationManager} migrate - Migration manager
|
|
276
287
|
* @property {function(): Promise<void>} destroy - Close all connections
|
|
288
|
+
* @property {OrmCachePublicApi|null} [cache] - Cache controls when enabled
|
|
289
|
+
*/
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @typedef {Object} OrmCachePublicApi
|
|
293
|
+
* @property {() => void} purge
|
|
294
|
+
* @property {(tags: string[]) => void} invalidateTags
|
|
295
|
+
* @property {(modelName: string) => void} invalidateModel
|
|
296
|
+
* @property {() => object} getMetrics
|
|
297
|
+
* @property {() => void} resetMetrics
|
|
277
298
|
*/
|
|
278
299
|
|
|
279
300
|
/**
|
package/index.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ import type { Application, NextFunction, Request, RequestHandler, Response } fro
|
|
|
6
6
|
import type { Knex } from 'knex';
|
|
7
7
|
import type { ZodObject, ZodTypeAny } from 'zod';
|
|
8
8
|
|
|
9
|
+
/** Registered as createApp middlewares[name]: plain handler or (options) => handler (for middleware: ['name', options]). */
|
|
10
|
+
export type WebspressoRegisteredMiddleware =
|
|
11
|
+
| RequestHandler
|
|
12
|
+
| ((options: unknown) => RequestHandler);
|
|
13
|
+
|
|
9
14
|
// --- Express / app ---
|
|
10
15
|
|
|
11
16
|
export interface ErrorPageContext {
|
|
@@ -23,7 +28,7 @@ export interface CreateAppOptions {
|
|
|
23
28
|
publicDir?: string;
|
|
24
29
|
logging?: boolean;
|
|
25
30
|
helmet?: boolean | Record<string, unknown>;
|
|
26
|
-
middlewares?: Record<string,
|
|
31
|
+
middlewares?: Record<string, WebspressoRegisteredMiddleware>;
|
|
27
32
|
plugins?: WebspressoPlugin[];
|
|
28
33
|
assets?: {
|
|
29
34
|
version?: string;
|
|
@@ -40,6 +45,11 @@ export interface CreateAppOptions {
|
|
|
40
45
|
timeout?: string | false;
|
|
41
46
|
auth?: unknown;
|
|
42
47
|
db?: DatabaseInstance | null;
|
|
48
|
+
/** Opt-in Alpine / swup assets under `/__webspresso/client-runtime/*`. Env: WEBSPRESSO_ALPINE, WEBSPRESSO_SWUP. */
|
|
49
|
+
clientRuntime?: {
|
|
50
|
+
alpine?: boolean | Record<string, unknown>;
|
|
51
|
+
swup?: boolean | Record<string, unknown>;
|
|
52
|
+
};
|
|
43
53
|
setupRoutes?: (app: Application, ctx: SetupRoutesContext) => void;
|
|
44
54
|
[key: string]: unknown;
|
|
45
55
|
}
|
|
@@ -49,6 +59,7 @@ export interface SetupRoutesContext {
|
|
|
49
59
|
authMiddleware?: RequestHandler;
|
|
50
60
|
pluginManager: PluginManager;
|
|
51
61
|
options: CreateAppOptions;
|
|
62
|
+
clientRuntime: { alpine: boolean; swup: boolean };
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
export interface CreateAppResult {
|
|
@@ -220,6 +231,7 @@ export interface ModelOptions {
|
|
|
220
231
|
rest?: RestMetadata;
|
|
221
232
|
hooks?: Record<string, (...args: unknown[]) => unknown>;
|
|
222
233
|
hidden?: string[];
|
|
234
|
+
cache?: boolean | 'auto' | 'smart' | { strategy: 'auto' | 'smart' };
|
|
223
235
|
}
|
|
224
236
|
|
|
225
237
|
export interface ModelDefinition {
|
|
@@ -248,6 +260,7 @@ export interface ModelDefinition {
|
|
|
248
260
|
};
|
|
249
261
|
hidden: string[];
|
|
250
262
|
hooks: Record<string, unknown>;
|
|
263
|
+
cache?: boolean | 'auto' | 'smart' | { strategy: 'auto' | 'smart' };
|
|
251
264
|
}
|
|
252
265
|
|
|
253
266
|
export function defineModel(options: ModelOptions): ModelDefinition;
|
|
@@ -343,6 +356,24 @@ export interface MigrationConfig {
|
|
|
343
356
|
tableName?: string;
|
|
344
357
|
}
|
|
345
358
|
|
|
359
|
+
export interface OrmMemoryCacheOptions {
|
|
360
|
+
maxEntries?: number;
|
|
361
|
+
defaultTtlMs?: number;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export interface OrmDatabaseCacheConfig {
|
|
365
|
+
enabled?: boolean;
|
|
366
|
+
defaultStrategy?: 'auto' | 'smart';
|
|
367
|
+
provider?: {
|
|
368
|
+
get(key: string): unknown;
|
|
369
|
+
set(key: string, value: unknown, opts?: { tags?: string[]; ttlMs?: number }): void;
|
|
370
|
+
invalidateTags(tags: string[]): void;
|
|
371
|
+
clear(): void;
|
|
372
|
+
getSizeStats(): { entries: number; tags: number };
|
|
373
|
+
};
|
|
374
|
+
memory?: OrmMemoryCacheOptions;
|
|
375
|
+
}
|
|
376
|
+
|
|
346
377
|
export interface DatabaseConfig {
|
|
347
378
|
client?: string;
|
|
348
379
|
connection?: string | Record<string, unknown>;
|
|
@@ -350,9 +381,18 @@ export interface DatabaseConfig {
|
|
|
350
381
|
migrations?: MigrationConfig;
|
|
351
382
|
pool?: Record<string, unknown>;
|
|
352
383
|
useNullAsDefault?: boolean;
|
|
384
|
+
cache?: boolean | OrmDatabaseCacheConfig;
|
|
353
385
|
[key: string]: unknown;
|
|
354
386
|
}
|
|
355
387
|
|
|
388
|
+
export interface OrmCachePublicApi {
|
|
389
|
+
purge(): void;
|
|
390
|
+
invalidateTags(tags: string[]): void;
|
|
391
|
+
invalidateModel(modelName: string): void;
|
|
392
|
+
getMetrics(): Record<string, unknown>;
|
|
393
|
+
resetMetrics(): void;
|
|
394
|
+
}
|
|
395
|
+
|
|
356
396
|
export interface MigrationStatus {
|
|
357
397
|
name: string;
|
|
358
398
|
completed: boolean;
|
|
@@ -402,11 +442,20 @@ export interface DatabaseInstance {
|
|
|
402
442
|
query(modelName: string, scopeContext?: ScopeContext): QueryBuilder;
|
|
403
443
|
transaction<T>(callback: (ctx: TransactionContext) => Promise<T>): Promise<T>;
|
|
404
444
|
createSeeder(): unknown;
|
|
445
|
+
cache: OrmCachePublicApi | null;
|
|
405
446
|
destroy(): Promise<void>;
|
|
406
447
|
}
|
|
407
448
|
|
|
408
449
|
export function createDatabase(config: DatabaseConfig): DatabaseInstance;
|
|
409
450
|
|
|
451
|
+
export function createMemoryCacheProvider(options?: OrmMemoryCacheOptions): {
|
|
452
|
+
get(key: string): unknown;
|
|
453
|
+
set(key: string, value: unknown, opts?: { tags?: string[]; ttlMs?: number }): void;
|
|
454
|
+
invalidateTags(tags: string[]): void;
|
|
455
|
+
clear(): void;
|
|
456
|
+
getSizeStats(): { entries: number; tags: number };
|
|
457
|
+
};
|
|
458
|
+
|
|
410
459
|
// --- ORM: nanoid / zod ---
|
|
411
460
|
|
|
412
461
|
export function generateNanoid(options?: { maxLength?: number } | number): string;
|
|
@@ -480,3 +529,5 @@ export interface RestResourcePluginOptions {
|
|
|
480
529
|
}
|
|
481
530
|
|
|
482
531
|
export function restResourcePlugin(options?: RestResourcePluginOptions): WebspressoPlugin;
|
|
532
|
+
|
|
533
|
+
export function ormCacheAdminPlugin(options: { db: DatabaseInstance }): WebspressoPlugin;
|
package/index.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const { createApp } = require('./src/server');
|
|
6
|
+
const { resolveClientRuntime } = require('./src/client-runtime/resolve');
|
|
7
|
+
const { CLIENT_RUNTIME_BASE } = require('./src/client-runtime/mount');
|
|
6
8
|
const {
|
|
7
9
|
attachDbMiddleware,
|
|
8
10
|
getAppContext,
|
|
@@ -38,11 +40,13 @@ const {
|
|
|
38
40
|
const orm = require('./core/orm');
|
|
39
41
|
|
|
40
42
|
// Built-in plugins
|
|
41
|
-
const { schemaExplorerPlugin, adminPanelPlugin, siteAnalyticsPlugin, auditLogPlugin, recaptchaPlugin, swaggerPlugin, healthCheckPlugin, restResourcePlugin } = require('./plugins');
|
|
43
|
+
const { schemaExplorerPlugin, adminPanelPlugin, siteAnalyticsPlugin, auditLogPlugin, recaptchaPlugin, swaggerPlugin, healthCheckPlugin, restResourcePlugin, ormCacheAdminPlugin } = require('./plugins');
|
|
42
44
|
|
|
43
45
|
module.exports = {
|
|
44
46
|
// Main API
|
|
45
47
|
createApp,
|
|
48
|
+
resolveClientRuntime,
|
|
49
|
+
CLIENT_RUNTIME_BASE,
|
|
46
50
|
|
|
47
51
|
attachDbMiddleware,
|
|
48
52
|
getAppContext,
|
|
@@ -90,4 +94,5 @@ module.exports = {
|
|
|
90
94
|
swaggerPlugin,
|
|
91
95
|
healthCheckPlugin,
|
|
92
96
|
restResourcePlugin,
|
|
97
|
+
ormCacheAdminPlugin,
|
|
93
98
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.67",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -42,9 +42,13 @@
|
|
|
42
42
|
"utils/",
|
|
43
43
|
"core/",
|
|
44
44
|
"plugins/",
|
|
45
|
-
"templates/"
|
|
45
|
+
"templates/",
|
|
46
|
+
"views/partials/webspresso-client-runtime.njk"
|
|
46
47
|
],
|
|
47
48
|
"dependencies": {
|
|
49
|
+
"@swup/head-plugin": "^2.3.1",
|
|
50
|
+
"@swup/scripts-plugin": "^2.1.0",
|
|
51
|
+
"alpinejs": "^3.15.11",
|
|
48
52
|
"bcrypt": "^5.1.1",
|
|
49
53
|
"commander": "^11.1.0",
|
|
50
54
|
"connect-timeout": "^1.9.1",
|
|
@@ -57,6 +61,7 @@
|
|
|
57
61
|
"knex": "^3.1.0",
|
|
58
62
|
"nunjucks": "^3.2.4",
|
|
59
63
|
"sharp": "^0.33.5",
|
|
64
|
+
"swup": "^4.8.3",
|
|
60
65
|
"zod": "^3.23.0",
|
|
61
66
|
"zod-to-json-schema": "^3.25.2"
|
|
62
67
|
},
|
|
@@ -85,10 +90,10 @@
|
|
|
85
90
|
}
|
|
86
91
|
},
|
|
87
92
|
"devDependencies": {
|
|
88
|
-
"@types/express": "^4.17.21",
|
|
89
|
-
"@types/node": "^20.14.0",
|
|
90
93
|
"@faker-js/faker": "^9.9.0",
|
|
91
94
|
"@playwright/test": "^1.48.0",
|
|
95
|
+
"@types/express": "^4.17.21",
|
|
96
|
+
"@types/node": "^20.14.0",
|
|
92
97
|
"@vitest/coverage-v8": "^3.0.0",
|
|
93
98
|
"better-sqlite3": "^11.10.0",
|
|
94
99
|
"chokidar": "^3.5.3",
|
package/plugins/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const recaptchaPlugin = require('./recaptcha');
|
|
|
15
15
|
const swaggerPlugin = require('./swagger');
|
|
16
16
|
const healthCheckPlugin = require('./health-check');
|
|
17
17
|
const restResourcePlugin = require('./rest-resources');
|
|
18
|
+
const ormCacheAdminPlugin = require('./orm-cache-admin');
|
|
18
19
|
|
|
19
20
|
module.exports = {
|
|
20
21
|
sitemapPlugin,
|
|
@@ -29,5 +30,6 @@ module.exports = {
|
|
|
29
30
|
swaggerPlugin,
|
|
30
31
|
healthCheckPlugin,
|
|
31
32
|
restResourcePlugin,
|
|
33
|
+
ormCacheAdminPlugin,
|
|
32
34
|
};
|
|
33
35
|
|