webspresso 0.0.101 → 0.0.103
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/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +11 -0
- package/.agents/CONVENTIONS.md +4 -0
- package/.agents/ORM.md +13 -4
- package/.agents/PLUGINS.md +32 -6
- package/.agents/ROUTING.md +99 -2
- package/.agents/skills/webspresso-framework/SKILL.md +32 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +4 -5
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
- package/AGENTS.md +6 -0
- package/README.md +20 -7
- package/bin/commands/add-tailwind.js +10 -0
- package/bin/commands/build.js +34 -0
- package/bin/commands/routes.js +8 -0
- package/core/applySchema.js +18 -1
- package/core/auth/jwt.js +10 -17
- package/core/auth/user-agent.js +61 -42
- package/core/compression/compression-stream.js +2 -2
- package/core/compression/middleware.js +1 -1
- package/core/compression/negotiate.js +11 -4
- package/core/events/dispatcher.js +56 -33
- package/core/openapi/orm-components.js +2 -3
- package/core/orm/cache/listeners.js +12 -2
- package/core/orm/json-fields.js +7 -1
- package/core/orm/model.js +99 -8
- package/core/orm/query-builder.js +71 -25
- package/core/orm/repository.js +42 -17
- package/core/orm/schema-helpers.js +49 -0
- package/core/orm/scopes.js +40 -9
- package/core/orm/types.js +12 -1
- package/core/queue/manager.js +5 -0
- package/core/validation/index.js +9 -3
- package/index.d.ts +178 -6
- package/index.js +71 -16
- package/package.json +6 -5
- package/plugins/admin-panel/index.js +12 -0
- package/plugins/audit-log/middleware.js +39 -4
- package/plugins/csrf/index.js +1 -1
- package/plugins/email/service.js +32 -1
- package/plugins/file-manager/api-handlers.js +10 -17
- package/plugins/file-manager/index.js +24 -9
- package/plugins/file-manager/security.js +18 -6
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +258 -0
- package/plugins/site-analytics/bot-patterns.js +3 -0
- package/plugins/site-analytics/client-error-handler.js +3 -1
- package/plugins/site-analytics/index.js +10 -1
- package/plugins/site-analytics/tracking.js +245 -46
- package/src/api/api-loader.js +18 -2
- package/src/api/validation-i18n.js +231 -0
- package/src/discovery/route-manifest.js +197 -0
- package/src/discovery/scan-routes.js +84 -8
- package/src/file-router.js +215 -97
- package/src/helpers.js +139 -26
- package/src/njk-frontmatter.js +17 -1
- package/src/plugin-manager.js +46 -13
- package/src/routing/route-table.js +15 -5
- package/src/server.js +86 -15
- package/src/services/builtins/system.js +4 -6
- package/src/services/registry.js +12 -2
- package/src/services/validator.js +57 -32
- package/src/template/precompile.js +247 -0
package/index.d.ts
CHANGED
|
@@ -68,6 +68,14 @@ export interface CreateAppOptions {
|
|
|
68
68
|
compression?: boolean | CompressionOptions;
|
|
69
69
|
/** Shutdown and lifecycle configuration */
|
|
70
70
|
shutdown?: ShutdownOptions;
|
|
71
|
+
/**
|
|
72
|
+
* Ahead-Of-Time (AOT) route manifest: boolean to enable/disable loading, or explicit path to manifest JSON file
|
|
73
|
+
*/
|
|
74
|
+
routesManifest?: boolean | string;
|
|
75
|
+
/**
|
|
76
|
+
* Precompiled Nunjucks templates: boolean to enable/disable loading, or explicit path to compiled bundle file
|
|
77
|
+
*/
|
|
78
|
+
precompiledTemplates?: boolean | string;
|
|
71
79
|
/** Development tooling and DX options */
|
|
72
80
|
dev?: {
|
|
73
81
|
banner?: boolean;
|
|
@@ -657,6 +665,25 @@ export function detectLocale(
|
|
|
657
665
|
defaultLocale: string
|
|
658
666
|
): string;
|
|
659
667
|
|
|
668
|
+
export interface TranslateValidationIssuesOptions {
|
|
669
|
+
req?: Request;
|
|
670
|
+
t?: Translator;
|
|
671
|
+
locale?: string;
|
|
672
|
+
pagesDir?: string;
|
|
673
|
+
routeDir?: string;
|
|
674
|
+
isDev?: boolean;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export function translateValidationIssues<T = unknown>(
|
|
678
|
+
issues: T[],
|
|
679
|
+
options?: TranslateValidationIssuesOptions
|
|
680
|
+
): T[];
|
|
681
|
+
|
|
682
|
+
export function getTranslatorForRequest(
|
|
683
|
+
req?: Request,
|
|
684
|
+
options?: { pagesDir?: string; routeDir?: string; isDev?: boolean }
|
|
685
|
+
): { t: Translator; locale: string };
|
|
686
|
+
|
|
660
687
|
export function parseNjkFrontmatter(content: string): {
|
|
661
688
|
body: string;
|
|
662
689
|
fm: Record<string, unknown> | null;
|
|
@@ -1045,11 +1072,20 @@ export type InferModel<T> = T extends ModelDefinition<infer TModel>
|
|
|
1045
1072
|
export type InferModelCreate<T> = Partial<InferModel<T>> & Record<string, any>;
|
|
1046
1073
|
export type InferModelUpdate<T> = Partial<InferModel<T>>;
|
|
1047
1074
|
|
|
1048
|
-
export interface ModelOptions<TSchema
|
|
1075
|
+
export interface ModelOptions<TSchema = ZodObject<Record<string, ZodTypeAny>>> {
|
|
1049
1076
|
name: string;
|
|
1050
1077
|
table: string;
|
|
1051
|
-
schema: TSchema
|
|
1078
|
+
schema: TSchema | Record<string, any>;
|
|
1052
1079
|
primaryKey?: string;
|
|
1080
|
+
timestamps?: boolean | { createdAt?: string; updatedAt?: string; snakeCase?: boolean };
|
|
1081
|
+
softDelete?: boolean | { deletedAt?: string; snakeCase?: boolean };
|
|
1082
|
+
softDeletes?: boolean | { deletedAt?: string; snakeCase?: boolean };
|
|
1083
|
+
options?: {
|
|
1084
|
+
timestamps?: boolean | { createdAt?: string; updatedAt?: string; snakeCase?: boolean };
|
|
1085
|
+
softDelete?: boolean | { deletedAt?: string; snakeCase?: boolean };
|
|
1086
|
+
softDeletes?: boolean | { deletedAt?: string; snakeCase?: boolean };
|
|
1087
|
+
[key: string]: unknown;
|
|
1088
|
+
};
|
|
1053
1089
|
relations?: Record<string, RelationDefinition>;
|
|
1054
1090
|
scopes?: ScopeOptions;
|
|
1055
1091
|
admin?: AdminMetadata;
|
|
@@ -1077,6 +1113,11 @@ export interface ModelDefinition<TModel = Record<string, any>> {
|
|
|
1077
1113
|
timestamps: boolean;
|
|
1078
1114
|
tenant: string | null;
|
|
1079
1115
|
};
|
|
1116
|
+
options?: {
|
|
1117
|
+
timestamps?: boolean | { createdAt?: string; updatedAt?: string; snakeCase?: boolean };
|
|
1118
|
+
softDelete?: boolean | { deletedAt?: string; snakeCase?: boolean };
|
|
1119
|
+
[key: string]: unknown;
|
|
1120
|
+
};
|
|
1080
1121
|
columns: Map<string, unknown>;
|
|
1081
1122
|
admin: {
|
|
1082
1123
|
enabled: boolean;
|
|
@@ -1102,9 +1143,9 @@ export interface ModelDefinition<TModel = Record<string, any>> {
|
|
|
1102
1143
|
cache?: boolean | 'auto' | 'smart' | { strategy: 'auto' | 'smart' };
|
|
1103
1144
|
}
|
|
1104
1145
|
|
|
1105
|
-
export function defineModel<TSchema
|
|
1146
|
+
export function defineModel<TSchema = ZodObject<any>>(
|
|
1106
1147
|
options: ModelOptions<TSchema>
|
|
1107
|
-
): ModelDefinition<
|
|
1148
|
+
): ModelDefinition<any>;
|
|
1108
1149
|
|
|
1109
1150
|
export function getModel<T = Record<string, any>>(name: string): ModelDefinition<T> | undefined;
|
|
1110
1151
|
|
|
@@ -1120,7 +1161,10 @@ export function isCascadeEnabled(relation: RelationDefinition, operation: 'delet
|
|
|
1120
1161
|
|
|
1121
1162
|
/** zdb column helpers (id, string, timestamp, …) + `schema()` */
|
|
1122
1163
|
export type Zdb = Record<string, any> & {
|
|
1123
|
-
schema(shape: Record<string,
|
|
1164
|
+
schema(shape: Record<string, any>): ZodObject<Record<string, ZodTypeAny>>;
|
|
1165
|
+
timestamps(options?: { snakeCase?: boolean; createdAt?: string; updatedAt?: string }): Record<string, any>;
|
|
1166
|
+
softDelete(options?: Record<string, any>): any;
|
|
1167
|
+
softDeletes(options?: { snakeCase?: boolean; deletedAt?: string }): Record<string, any>;
|
|
1124
1168
|
};
|
|
1125
1169
|
|
|
1126
1170
|
export const zdb: Zdb;
|
|
@@ -1561,9 +1605,38 @@ export interface AdminPanelPluginOptions {
|
|
|
1561
1605
|
windowMs?: number;
|
|
1562
1606
|
blockDurationMs?: number;
|
|
1563
1607
|
};
|
|
1608
|
+
/** Lazy / On-Demand mount: defers plugin initialization until first request to adminPath or service execution */
|
|
1609
|
+
lazy?: boolean | LazyPluginOptions;
|
|
1564
1610
|
[key: string]: unknown;
|
|
1565
1611
|
}
|
|
1566
1612
|
|
|
1613
|
+
export interface LazyPluginOptions {
|
|
1614
|
+
name?: string;
|
|
1615
|
+
prefix?: string | string[];
|
|
1616
|
+
services?: string[];
|
|
1617
|
+
helpers?: string[];
|
|
1618
|
+
events?: string[];
|
|
1619
|
+
dependencies?: Record<string, string> | string[];
|
|
1620
|
+
csp?: Record<string, string[]>;
|
|
1621
|
+
onHydrate?: (realPlugin: WebspressoPlugin) => void;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
export interface LazyPlugin extends WebspressoPlugin {
|
|
1625
|
+
isLazy: true;
|
|
1626
|
+
readonly isHydrated: boolean;
|
|
1627
|
+
hydrate(): WebspressoPlugin;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
export function lazyPlugin(
|
|
1631
|
+
pluginFactory: (() => WebspressoPlugin) | WebspressoPlugin,
|
|
1632
|
+
lazyOptions?: LazyPluginOptions
|
|
1633
|
+
): LazyPlugin;
|
|
1634
|
+
|
|
1635
|
+
export function defineLazyPlugin(
|
|
1636
|
+
pluginFactory: (() => WebspressoPlugin) | WebspressoPlugin,
|
|
1637
|
+
lazyOptions?: LazyPluginOptions
|
|
1638
|
+
): LazyPlugin;
|
|
1639
|
+
|
|
1567
1640
|
export function adminPanelPlugin(options?: AdminPanelPluginOptions): WebspressoPlugin;
|
|
1568
1641
|
|
|
1569
1642
|
export function dataExchangePlugin(options?: Record<string, unknown>): WebspressoPlugin;
|
|
@@ -3142,11 +3215,14 @@ export interface ApiContext<TBody = any, TParams = Record<string, string>, TQuer
|
|
|
3142
3215
|
query: TQuery;
|
|
3143
3216
|
body: TBody;
|
|
3144
3217
|
input: { body?: TBody; params?: TParams; query?: TQuery; headers?: Record<string, string> };
|
|
3218
|
+
locale: string;
|
|
3219
|
+
t: Translator;
|
|
3145
3220
|
service: WebspressoServiceCaller;
|
|
3146
3221
|
module: string | null;
|
|
3147
3222
|
config: Record<string, unknown>;
|
|
3148
3223
|
logger: Console;
|
|
3149
3224
|
db: unknown;
|
|
3225
|
+
sse?: (sourceOrOptions?: any, maybeOptions?: any) => any;
|
|
3150
3226
|
}
|
|
3151
3227
|
|
|
3152
3228
|
export interface DefineApiOptions<TResult = any, TBody = any, TParams = Record<string, string>, TQuery = Record<string, string>> {
|
|
@@ -3213,7 +3289,21 @@ export function scanRoutes(options?: {
|
|
|
3213
3289
|
pages?: boolean | { dir?: string; prefix?: string };
|
|
3214
3290
|
api?: boolean | { dir?: string; prefix?: string };
|
|
3215
3291
|
modules?: boolean | { dir?: string };
|
|
3216
|
-
}): RouteDescriptor[];
|
|
3292
|
+
}): RouteDescriptor[] & { scannedPagesFiles?: string[]; scannedPagesDir?: string | null };
|
|
3293
|
+
|
|
3294
|
+
export function discoverAppRoutes(options?: {
|
|
3295
|
+
rootDir?: string;
|
|
3296
|
+
pages?: boolean | { dir?: string; prefix?: string };
|
|
3297
|
+
api?: boolean | { dir?: string; prefix?: string };
|
|
3298
|
+
modules?: boolean | { dir?: string };
|
|
3299
|
+
routes?: Array<{ method?: string; path: string }>;
|
|
3300
|
+
isDev?: boolean;
|
|
3301
|
+
}): {
|
|
3302
|
+
descriptors: RouteDescriptor[];
|
|
3303
|
+
routeTable: RouteTable;
|
|
3304
|
+
scannedPagesFiles: string[];
|
|
3305
|
+
scannedPagesDir: string | null;
|
|
3306
|
+
};
|
|
3217
3307
|
|
|
3218
3308
|
export function parseFileRoute(
|
|
3219
3309
|
relativePath: string,
|
|
@@ -3225,6 +3315,88 @@ export function compileRouteTable(
|
|
|
3225
3315
|
options?: { explicitRoutes?: Array<{ method?: string; path: string }>; isDev?: boolean }
|
|
3226
3316
|
): RouteTable;
|
|
3227
3317
|
|
|
3318
|
+
export function generateRouteManifest(options?: {
|
|
3319
|
+
rootDir?: string;
|
|
3320
|
+
pages?: boolean | string | { dir?: string; prefix?: string };
|
|
3321
|
+
api?: boolean | string | { dir?: string; prefix?: string };
|
|
3322
|
+
modules?: boolean | { dir?: string };
|
|
3323
|
+
pagesDir?: string;
|
|
3324
|
+
outDir?: string;
|
|
3325
|
+
outFile?: string;
|
|
3326
|
+
silent?: boolean;
|
|
3327
|
+
}): {
|
|
3328
|
+
manifest: {
|
|
3329
|
+
version: number;
|
|
3330
|
+
generatedAt: string;
|
|
3331
|
+
pagesDirRel: string;
|
|
3332
|
+
scannedPagesFiles: string[];
|
|
3333
|
+
routes: Array<{
|
|
3334
|
+
type: 'page' | 'api';
|
|
3335
|
+
method: string;
|
|
3336
|
+
path: string;
|
|
3337
|
+
source: string;
|
|
3338
|
+
module?: string | null;
|
|
3339
|
+
moduleConfig?: { name?: string } | null;
|
|
3340
|
+
}>;
|
|
3341
|
+
};
|
|
3342
|
+
manifestPath: string;
|
|
3343
|
+
routeCount: number;
|
|
3344
|
+
scannedFilesCount: number;
|
|
3345
|
+
};
|
|
3346
|
+
|
|
3347
|
+
export function loadRouteManifest(options?: {
|
|
3348
|
+
rootDir?: string;
|
|
3349
|
+
manifestPath?: string;
|
|
3350
|
+
}): {
|
|
3351
|
+
manifest: {
|
|
3352
|
+
version: number;
|
|
3353
|
+
generatedAt: string;
|
|
3354
|
+
pagesDirRel: string;
|
|
3355
|
+
scannedPagesFiles: string[];
|
|
3356
|
+
routes: Array<{
|
|
3357
|
+
type: 'page' | 'api';
|
|
3358
|
+
method: string;
|
|
3359
|
+
path: string;
|
|
3360
|
+
source: string;
|
|
3361
|
+
module?: string | null;
|
|
3362
|
+
moduleConfig?: { name?: string } | null;
|
|
3363
|
+
}>;
|
|
3364
|
+
};
|
|
3365
|
+
descriptors: RouteDescriptor[] & { scannedPagesFiles?: string[]; scannedPagesDir?: string | null };
|
|
3366
|
+
} | null;
|
|
3367
|
+
|
|
3368
|
+
export function precompileTemplates(options?: {
|
|
3369
|
+
rootDir?: string;
|
|
3370
|
+
viewsDir?: string;
|
|
3371
|
+
pagesDir?: string;
|
|
3372
|
+
outDir?: string;
|
|
3373
|
+
outFile?: string;
|
|
3374
|
+
env?: any;
|
|
3375
|
+
}): {
|
|
3376
|
+
templateCount: number;
|
|
3377
|
+
manifestPath: string;
|
|
3378
|
+
bundle: {
|
|
3379
|
+
version: number;
|
|
3380
|
+
generatedAt: string;
|
|
3381
|
+
templates: Record<string, string>;
|
|
3382
|
+
frontmatter: Record<string, any>;
|
|
3383
|
+
};
|
|
3384
|
+
};
|
|
3385
|
+
|
|
3386
|
+
export function loadPrecompiledTemplates(options?: {
|
|
3387
|
+
rootDir?: string;
|
|
3388
|
+
bundlePath?: string;
|
|
3389
|
+
}): {
|
|
3390
|
+
loader: any;
|
|
3391
|
+
templateCount: number;
|
|
3392
|
+
bundle: {
|
|
3393
|
+
version: number;
|
|
3394
|
+
generatedAt: string;
|
|
3395
|
+
templates: Record<string, string>;
|
|
3396
|
+
frontmatter: Record<string, any>;
|
|
3397
|
+
};
|
|
3398
|
+
} | null;
|
|
3399
|
+
|
|
3228
3400
|
export interface GenerateTypesOptions {
|
|
3229
3401
|
/** Root directory of the project (default: process.cwd()) */
|
|
3230
3402
|
rootDir?: string;
|
package/index.js
CHANGED
|
@@ -58,13 +58,21 @@ const {
|
|
|
58
58
|
const { definePage } = require('./src/pages/define-page');
|
|
59
59
|
const { defineApi } = require('./src/api/define-api');
|
|
60
60
|
const { defineModule } = require('./src/modules/define-module');
|
|
61
|
-
const { scanRoutes } = require('./src/discovery/scan-routes');
|
|
61
|
+
const { scanRoutes, discoverAppRoutes } = require('./src/discovery/scan-routes');
|
|
62
|
+
const { generateRouteManifest, loadRouteManifest } = require('./src/discovery/route-manifest');
|
|
63
|
+
const { precompileTemplates, loadPrecompiledTemplates } = require('./src/template/precompile');
|
|
62
64
|
const { parseFileRoute } = require('./src/discovery/file-route-parser');
|
|
63
65
|
const { compileRouteTable, RouteTable } = require('./src/routing/route-table');
|
|
64
66
|
const { mountDiscoveredRoutes } = require('./src/routing/mount-discovered-routes');
|
|
65
67
|
|
|
66
|
-
// ORM
|
|
67
|
-
|
|
68
|
+
// Lazy-loaded ORM accessor
|
|
69
|
+
let _orm = null;
|
|
70
|
+
function getOrm() {
|
|
71
|
+
if (!_orm) {
|
|
72
|
+
_orm = require('./core/orm');
|
|
73
|
+
}
|
|
74
|
+
return _orm;
|
|
75
|
+
}
|
|
68
76
|
|
|
69
77
|
// Lazy-loaded built-in plugins
|
|
70
78
|
const plugins = require('./plugins');
|
|
@@ -99,12 +107,6 @@ module.exports = {
|
|
|
99
107
|
discoverServices: require('./src/services').discoverServices,
|
|
100
108
|
filePathToServiceName: require('./src/services').filePathToServiceName,
|
|
101
109
|
toCamelCase: require('./src/services').toCamelCase,
|
|
102
|
-
createAuthServices: require('./src/services').createAuthServices,
|
|
103
|
-
createMailServices: require('./src/services').createMailServices,
|
|
104
|
-
createMediaServices: require('./src/services').createMediaServices,
|
|
105
|
-
createFileManagerServices: require('./src/services').createFileManagerServices,
|
|
106
|
-
createSystemServices: require('./src/services').createSystemServices,
|
|
107
|
-
createExchangeServices: require('./src/services').createExchangeServices,
|
|
108
110
|
validateServiceInput: require('./src/services').validateServiceInput,
|
|
109
111
|
executeService: require('./src/services').executeService,
|
|
110
112
|
|
|
@@ -171,12 +173,6 @@ module.exports = {
|
|
|
171
173
|
createPluginManager,
|
|
172
174
|
getPluginManager,
|
|
173
175
|
resetPluginManager,
|
|
174
|
-
|
|
175
|
-
// ORM
|
|
176
|
-
...orm,
|
|
177
|
-
|
|
178
|
-
// Direct zdb export (for convenience)
|
|
179
|
-
zdb: orm.zdb,
|
|
180
176
|
|
|
181
177
|
// Discovery, Pages, API, Modules & Routing primitives
|
|
182
178
|
definePage,
|
|
@@ -186,11 +182,18 @@ module.exports = {
|
|
|
186
182
|
defineModule,
|
|
187
183
|
module: defineModule,
|
|
188
184
|
scanRoutes,
|
|
185
|
+
discoverAppRoutes,
|
|
186
|
+
generateRouteManifest,
|
|
187
|
+
loadRouteManifest,
|
|
188
|
+
precompileTemplates,
|
|
189
|
+
loadPrecompiledTemplates,
|
|
189
190
|
parseFileRoute,
|
|
190
191
|
compileRouteTable,
|
|
191
192
|
RouteTable,
|
|
192
193
|
mountDiscoveredRoutes,
|
|
193
194
|
resolveMiddlewares: require('./src/routing/middleware-resolver').resolveMiddlewares,
|
|
195
|
+
translateValidationIssues: require('./src/api/validation-i18n').translateValidationIssues,
|
|
196
|
+
getTranslatorForRequest: require('./src/api/validation-i18n').getTranslatorForRequest,
|
|
194
197
|
|
|
195
198
|
// Forms & Flash subsystem (AdonisJS & Django-inspired)
|
|
196
199
|
createFlashMiddleware: require('./core/forms').createFlashMiddleware,
|
|
@@ -218,10 +221,62 @@ module.exports = {
|
|
|
218
221
|
loadListeners: require('./core/events').loadListeners,
|
|
219
222
|
};
|
|
220
223
|
|
|
221
|
-
// Define lazy getters for plugins and heavy core subsystems
|
|
224
|
+
// Define lazy getters for plugins, ORM, and heavy core subsystems
|
|
222
225
|
Object.defineProperties(module.exports, {
|
|
223
226
|
...Object.getOwnPropertyDescriptors(plugins),
|
|
224
227
|
|
|
228
|
+
// Built-in Services Creators (lazy loaded)
|
|
229
|
+
createAuthServices: { get: () => require('./src/services').createAuthServices, enumerable: true, configurable: true },
|
|
230
|
+
createMailServices: { get: () => require('./src/services').createMailServices, enumerable: true, configurable: true },
|
|
231
|
+
createMediaServices: { get: () => require('./src/services').createMediaServices, enumerable: true, configurable: true },
|
|
232
|
+
createFileManagerServices: { get: () => require('./src/services').createFileManagerServices, enumerable: true, configurable: true },
|
|
233
|
+
createSystemServices: { get: () => require('./src/services').createSystemServices, enumerable: true, configurable: true },
|
|
234
|
+
createExchangeServices: { get: () => require('./src/services').createExchangeServices, enumerable: true, configurable: true },
|
|
235
|
+
|
|
236
|
+
// ORM & Database Layer (lazy loaded)
|
|
237
|
+
orm: { get: () => getOrm(), enumerable: true, configurable: true },
|
|
238
|
+
createDatabase: { get: () => getOrm().createDatabase, enumerable: true, configurable: true },
|
|
239
|
+
createInMemoryDatabase: { get: () => getOrm().createInMemoryDatabase, enumerable: true, configurable: true },
|
|
240
|
+
defineModel: { get: () => getOrm().defineModel, enumerable: true, configurable: true },
|
|
241
|
+
zdb: { get: () => getOrm().zdb, enumerable: true, configurable: true },
|
|
242
|
+
BaseRepository: { get: () => getOrm().BaseRepository, enumerable: true, configurable: true },
|
|
243
|
+
Repository: { get: () => getOrm().Repository, enumerable: true, configurable: true },
|
|
244
|
+
createRepository: { get: () => getOrm().createRepository, enumerable: true, configurable: true },
|
|
245
|
+
getModel: { get: () => getOrm().getModel, enumerable: true, configurable: true },
|
|
246
|
+
getAllModels: { get: () => getOrm().getAllModels, enumerable: true, configurable: true },
|
|
247
|
+
hasModel: { get: () => getOrm().hasModel, enumerable: true, configurable: true },
|
|
248
|
+
clearRegistry: { get: () => getOrm().clearRegistry, enumerable: true, configurable: true },
|
|
249
|
+
isCascadeEnabled: { get: () => getOrm().isCascadeEnabled, enumerable: true, configurable: true },
|
|
250
|
+
extractColumnsFromSchema: { get: () => getOrm().extractColumnsFromSchema, enumerable: true, configurable: true },
|
|
251
|
+
getColumnMeta: { get: () => getOrm().getColumnMeta, enumerable: true, configurable: true },
|
|
252
|
+
generateNanoid: { get: () => getOrm().generateNanoid, enumerable: true, configurable: true },
|
|
253
|
+
zodNanoid: { get: () => getOrm().zodNanoid, enumerable: true, configurable: true },
|
|
254
|
+
extendZ: { get: () => getOrm().extendZ, enumerable: true, configurable: true },
|
|
255
|
+
omitHiddenColumns: { get: () => getOrm().omitHiddenColumns, enumerable: true, configurable: true },
|
|
256
|
+
sanitizeForOutput: { get: () => getOrm().sanitizeForOutput, enumerable: true, configurable: true },
|
|
257
|
+
ModelEvents: { get: () => getOrm().ModelEvents, enumerable: true, configurable: true },
|
|
258
|
+
Hooks: { get: () => getOrm().Hooks, enumerable: true, configurable: true },
|
|
259
|
+
HookCancellationError: { get: () => getOrm().HookCancellationError, enumerable: true, configurable: true },
|
|
260
|
+
createEventContext: { get: () => getOrm().createEventContext, enumerable: true, configurable: true },
|
|
261
|
+
getTransactionContext: { get: () => getOrm().getTransactionContext, enumerable: true, configurable: true },
|
|
262
|
+
createTransactionContext: { get: () => getOrm().createTransactionContext, enumerable: true, configurable: true },
|
|
263
|
+
runTransaction: { get: () => getOrm().runTransaction, enumerable: true, configurable: true },
|
|
264
|
+
createSchemaHelpers: { get: () => getOrm().createSchemaHelpers, enumerable: true, configurable: true },
|
|
265
|
+
loadRelations: { get: () => getOrm().loadRelations, enumerable: true, configurable: true },
|
|
266
|
+
loadRelation: { get: () => getOrm().loadRelation, enumerable: true, configurable: true },
|
|
267
|
+
parseRelationTree: { get: () => getOrm().parseRelationTree, enumerable: true, configurable: true },
|
|
268
|
+
QueryComplexityError: { get: () => getOrm().QueryComplexityError, enumerable: true, configurable: true },
|
|
269
|
+
validateQueryComplexity: { get: () => getOrm().validateQueryComplexity, enumerable: true, configurable: true },
|
|
270
|
+
resolveQueryLimits: { get: () => getOrm().resolveQueryLimits, enumerable: true, configurable: true },
|
|
271
|
+
DEFAULT_QUERY_LIMITS: { get: () => getOrm().DEFAULT_QUERY_LIMITS, enumerable: true, configurable: true },
|
|
272
|
+
buildSearchClause: { get: () => getOrm().buildSearchClause, enumerable: true, configurable: true },
|
|
273
|
+
tokenizeSearchTerm: { get: () => getOrm().tokenizeSearchTerm, enumerable: true, configurable: true },
|
|
274
|
+
detectDialect: { get: () => getOrm().detectDialect, enumerable: true, configurable: true },
|
|
275
|
+
defineFactory: { get: () => getOrm().defineFactory, enumerable: true, configurable: true },
|
|
276
|
+
FactoryBuilder: { get: () => getOrm().FactoryBuilder, enumerable: true, configurable: true },
|
|
277
|
+
diffSchema: { get: () => getOrm().diffSchema, enumerable: true, configurable: true },
|
|
278
|
+
makeMigrations: { get: () => getOrm().makeMigrations, enumerable: true, configurable: true },
|
|
279
|
+
|
|
225
280
|
// Cascading Middleware & Layouts
|
|
226
281
|
cascading: { get: () => require('./src/routing/cascading'), enumerable: true, configurable: true },
|
|
227
282
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.103",
|
|
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",
|
|
@@ -47,21 +47,21 @@
|
|
|
47
47
|
"test:services": "vitest run tests/**/service*",
|
|
48
48
|
"test:plugins": "vitest run tests/**/plugin* tests/unit/*plugin*.test.js",
|
|
49
49
|
"test:core": "vitest run tests/unit/core* tests/unit/server* tests/unit/routing* tests/unit/template*",
|
|
50
|
-
"test:branches": "vitest run tests/unit/*branches
|
|
51
|
-
"test:fast": "vitest run tests/unit/*branches
|
|
50
|
+
"test:branches": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js",
|
|
51
|
+
"test:fast": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js",
|
|
52
52
|
"test:watch": "vitest",
|
|
53
53
|
"test:coverage": "vitest run --coverage",
|
|
54
54
|
"test:coverage:quick": "vitest run --coverage",
|
|
55
55
|
"test:coverage:changed": "vitest run --coverage --changed",
|
|
56
56
|
"test:coverage:full": "FULL_REPORT=true vitest run --coverage",
|
|
57
|
-
"test:coverage:branches": "vitest run tests/unit/*branches
|
|
57
|
+
"test:coverage:branches": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js --coverage",
|
|
58
58
|
"test:coverage:unit": "vitest run tests/unit/ --coverage",
|
|
59
59
|
"test:coverage:core": "vitest run tests/unit/core* tests/unit/server* tests/unit/routing* tests/unit/template* --coverage",
|
|
60
60
|
"test:coverage:auth": "vitest run tests/**/auth* tests/unit/server-auth-branches.test.js --coverage",
|
|
61
61
|
"test:coverage:orm": "vitest run tests/**/orm* tests/**/database* --coverage",
|
|
62
62
|
"test:coverage:services": "vitest run tests/**/service* --coverage",
|
|
63
63
|
"test:coverage:plugins": "vitest run tests/**/plugin* tests/unit/*plugin*.test.js --coverage",
|
|
64
|
-
"test:coverage:fast": "vitest run tests/unit/*branches
|
|
64
|
+
"test:coverage:fast": "vitest run tests/unit/*scenarios*.test.js tests/unit/*branches*.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js --coverage",
|
|
65
65
|
"test:e2e": "playwright test",
|
|
66
66
|
"test:e2e:ui": "playwright test --ui",
|
|
67
67
|
"test:e2e:debug": "playwright test --debug",
|
|
@@ -176,6 +176,7 @@
|
|
|
176
176
|
"dotenv": "^16.3.1",
|
|
177
177
|
"express-rate-limit": "^8.4.1",
|
|
178
178
|
"release-it": "^20.2.1",
|
|
179
|
+
"softscope": "^0.3.2",
|
|
179
180
|
"supertest": "^6.3.4",
|
|
180
181
|
"typescript": "~5.6.3",
|
|
181
182
|
"vitest": "^4.1.11",
|
|
@@ -55,8 +55,20 @@ function adminPanelPlugin(options = {}) {
|
|
|
55
55
|
richTextSanitize = true,
|
|
56
56
|
dataExchange: dataExchangeOption = true,
|
|
57
57
|
loginRateLimit,
|
|
58
|
+
lazy: lazyOption,
|
|
58
59
|
} = options;
|
|
59
60
|
|
|
61
|
+
if (lazyOption) {
|
|
62
|
+
const { lazyPlugin } = require('../lazy');
|
|
63
|
+
const lazyOpts = typeof lazyOption === 'object' ? lazyOption : {};
|
|
64
|
+
return lazyPlugin(() => adminPanelPlugin({ ...options, lazy: false }), {
|
|
65
|
+
name: 'admin-panel',
|
|
66
|
+
prefix: adminPath,
|
|
67
|
+
services: ['exchange.export', 'exchange.import'],
|
|
68
|
+
...lazyOpts,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
const adminUsersConfig = typeof adminUsersOption === 'object' ? adminUsersOption : { enabled: adminUsersOption !== false };
|
|
61
73
|
|
|
62
74
|
// Validate required options
|
|
@@ -95,11 +95,9 @@ function createAuditMiddleware(options) {
|
|
|
95
95
|
metadata: metadata || null,
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
knex(tableName).insert(row).catch(
|
|
99
|
-
console.warn('[audit-log] Failed to insert audit row:', err.message);
|
|
100
|
-
});
|
|
98
|
+
knex(tableName).insert(row).catch(logAuditError);
|
|
101
99
|
} catch (e) {
|
|
102
|
-
|
|
100
|
+
logAuditError(e, 'finish handler error');
|
|
103
101
|
}
|
|
104
102
|
});
|
|
105
103
|
|
|
@@ -107,9 +105,46 @@ function createAuditMiddleware(options) {
|
|
|
107
105
|
};
|
|
108
106
|
}
|
|
109
107
|
|
|
108
|
+
let lastAuditWarnTime = 0;
|
|
109
|
+
let suppressedAuditErrors = 0;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Format and sanitize DB errors to prevent leaking raw SQL queries
|
|
113
|
+
* @param {Error|Object|string} e
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
function formatDbError(e) {
|
|
117
|
+
if (!e) return 'Unknown error';
|
|
118
|
+
let msg;
|
|
119
|
+
if (typeof e.message === 'string' && e.message.includes(' - ')) {
|
|
120
|
+
msg = e.message.split(' - ').pop().trim();
|
|
121
|
+
} else {
|
|
122
|
+
msg = String(e.code || e.message || 'Database error');
|
|
123
|
+
}
|
|
124
|
+
return msg.replace(/[\r\n]+/g, ' ').trim().slice(0, 150);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function logAuditError(err, prefix = 'Failed to insert audit row') {
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
const formatted = formatDbError(err);
|
|
130
|
+
if (lastAuditWarnTime === 0 || now - lastAuditWarnTime >= 60000) {
|
|
131
|
+
const elapsedSec = lastAuditWarnTime > 0 ? Math.round((now - lastAuditWarnTime) / 1000) : 0;
|
|
132
|
+
if (suppressedAuditErrors > 0) {
|
|
133
|
+
console.warn(`[audit-log] ${prefix} (suppressed ${suppressedAuditErrors} occurrences in last ${elapsedSec}s): ${formatted}`);
|
|
134
|
+
} else {
|
|
135
|
+
console.warn(`[audit-log] ${prefix}: ${formatted}`);
|
|
136
|
+
}
|
|
137
|
+
lastAuditWarnTime = now;
|
|
138
|
+
suppressedAuditErrors = 0;
|
|
139
|
+
} else {
|
|
140
|
+
suppressedAuditErrors++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
110
144
|
module.exports = {
|
|
111
145
|
createAuditMiddleware,
|
|
112
146
|
stringifyId,
|
|
113
147
|
extractResourceIdFromJsonBody,
|
|
114
148
|
buildMetadata,
|
|
149
|
+
formatDbError,
|
|
115
150
|
};
|
package/plugins/csrf/index.js
CHANGED
|
@@ -199,7 +199,7 @@ function csrfPlugin(options = {}) {
|
|
|
199
199
|
|
|
200
200
|
if (!submitted || !safeCompare(submitted, csrfTokenValue)) {
|
|
201
201
|
res.status(errorStatus);
|
|
202
|
-
if (req.accepts('html') && !req.path.startsWith('/api')) {
|
|
202
|
+
if (typeof req.accepts === 'function' && req.accepts('html') && !req.path.startsWith('/api')) {
|
|
203
203
|
return res.send(`<h1>${errorStatus} Forbidden</h1><p>${errorMessage}</p>`);
|
|
204
204
|
} else {
|
|
205
205
|
return res.json({ error: errorMessage });
|
package/plugins/email/service.js
CHANGED
|
@@ -30,6 +30,37 @@ function createEmailService(options) {
|
|
|
30
30
|
console.warn('[email]', err.message);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
let lastEmailLogWarnTime = 0;
|
|
34
|
+
let suppressedEmailLogErrors = 0;
|
|
35
|
+
|
|
36
|
+
function formatDbError(e) {
|
|
37
|
+
if (!e) return 'Unknown error';
|
|
38
|
+
let msg;
|
|
39
|
+
if (typeof e.message === 'string' && e.message.includes(' - ')) {
|
|
40
|
+
msg = e.message.split(' - ').pop().trim();
|
|
41
|
+
} else {
|
|
42
|
+
msg = String(e.code || e.message || 'Database error');
|
|
43
|
+
}
|
|
44
|
+
return msg.replace(/[\r\n]+/g, ' ').trim().slice(0, 150);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function logEmailError(err) {
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
const formatted = formatDbError(err);
|
|
50
|
+
if (lastEmailLogWarnTime === 0 || now - lastEmailLogWarnTime >= 60000) {
|
|
51
|
+
const elapsedSec = lastEmailLogWarnTime > 0 ? Math.round((now - lastEmailLogWarnTime) / 1000) : 0;
|
|
52
|
+
if (suppressedEmailLogErrors > 0) {
|
|
53
|
+
console.warn(`[email] Failed to write email log (suppressed ${suppressedEmailLogErrors} occurrences in last ${elapsedSec}s): ${formatted}`);
|
|
54
|
+
} else {
|
|
55
|
+
console.warn('[email] Failed to write email log:', formatted);
|
|
56
|
+
}
|
|
57
|
+
lastEmailLogWarnTime = now;
|
|
58
|
+
suppressedEmailLogErrors = 0;
|
|
59
|
+
} else {
|
|
60
|
+
suppressedEmailLogErrors++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
/**
|
|
34
65
|
* @param {Object} row
|
|
35
66
|
*/
|
|
@@ -49,7 +80,7 @@ function createEmailService(options) {
|
|
|
49
80
|
created_at: new Date(),
|
|
50
81
|
});
|
|
51
82
|
} catch (err) {
|
|
52
|
-
|
|
83
|
+
logEmailError(err);
|
|
53
84
|
}
|
|
54
85
|
}
|
|
55
86
|
|
|
@@ -4,15 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const multer = require('multer');
|
|
7
|
-
const
|
|
8
|
-
listFiles,
|
|
9
|
-
createDirectory,
|
|
10
|
-
renameItem,
|
|
11
|
-
deleteItem,
|
|
12
|
-
moveItem,
|
|
13
|
-
saveUploadedFile,
|
|
14
|
-
calculateStorageUsage,
|
|
15
|
-
} = require('./core');
|
|
7
|
+
const core = require('./core');
|
|
16
8
|
|
|
17
9
|
/**
|
|
18
10
|
* Create Multer middleware for in-memory upload parsing
|
|
@@ -71,7 +63,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
71
63
|
const permissions = options.permissions || null;
|
|
72
64
|
const securityOpts = options.security || {};
|
|
73
65
|
|
|
74
|
-
const parseUpload = createUploadMiddleware(maxUploadBytes);
|
|
66
|
+
const parseUpload = options.uploadMiddleware || createUploadMiddleware(maxUploadBytes);
|
|
75
67
|
|
|
76
68
|
return {
|
|
77
69
|
async list(req, res) {
|
|
@@ -88,7 +80,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
88
80
|
const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
|
|
89
81
|
const perPage = req.query.perPage ? parseInt(req.query.perPage, 10) : undefined;
|
|
90
82
|
|
|
91
|
-
const result = await listFiles(baseDir, relPath, {
|
|
83
|
+
const result = await core.listFiles(baseDir, relPath, {
|
|
92
84
|
search,
|
|
93
85
|
type,
|
|
94
86
|
sort,
|
|
@@ -115,7 +107,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
115
107
|
if (!name) {
|
|
116
108
|
return res.status(400).json({ success: false, error: 'Directory name is required' });
|
|
117
109
|
}
|
|
118
|
-
const result = await createDirectory(baseDir, relPath, name);
|
|
110
|
+
const result = await core.createDirectory(baseDir, relPath, name);
|
|
119
111
|
return res.status(201).json({ success: true, data: result });
|
|
120
112
|
} catch (err) {
|
|
121
113
|
const status = err.statusCode || err.status || 400;
|
|
@@ -133,7 +125,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
133
125
|
if (!itemPath || !newName) {
|
|
134
126
|
return res.status(400).json({ success: false, error: 'Path and newName are required' });
|
|
135
127
|
}
|
|
136
|
-
const result = await renameItem(baseDir, itemPath, newName);
|
|
128
|
+
const result = await core.renameItem(baseDir, itemPath, newName);
|
|
137
129
|
return res.json({ success: true, data: result });
|
|
138
130
|
} catch (err) {
|
|
139
131
|
const status = err.statusCode || err.status || 400;
|
|
@@ -151,7 +143,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
151
143
|
if (!source) {
|
|
152
144
|
return res.status(400).json({ success: false, error: 'Source path is required' });
|
|
153
145
|
}
|
|
154
|
-
const result = await moveItem(baseDir, source, destination);
|
|
146
|
+
const result = await core.moveItem(baseDir, source, destination);
|
|
155
147
|
return res.json({ success: true, data: result });
|
|
156
148
|
} catch (err) {
|
|
157
149
|
const status = err.statusCode || err.status || 400;
|
|
@@ -169,7 +161,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
169
161
|
if (!itemPath) {
|
|
170
162
|
return res.status(400).json({ success: false, error: 'Path is required' });
|
|
171
163
|
}
|
|
172
|
-
const result = await deleteItem(baseDir, itemPath);
|
|
164
|
+
const result = await core.deleteItem(baseDir, itemPath);
|
|
173
165
|
return res.json({ success: true, data: result });
|
|
174
166
|
} catch (err) {
|
|
175
167
|
const status = err.statusCode || err.status || 400;
|
|
@@ -199,7 +191,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
199
191
|
|
|
200
192
|
// Storage Quota Check
|
|
201
193
|
if (maxTotalStorageBytes && typeof maxTotalStorageBytes === 'number') {
|
|
202
|
-
const currentUsage = await calculateStorageUsage(baseDir);
|
|
194
|
+
const currentUsage = await core.calculateStorageUsage(baseDir);
|
|
203
195
|
const incomingBytes = files.reduce((acc, f) => acc + (f.size || f.buffer?.length || 0), 0);
|
|
204
196
|
if (currentUsage + incomingBytes > maxTotalStorageBytes) {
|
|
205
197
|
return res.status(507).json({
|
|
@@ -229,7 +221,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
229
221
|
try {
|
|
230
222
|
const uploaded = [];
|
|
231
223
|
for (const file of files) {
|
|
232
|
-
const saved = await saveUploadedFile(
|
|
224
|
+
const saved = await core.saveUploadedFile(
|
|
233
225
|
baseDir,
|
|
234
226
|
targetRelPath,
|
|
235
227
|
file.buffer,
|
|
@@ -257,5 +249,6 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
257
249
|
|
|
258
250
|
module.exports = {
|
|
259
251
|
createFileManagerApiHandlers,
|
|
252
|
+
createUploadMiddleware,
|
|
260
253
|
isActionAllowed,
|
|
261
254
|
};
|