webspresso 0.0.104 → 0.0.106
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 +21 -2
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +220 -8
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/FLAGS.md +118 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/LOGGING.md +93 -0
- package/.agents/METRICS.md +127 -0
- package/.agents/MIGRATION_JOBS.md +95 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +38 -3
- package/.agents/QUEUE.md +42 -0
- package/.agents/RATE_LIMIT.md +147 -0
- package/.agents/RESILIENCE.md +235 -0
- package/.agents/SCALE_OUT.md +109 -0
- package/.agents/SECRETS.md +117 -0
- package/AGENTS.md +16 -0
- package/README.md +2 -2
- package/bin/commands/add-aws.js +396 -0
- package/bin/commands/add-ci.js +523 -0
- package/bin/commands/add-cloudflare.js +240 -0
- package/bin/commands/add-cloudrun.js +381 -0
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/add-migration-job.js +396 -0
- package/bin/commands/assets-sync.js +209 -0
- package/bin/commands/client-generate.js +52 -0
- package/bin/commands/console.js +53 -0
- package/bin/commands/db-migrate.js +47 -2
- package/bin/commands/db-studio.js +84 -0
- package/bin/commands/deploy-check.js +242 -13
- package/bin/commands/dev.js +1 -1
- package/bin/commands/export.js +87 -0
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/secrets-pull.js +208 -0
- package/bin/commands/start.js +1 -1
- package/bin/utils/db.js +7 -1
- package/bin/utils/project.js +2 -2
- package/bin/webspresso.js +22 -0
- package/core/app.js +421 -0
- package/core/auth/authenticate.js +93 -0
- package/core/auth/hash.js +5 -4
- package/core/auth/index.js +34 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +53 -10
- package/core/auth/middleware.js +16 -68
- package/core/auth/oauth.js +8 -17
- package/core/auth/redis-session.js +1008 -0
- package/core/auth/sessions.js +156 -0
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/cdn.js +296 -0
- package/core/cache/index.js +33 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +354 -0
- package/core/commands/registry.js +6 -20
- package/core/console/commands.js +148 -0
- package/core/console/context.js +260 -0
- package/core/console/eval.js +70 -0
- package/core/console/index.js +22 -0
- package/core/console/repl.js +102 -0
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +49 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +9 -7
- package/core/events/signal.js +7 -0
- package/core/export/discovery.js +191 -0
- package/core/export/exporter.js +277 -0
- package/core/export/index.js +24 -0
- package/core/flags/evaluator.js +252 -0
- package/core/flags/hasher.js +37 -0
- package/core/flags/index.js +21 -0
- package/core/flags/manager.js +263 -0
- package/core/flags/store.js +126 -0
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/http/adapter.js +74 -0
- package/core/http/express-adapter.js +46 -0
- package/core/http/hono-adapter.js +531 -0
- package/core/http/index.js +41 -0
- package/core/http/pipeline.js +485 -0
- package/core/index.js +336 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +325 -0
- package/core/limits/normalize.js +222 -0
- package/core/logger/formatters.js +264 -0
- package/core/logger/index.js +260 -0
- package/core/logger/trace.js +137 -0
- package/core/metrics/collectors.js +125 -0
- package/core/metrics/index.js +45 -0
- package/core/metrics/middleware.js +230 -0
- package/core/metrics/registry.js +366 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/d1.js +265 -0
- package/core/orm/index.js +176 -13
- package/core/orm/json-fields.js +22 -8
- package/core/orm/migrate-cloud.js +194 -0
- package/core/orm/model.js +10 -0
- package/core/orm/pool.js +97 -0
- package/core/orm/query-builder.js +72 -9
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/cloudflare.js +131 -0
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/adapters/pubsub.js +400 -0
- package/core/queue/adapters/sqs.js +419 -0
- package/core/queue/adapters/wait-until.js +77 -0
- package/core/queue/index.js +8 -0
- package/core/queue/manager.js +66 -41
- package/core/rate-limit/index.js +22 -0
- package/core/rate-limit/limiter.js +219 -0
- package/core/rate-limit/stores/memory.js +129 -0
- package/core/rate-limit/stores/redis.js +151 -0
- package/core/rate-limit/utils.js +112 -0
- package/core/resilience/circuit-breaker.js +422 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/secrets/index.js +113 -0
- package/core/secrets/manager.js +308 -0
- package/core/secrets/providers/aws.js +288 -0
- package/core/secrets/providers/env.js +44 -0
- package/core/secrets/providers/gcp.js +307 -0
- package/core/secrets/providers/memory.js +76 -0
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +129 -19
- package/core/stream/index.js +19 -0
- package/core/stream/text-stream.js +326 -0
- package/core/studio/index.js +43 -0
- package/core/studio/inspector.js +258 -0
- package/core/studio/server.js +325 -0
- package/core/studio/ui.js +681 -0
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +1549 -2
- package/index.js +272 -2
- package/package.json +3 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +13 -6
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/index.js +3 -2
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/index.js +3 -1
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +3 -2
- package/plugins/cors.js +3 -2
- package/plugins/csrf/index.js +9 -12
- package/plugins/csv/index.js +8 -5
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/email/index.js +11 -9
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +5 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/queue/index.js +18 -2
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +16 -9
- package/plugins/rls/index.js +170 -0
- package/plugins/seo-checker/index.js +2 -2
- package/plugins/site-analytics/index.js +3 -1
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +12 -5
- package/plugins/upload/gcs-provider.js +148 -0
- package/plugins/upload/index.js +136 -77
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/upload/s3-provider.js +257 -0
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +33 -23
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/plugins/xlsx/index.js +7 -5
- package/src/api/api-loader.js +12 -95
- package/src/api/define-api.js +1 -0
- package/src/app-context.js +41 -5
- package/src/client-runtime/constants.js +10 -0
- package/src/client-runtime/mount.js +2 -3
- package/src/dev/banner.js +6 -2
- package/src/dev/client-gen.js +458 -0
- package/src/discovery/scan-routes.js +5 -2
- package/src/file-router.js +3 -3
- package/src/helpers.js +212 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +13 -154
- package/src/plugin-manager.js +99 -18
- package/src/server.js +335 -133
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +95 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +15 -1
- package/src/services/validator.js +38 -20
- package/src/template/precompile.js +8 -19
|
@@ -8,21 +8,10 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { trimUrlPathSlashes } = require('../../core/url-path-normalize');
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const { walkDirectoryDetailed } = require('../../core/utils/fs');
|
|
12
|
+
const { toCamelCase } = require('../../core/utils/string');
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
* Convert kebab-case or snake_case segment to camelCase
|
|
15
|
-
* @param {string} str
|
|
16
|
-
* @returns {string}
|
|
17
|
-
*/
|
|
18
|
-
function toCamelCase(str) {
|
|
19
|
-
if (!str) return '';
|
|
20
|
-
const parts = str.split(/[-_]+/);
|
|
21
|
-
return parts.map((part, index) => {
|
|
22
|
-
if (!part) return '';
|
|
23
|
-
return index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1);
|
|
24
|
-
}).join('');
|
|
25
|
-
}
|
|
14
|
+
const VALID_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
|
|
26
15
|
|
|
27
16
|
/**
|
|
28
17
|
* Check if a file should be included as a service
|
|
@@ -105,36 +94,26 @@ function discoverServices(servicesDir) {
|
|
|
105
94
|
return [];
|
|
106
95
|
}
|
|
107
96
|
|
|
108
|
-
const
|
|
97
|
+
const items = walkDirectoryDetailed(servicesDir, {
|
|
98
|
+
extensions: VALID_EXTENSIONS,
|
|
99
|
+
ignorePrefixes: ['.', '_'],
|
|
100
|
+
ignoreNames: ['node_modules'],
|
|
101
|
+
ignoreTests: true,
|
|
102
|
+
});
|
|
109
103
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (entry.isDirectory()) {
|
|
122
|
-
scan(fullPath, relPath);
|
|
123
|
-
} else if (entry.isFile() && isValidServiceFile(entry.name)) {
|
|
124
|
-
const { name, aliases } = filePathToServiceName(relPath);
|
|
125
|
-
if (name) {
|
|
126
|
-
results.push({
|
|
127
|
-
name,
|
|
128
|
-
aliases,
|
|
129
|
-
filePath: fullPath,
|
|
130
|
-
relativePath: relPath.replace(/\\/g, '/'),
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
104
|
+
const results = [];
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
const { name, aliases } = filePathToServiceName(item.relativePath);
|
|
107
|
+
if (name) {
|
|
108
|
+
results.push({
|
|
109
|
+
name,
|
|
110
|
+
aliases,
|
|
111
|
+
filePath: item.absolutePath,
|
|
112
|
+
relativePath: item.relativePath,
|
|
113
|
+
});
|
|
134
114
|
}
|
|
135
115
|
}
|
|
136
116
|
|
|
137
|
-
scan(path.resolve(servicesDir));
|
|
138
117
|
return results;
|
|
139
118
|
}
|
|
140
119
|
|
package/src/services/executor.js
CHANGED
|
@@ -17,6 +17,12 @@ const {
|
|
|
17
17
|
runWithAmbientTransaction,
|
|
18
18
|
hasAmbientTransaction,
|
|
19
19
|
} = require('../../core/orm/transaction');
|
|
20
|
+
const { ensureArray } = require('../../core/utils/object');
|
|
21
|
+
const {
|
|
22
|
+
CircuitBreaker,
|
|
23
|
+
circuitBreakerRegistry,
|
|
24
|
+
retry,
|
|
25
|
+
} = require('../../core/resilience');
|
|
20
26
|
|
|
21
27
|
const SERVICE_CALL_STACK_SYMBOL = Symbol.for('webspresso.service.call_stack');
|
|
22
28
|
|
|
@@ -93,7 +99,7 @@ async function checkServiceAuth(serviceDef, ctx, options = {}) {
|
|
|
93
99
|
if (!user) {
|
|
94
100
|
throw new UnauthorizedError(`Authentication required to execute service '${serviceDef.name}'`);
|
|
95
101
|
}
|
|
96
|
-
const roles =
|
|
102
|
+
const roles = ensureArray(user.roles || user.role);
|
|
97
103
|
if (!roles.includes(authRequirement)) {
|
|
98
104
|
throw new ForbiddenError(
|
|
99
105
|
`Insufficient permissions to execute service '${serviceDef.name}' (required role: '${authRequirement}')`
|
|
@@ -107,7 +113,7 @@ async function checkServiceAuth(serviceDef, ctx, options = {}) {
|
|
|
107
113
|
if (!user) {
|
|
108
114
|
throw new UnauthorizedError(`Authentication required to execute service '${serviceDef.name}'`);
|
|
109
115
|
}
|
|
110
|
-
const roles =
|
|
116
|
+
const roles = ensureArray(user.roles || user.role);
|
|
111
117
|
const hasAny = authRequirement.some(r => roles.includes(r));
|
|
112
118
|
if (!hasAny) {
|
|
113
119
|
throw new ForbiddenError(
|
|
@@ -185,15 +191,71 @@ async function executeService(registry, name, input = {}, ctx = {}, options = {}
|
|
|
185
191
|
registry.invalidate(subName, subInput, subCtx || executionContext);
|
|
186
192
|
executionContext.service.clearCache = (subName) =>
|
|
187
193
|
registry.clearCache(subName);
|
|
194
|
+
executionContext.breaker = (bName, bOpts) =>
|
|
195
|
+
circuitBreakerRegistry.getBreaker(bName, bOpts);
|
|
196
|
+
executionContext.retry = retry;
|
|
188
197
|
|
|
189
198
|
// Validate input
|
|
190
199
|
const validatedInput = await validateServiceInput(serviceDef.schema, input, name);
|
|
200
|
+
if (validatedInput && typeof validatedInput === 'object' && !('input' in validatedInput)) {
|
|
201
|
+
try {
|
|
202
|
+
Object.defineProperty(validatedInput, 'input', {
|
|
203
|
+
value: validatedInput,
|
|
204
|
+
enumerable: false,
|
|
205
|
+
configurable: true,
|
|
206
|
+
});
|
|
207
|
+
} catch (_) {}
|
|
208
|
+
}
|
|
191
209
|
|
|
192
210
|
const startTime = Date.now();
|
|
193
211
|
const logger = executionContext.logger || registry.logger || null;
|
|
194
212
|
|
|
195
|
-
const timeoutConfig = options.timeout ?? serviceDef.timeout;
|
|
196
|
-
const timeoutMs = timeoutConfig
|
|
213
|
+
const timeoutConfig = options.timeout ?? serviceDef.timeout ?? executionContext?.timeouts?.service ?? registry?.timeouts?.service ?? registry?.timeout;
|
|
214
|
+
const timeoutMs = (timeoutConfig !== undefined && timeoutConfig !== null && timeoutConfig !== false)
|
|
215
|
+
? parseTtlMs(timeoutConfig)
|
|
216
|
+
: null;
|
|
217
|
+
|
|
218
|
+
// Resolve circuit breaker configuration
|
|
219
|
+
let breaker = null;
|
|
220
|
+
const cbConfig = options.skipCircuitBreaker === true
|
|
221
|
+
? null
|
|
222
|
+
: (options.circuitBreaker !== undefined ? options.circuitBreaker : serviceDef.circuitBreaker);
|
|
223
|
+
|
|
224
|
+
if (cbConfig) {
|
|
225
|
+
if (cbConfig instanceof CircuitBreaker) {
|
|
226
|
+
breaker = cbConfig;
|
|
227
|
+
} else if (typeof cbConfig === 'string') {
|
|
228
|
+
breaker = circuitBreakerRegistry.getBreaker(cbConfig);
|
|
229
|
+
} else if (typeof cbConfig === 'object' && cbConfig !== null) {
|
|
230
|
+
const breakerName = cbConfig.name || name;
|
|
231
|
+
breaker = circuitBreakerRegistry.getBreaker(breakerName, cbConfig);
|
|
232
|
+
} else if (cbConfig === true) {
|
|
233
|
+
breaker = circuitBreakerRegistry.getBreaker(name);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Resolve retry configuration
|
|
238
|
+
const retryConfig = options.skipRetry === true
|
|
239
|
+
? null
|
|
240
|
+
: (options.retry !== undefined ? options.retry : serviceDef.retry);
|
|
241
|
+
|
|
242
|
+
const runWithRetry = async (fn) => {
|
|
243
|
+
if (!retryConfig) {
|
|
244
|
+
return await fn();
|
|
245
|
+
}
|
|
246
|
+
const retryOpts = typeof retryConfig === 'number'
|
|
247
|
+
? { retries: retryConfig }
|
|
248
|
+
: (typeof retryConfig === 'object' && retryConfig !== null ? retryConfig : { retries: 3 });
|
|
249
|
+
|
|
250
|
+
return await retry((attempt) => fn(attempt), retryOpts);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const runWithBreaker = async (fn) => {
|
|
254
|
+
if (!breaker) {
|
|
255
|
+
return await fn(validatedInput, executionContext);
|
|
256
|
+
}
|
|
257
|
+
return await breaker.execute(fn, validatedInput, executionContext);
|
|
258
|
+
};
|
|
197
259
|
|
|
198
260
|
// Helper to execute handler with optional database transaction
|
|
199
261
|
const executeHandler = async (activeCtx) => {
|
|
@@ -227,6 +289,9 @@ async function executeService(registry, name, input = {}, ctx = {}, options = {}
|
|
|
227
289
|
registry.invalidate(subName, subInput, subCtx || txCtx);
|
|
228
290
|
txCtx.service.clearCache = (subName) =>
|
|
229
291
|
registry.clearCache(subName);
|
|
292
|
+
txCtx.breaker = (bName, bOpts) =>
|
|
293
|
+
circuitBreakerRegistry.getBreaker(bName, bOpts);
|
|
294
|
+
txCtx.retry = retry;
|
|
230
295
|
return await serviceDef.handler(validatedInput, txCtx);
|
|
231
296
|
});
|
|
232
297
|
});
|
|
@@ -236,31 +301,36 @@ async function executeService(registry, name, input = {}, ctx = {}, options = {}
|
|
|
236
301
|
};
|
|
237
302
|
|
|
238
303
|
try {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
304
|
+
const invokeOperation = () => {
|
|
305
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
306
|
+
let timerId;
|
|
307
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
308
|
+
timerId = setTimeout(() => {
|
|
309
|
+
const timeoutErr = new WebspressoError(`Service '${name}' timed out after ${timeoutMs}ms`);
|
|
310
|
+
timeoutErr.code = 'SERVICE_TIMEOUT';
|
|
311
|
+
timeoutErr.statusCode = 504;
|
|
312
|
+
timeoutErr.service = name;
|
|
313
|
+
reject(timeoutErr);
|
|
314
|
+
}, timeoutMs);
|
|
315
|
+
if (timerId && typeof timerId.unref === 'function') {
|
|
316
|
+
timerId.unref();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
252
319
|
|
|
253
|
-
|
|
254
|
-
result = await Promise.race([
|
|
320
|
+
return Promise.race([
|
|
255
321
|
executeHandler(executionContext),
|
|
256
322
|
timeoutPromise,
|
|
257
|
-
])
|
|
258
|
-
|
|
259
|
-
|
|
323
|
+
]).finally(() => {
|
|
324
|
+
if (timerId) {
|
|
325
|
+
clearTimeout(timerId);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
260
328
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
329
|
+
|
|
330
|
+
return executeHandler(executionContext);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const result = await runWithBreaker(() => runWithRetry(invokeOperation));
|
|
264
334
|
|
|
265
335
|
const duration = Date.now() - startTime;
|
|
266
336
|
if (logger && typeof logger.debug === 'function') {
|
package/src/services/memoize.js
CHANGED
|
@@ -4,36 +4,14 @@
|
|
|
4
4
|
* @module src/services/memoize
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const { parseDurationMs } = require('../../core/utils/time');
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Parse TTL duration into milliseconds
|
|
9
11
|
* @param {string|number|boolean} ttl - Duration string ('5s', '1m', '5m', '1h', '1d'), ms number, or boolean
|
|
10
12
|
* @returns {number} Milliseconds
|
|
11
13
|
*/
|
|
12
|
-
|
|
13
|
-
if (typeof ttl === 'number') {
|
|
14
|
-
return ttl > 0 ? ttl : 60_000;
|
|
15
|
-
}
|
|
16
|
-
if (ttl === true || !ttl) {
|
|
17
|
-
return 60_000; // Default 1 minute
|
|
18
|
-
}
|
|
19
|
-
if (typeof ttl === 'string') {
|
|
20
|
-
const match = ttl.trim().match(/^(\d+)\s*([smhd])?$/i);
|
|
21
|
-
if (match) {
|
|
22
|
-
const val = parseInt(match[1], 10);
|
|
23
|
-
const unit = (match[2] || 's').toLowerCase();
|
|
24
|
-
switch (unit) {
|
|
25
|
-
case 's': return val * 1_000;
|
|
26
|
-
case 'm': return val * 60_000;
|
|
27
|
-
case 'h': return val * 3_600_000;
|
|
28
|
-
case 'd': return val * 86_400_000;
|
|
29
|
-
default: return val * 1_000;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
const num = parseInt(ttl, 10);
|
|
33
|
-
return !Number.isNaN(num) && num > 0 ? num : 60_000;
|
|
34
|
-
}
|
|
35
|
-
return 60_000;
|
|
36
|
-
}
|
|
14
|
+
const parseTtlMs = parseDurationMs;
|
|
37
15
|
|
|
38
16
|
/**
|
|
39
17
|
* Safely deep clone an object or array to prevent cache mutation
|
package/src/services/registry.js
CHANGED
|
@@ -21,6 +21,8 @@ class ServiceRegistry {
|
|
|
21
21
|
this.servicesDir = options.servicesDir ? path.resolve(options.servicesDir) : null;
|
|
22
22
|
this.isDev = Boolean(options.isDev);
|
|
23
23
|
this.logger = options.logger || null;
|
|
24
|
+
this.timeouts = options.timeouts || null;
|
|
25
|
+
this.timeout = options.timeout !== undefined ? options.timeout : null;
|
|
24
26
|
|
|
25
27
|
/** @type {Map<string, Object>} */
|
|
26
28
|
this.services = new Map();
|
|
@@ -107,6 +109,8 @@ class ServiceRegistry {
|
|
|
107
109
|
cache: definition.cache,
|
|
108
110
|
timeout: definition.timeout,
|
|
109
111
|
transaction: definition.transaction,
|
|
112
|
+
circuitBreaker: definition.circuitBreaker,
|
|
113
|
+
retry: definition.retry,
|
|
110
114
|
};
|
|
111
115
|
|
|
112
116
|
this.services.set(name, serviceDef);
|
|
@@ -125,7 +129,15 @@ class ServiceRegistry {
|
|
|
125
129
|
* @param {Object|Function} definition - Service definition or handler function
|
|
126
130
|
* @returns {ServiceRegistry}
|
|
127
131
|
*/
|
|
128
|
-
register(
|
|
132
|
+
register(nameOrDef, maybeDef, options = {}) {
|
|
133
|
+
let name = nameOrDef;
|
|
134
|
+
let definition = maybeDef;
|
|
135
|
+
|
|
136
|
+
if (nameOrDef && typeof nameOrDef === 'object' && nameOrDef.name && maybeDef === undefined) {
|
|
137
|
+
name = nameOrDef.name;
|
|
138
|
+
definition = nameOrDef;
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
if (!name || typeof name !== 'string') {
|
|
130
142
|
throw new WebspressoError('Service name must be a non-empty string');
|
|
131
143
|
}
|
|
@@ -164,6 +176,8 @@ class ServiceRegistry {
|
|
|
164
176
|
cache: def.cache,
|
|
165
177
|
timeout: def.timeout,
|
|
166
178
|
transaction: def.transaction,
|
|
179
|
+
circuitBreaker: def.circuitBreaker,
|
|
180
|
+
retry: def.retry,
|
|
167
181
|
};
|
|
168
182
|
|
|
169
183
|
this.services.set(name, serviceDef);
|
|
@@ -72,6 +72,26 @@ function descriptorToZod(typeName) {
|
|
|
72
72
|
return finalSchema;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function shapeFromDescriptor(descriptorObj) {
|
|
76
|
+
const shape = {};
|
|
77
|
+
for (const [key, val] of Object.entries(descriptorObj)) {
|
|
78
|
+
if (typeof val === 'string') {
|
|
79
|
+
shape[key] = descriptorToZod(val);
|
|
80
|
+
} else if (val instanceof z.ZodType || (val && typeof val.safeParse === 'function')) {
|
|
81
|
+
shape[key] = val;
|
|
82
|
+
} else if (typeof val === 'object' && val !== null) {
|
|
83
|
+
const isOpt = val.optional === true || val.required === false;
|
|
84
|
+
let base = val.type ? descriptorToZod(String(val.type)) : z.any();
|
|
85
|
+
shape[key] = isOpt ? base.optional() : base;
|
|
86
|
+
} else if (typeof val === 'function') {
|
|
87
|
+
shape[key] = z.custom(val);
|
|
88
|
+
} else {
|
|
89
|
+
shape[key] = z.any();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return z.object(shape);
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
const compiledSchemaCache = new WeakMap();
|
|
76
96
|
|
|
77
97
|
/**
|
|
@@ -100,6 +120,14 @@ function compileServiceSchema(rawSchema) {
|
|
|
100
120
|
const res = rawSchema({ z: zForServices });
|
|
101
121
|
if (res && (res instanceof z.ZodType || typeof res.safeParse === 'function')) {
|
|
102
122
|
compiled = res;
|
|
123
|
+
} else if (res && typeof res === 'object' && !(res instanceof Promise) && typeof res.then !== 'function' && (res.constructor === Object || !res.constructor)) {
|
|
124
|
+
if (res.input && (res.input instanceof z.ZodType || typeof res.input.safeParse === 'function' || (typeof res.input === 'object' && res.input !== null))) {
|
|
125
|
+
compiled = (res.input instanceof z.ZodType || typeof res.input.safeParse === 'function')
|
|
126
|
+
? res.input
|
|
127
|
+
: shapeFromDescriptor(res.input);
|
|
128
|
+
} else {
|
|
129
|
+
compiled = shapeFromDescriptor(res);
|
|
130
|
+
}
|
|
103
131
|
}
|
|
104
132
|
} catch (e) {
|
|
105
133
|
// If invoking with { z } throws, treat as a direct validator function
|
|
@@ -108,28 +136,18 @@ function compileServiceSchema(rawSchema) {
|
|
|
108
136
|
compiled = rawSchema;
|
|
109
137
|
}
|
|
110
138
|
} else if (typeof rawSchema === 'object' && rawSchema !== null) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
139
|
+
if (rawSchema.input && (rawSchema.input instanceof z.ZodType || typeof rawSchema.input.safeParse === 'function' || (typeof rawSchema.input === 'object' && rawSchema.input !== null))) {
|
|
140
|
+
compiled = (rawSchema.input instanceof z.ZodType || typeof rawSchema.input.safeParse === 'function')
|
|
141
|
+
? rawSchema.input
|
|
142
|
+
: shapeFromDescriptor(rawSchema.input);
|
|
115
143
|
} else {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
} else if (typeof val === 'object' && val !== null) {
|
|
123
|
-
const isOpt = val.optional === true || val.required === false;
|
|
124
|
-
let base = val.type ? descriptorToZod(String(val.type)) : z.any();
|
|
125
|
-
shape[key] = isOpt ? base.optional() : base;
|
|
126
|
-
} else if (typeof val === 'function') {
|
|
127
|
-
shape[key] = z.custom(val);
|
|
128
|
-
} else {
|
|
129
|
-
shape[key] = z.any();
|
|
130
|
-
}
|
|
144
|
+
// If object descriptor: { id: 'number', name: 'string', email: 'email' }
|
|
145
|
+
const keys = Object.keys(rawSchema);
|
|
146
|
+
if (keys.length === 0) {
|
|
147
|
+
compiled = null;
|
|
148
|
+
} else {
|
|
149
|
+
compiled = shapeFromDescriptor(rawSchema);
|
|
131
150
|
}
|
|
132
|
-
compiled = z.object(shape);
|
|
133
151
|
}
|
|
134
152
|
} else {
|
|
135
153
|
compiled = rawSchema;
|
|
@@ -11,6 +11,7 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const nunjucks = require('nunjucks');
|
|
13
13
|
const { parseNjkFrontmatter, frontmatterToPatches, seedProdRouteCache } = require('../njk-frontmatter');
|
|
14
|
+
const { walkDirectoryDetailed } = require('../../core/utils/fs');
|
|
14
15
|
|
|
15
16
|
const DEFAULT_PRECOMPILED_REL_PATH = path.join('.webspresso', 'templates.compiled.js');
|
|
16
17
|
|
|
@@ -21,25 +22,13 @@ const DEFAULT_PRECOMPILED_REL_PATH = path.join('.webspresso', 'templates.compile
|
|
|
21
22
|
* @returns {Array<{ absolutePath: string, relativePath: string }>}
|
|
22
23
|
*/
|
|
23
24
|
function scanTemplateDir(dir, baseDir = dir) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
32
|
-
results.push(...scanTemplateDir(full, baseDir));
|
|
33
|
-
} else if (entry.isFile()) {
|
|
34
|
-
const ext = path.extname(entry.name);
|
|
35
|
-
if (ext === '.njk' || ext === '.html') {
|
|
36
|
-
const rel = path.relative(baseDir, full).split(path.sep).join('/');
|
|
37
|
-
results.push({ absolutePath: full, relativePath: rel });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return results;
|
|
25
|
+
return walkDirectoryDetailed(dir, {
|
|
26
|
+
extensions: ['.njk', '.html'],
|
|
27
|
+
ignorePrefixes: ['.'],
|
|
28
|
+
ignoreNames: ['node_modules'],
|
|
29
|
+
ignoreTests: false,
|
|
30
|
+
baseDir,
|
|
31
|
+
});
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
/**
|