webspresso 0.0.89 → 0.0.91
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 +450 -53
- package/bin/commands/service.js +193 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/manager.js +36 -26
- package/core/auth/middleware.js +21 -2
- package/core/compression/compressible.js +51 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +301 -0
- package/core/compression/negotiate.js +153 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/orm/complexity.js +120 -0
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +45 -10
- package/core/orm/model.js +10 -2
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/scopes.js +2 -1
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +284 -0
- package/core/queue/adapters/memory.js +155 -0
- package/core/queue/adapters/redis.js +336 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +387 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/index.d.ts +829 -52
- package/index.js +103 -0
- package/package.json +22 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +67 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
- package/plugins/admin-panel/client/parts/03-pagination-intro.js +25 -18
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
- package/plugins/admin-panel/client/parts/08-record-list.js +27 -34
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +131 -18
- package/plugins/admin-panel/modules/admin-users.js +770 -0
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/content/client/inline-edit.js +41 -17
- package/plugins/cors.js +12 -8
- package/plugins/csrf/index.js +8 -8
- package/plugins/data-exchange/export-xlsx.js +5 -1
- package/plugins/data-exchange/import.js +16 -2
- package/plugins/data-exchange/index.js +14 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +29 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +220 -28
- package/src/helpers.js +24 -5
- package/src/plugin-manager.js +134 -7
- package/src/server.js +470 -44
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +140 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +288 -0
- package/src/services/validator.js +281 -0
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +2 -2
package/README.md
CHANGED
|
@@ -528,24 +528,39 @@ const { app } = createApp({
|
|
|
528
528
|
pagesDir: './pages',
|
|
529
529
|
viewsDir: './views',
|
|
530
530
|
errorPages: {
|
|
531
|
-
//
|
|
532
|
-
notFound: (req, res) =>
|
|
533
|
-
res.render('errors/404.njk', { url: req.url });
|
|
534
|
-
},
|
|
531
|
+
// 404 Not Found: Template path (relative to viewsDir) or custom handler fn
|
|
532
|
+
notFound: '404.njk', // or 'errors/404.njk', or (req, res, ctx) => res.render('404.njk', ctx)
|
|
535
533
|
|
|
536
|
-
//
|
|
537
|
-
serverError: 'errors/500.njk',
|
|
534
|
+
// 500 Internal Server Error: Template path or custom handler fn
|
|
535
|
+
serverError: 'errors/500.njk', // or (err, req, res, ctx) => ...
|
|
538
536
|
|
|
539
|
-
// Timeout
|
|
540
|
-
timeout: 'errors/503.njk'
|
|
537
|
+
// 503 Request Timeout: Template path or custom handler fn
|
|
538
|
+
timeout: 'errors/503.njk' // or (req, res, ctx) => ...
|
|
541
539
|
}
|
|
542
540
|
});
|
|
543
541
|
```
|
|
544
542
|
|
|
545
|
-
Error
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
-
|
|
543
|
+
#### Error Template Context & Behavior
|
|
544
|
+
|
|
545
|
+
All error templates rendered with Nunjucks automatically receive a rich request context:
|
|
546
|
+
- **`404.njk` (`notFound`)**: `{ fsy, locale, isDev, url, method }` — sets HTTP status `404`. If omitted, Webspresso provides a responsive built-in HTML 404 page for browsers, or `{ error: 'Not Found', status: 404 }` JSON for API requests.
|
|
547
|
+
- **`500.njk` (`serverError`)**: `{ fsy, locale, isDev, url, method, error, status }` — sets HTTP status `500` (or `err.status`).
|
|
548
|
+
- **`503.njk` (`timeout`)**: `{ fsy, locale, isDev, url, method }` — sets HTTP status `503`.
|
|
549
|
+
|
|
550
|
+
```njk
|
|
551
|
+
{# Example views/404.njk #}
|
|
552
|
+
{% extends "layout.njk" %}
|
|
553
|
+
|
|
554
|
+
{% block content %}
|
|
555
|
+
<div class="max-w-xl mx-auto text-center py-20">
|
|
556
|
+
<h1 class="text-6xl font-bold text-slate-800">404</h1>
|
|
557
|
+
<p class="text-xl text-slate-600 mt-4">Page Not Found: <code>{{ url }}</code></p>
|
|
558
|
+
<a href="{{ fsy.url('/') }}" class="mt-6 inline-block bg-blue-600 text-white px-6 py-2 rounded-lg">
|
|
559
|
+
Back to Home
|
|
560
|
+
</a>
|
|
561
|
+
</div>
|
|
562
|
+
{% endblock %}
|
|
563
|
+
```
|
|
549
564
|
|
|
550
565
|
**How errors reach this handler**
|
|
551
566
|
|
|
@@ -568,9 +583,11 @@ const { app } = createApp({
|
|
|
568
583
|
});
|
|
569
584
|
```
|
|
570
585
|
|
|
571
|
-
**Asset Management:**
|
|
586
|
+
**Asset Management & Cache-Busting:**
|
|
587
|
+
|
|
588
|
+
Webspresso includes a built-in `AssetManager` that automates static asset URL resolution, query-string cache-busting, Vite/Webpack build manifest lookups, and CDN prefixing in Nunjucks SSR templates.
|
|
572
589
|
|
|
573
|
-
Configure asset handling
|
|
590
|
+
Configure asset handling in `createApp`:
|
|
574
591
|
|
|
575
592
|
```javascript
|
|
576
593
|
const { createApp } = require('webspresso');
|
|
@@ -579,54 +596,84 @@ const path = require('path');
|
|
|
579
596
|
const { app } = createApp({
|
|
580
597
|
pagesDir: './pages',
|
|
581
598
|
viewsDir: './views',
|
|
582
|
-
publicDir: './public',
|
|
599
|
+
publicDir: './public', // default: 'public'
|
|
583
600
|
assets: {
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
601
|
+
// 1. Static query-string versioning (Cache Busting)
|
|
602
|
+
// Appends ?v=1.2.3 (or &v=... if existing query params) to asset URLs
|
|
603
|
+
version: process.env.APP_VERSION || '1.0.0', // or Git commit hash, build timestamp
|
|
604
|
+
|
|
605
|
+
// 2. Build Tool Manifest (Vite, Webpack, etc.)
|
|
606
|
+
// Resolves hashed bundle filenames automatically (e.g. style.css -> assets/style-abc123.css)
|
|
588
607
|
manifestPath: path.join(__dirname, 'public/.vite/manifest.json'),
|
|
589
|
-
|
|
590
|
-
// URL
|
|
591
|
-
|
|
608
|
+
|
|
609
|
+
// 3. Asset URL Prefix / CDN
|
|
610
|
+
// Prepended to all resolved asset paths
|
|
611
|
+
prefix: process.env.CDN_URL || '' // e.g. 'https://cdn.example.com' or '/static'
|
|
592
612
|
}
|
|
593
613
|
});
|
|
594
614
|
```
|
|
595
615
|
|
|
596
|
-
|
|
616
|
+
#### How Asset Resolution Works
|
|
617
|
+
|
|
618
|
+
1. **Manifest Match First**: If `manifestPath` is configured and an entry matches the asset key, `AssetManager` resolves to the hashed filename (from Vite or Webpack manifest) and prepends `prefix`.
|
|
619
|
+
2. **Fallback to Path & Versioning**: If no manifest is provided or the asset is not listed in the manifest, `AssetManager` uses the original path, prepends `prefix`, and appends `?v=<version>` (or `&v=<version>`) if `version` is set.
|
|
620
|
+
3. **Leading Slash Normalization**: Paths with or without leading slashes (e.g., `'css/style.css'` or `'/css/style.css'`) are handled seamlessly.
|
|
621
|
+
|
|
622
|
+
```json
|
|
623
|
+
// Example Vite manifest format (.vite/manifest.json)
|
|
624
|
+
{
|
|
625
|
+
"css/style.css": { "file": "assets/style-abc123.css", "src": "css/style.css" },
|
|
626
|
+
"js/app.js": { "file": "assets/app-xyz789.js", "src": "js/app.js" }
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Example Webpack manifest format (manifest.json)
|
|
630
|
+
{
|
|
631
|
+
"/css/style.css": "/dist/style.abc123.css",
|
|
632
|
+
"/js/app.js": "/dist/app.xyz789.js"
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
#### Template Helpers (`fsy`)
|
|
637
|
+
|
|
638
|
+
Use `fsy` helpers inside your Nunjucks layouts and pages:
|
|
597
639
|
|
|
598
640
|
```njk
|
|
599
|
-
{#
|
|
641
|
+
{# 1. URL string resolution #}
|
|
600
642
|
<link rel="stylesheet" href="{{ fsy.asset('/css/style.css') }}">
|
|
643
|
+
{# Outputs: <link rel="stylesheet" href="/css/style.css?v=1.0.0"> (or /assets/style-abc123.css) #}
|
|
601
644
|
|
|
602
|
-
{#
|
|
645
|
+
{# 2. CSS link tag helper #}
|
|
603
646
|
{{ fsy.css('/css/style.css') | safe }}
|
|
604
|
-
{
|
|
605
|
-
{{ fsy.
|
|
647
|
+
{# With custom attributes (media, id, integrity, etc.): #}
|
|
648
|
+
{{ fsy.css('/css/print.css', { media: 'print', id: 'print-styles' }) | safe }}
|
|
649
|
+
|
|
650
|
+
{# 3. JS script tag helper #}
|
|
651
|
+
{{ fsy.js('/js/app.js') | safe }}
|
|
652
|
+
{# Supports boolean flags (async, defer) and module type: #}
|
|
653
|
+
{{ fsy.js('/js/main.js', { defer: true, type: 'module' }) | safe }}
|
|
654
|
+
{# Outputs: <script src="/js/main.js?v=1.0.0" defer type="module"></script> #}
|
|
655
|
+
|
|
656
|
+
{# 4. Image tag helper #}
|
|
657
|
+
{{ fsy.img('/images/logo.png', 'Company Logo') | safe }}
|
|
658
|
+
{# With attributes (loading, class, width, height): #}
|
|
659
|
+
{{ fsy.img('/images/hero.jpg', 'Hero Banner', { loading: 'lazy', class: 'w-full rounded', width: 1200, height: 600 }) | safe }}
|
|
606
660
|
```
|
|
607
661
|
|
|
608
|
-
|
|
609
|
-
- `asset(path)` - Returns versioned/manifest-resolved URL
|
|
610
|
-
- `css(href, attrs)` - Generates `<link>` tag
|
|
611
|
-
- `js(src, attrs)` - Generates `<script>` tag
|
|
612
|
-
- `img(src, alt, attrs)` - Generates `<img>` tag
|
|
662
|
+
#### Programmatic Asset Manager API
|
|
613
663
|
|
|
614
|
-
|
|
664
|
+
You can also import and use `AssetManager` directly:
|
|
615
665
|
|
|
616
|
-
|
|
666
|
+
```javascript
|
|
667
|
+
const { AssetManager, configureAssets, getAssetManager } = require('webspresso');
|
|
617
668
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
669
|
+
// Configure global manager
|
|
670
|
+
configureAssets({
|
|
671
|
+
version: '2.0.0',
|
|
672
|
+
prefix: 'https://cdn.example.com'
|
|
673
|
+
});
|
|
624
674
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
"/css/style.css": "/dist/style.abc123.css",
|
|
628
|
-
"/js/app.js": "/dist/app.xyz789.js"
|
|
629
|
-
}
|
|
675
|
+
const manager = getAssetManager();
|
|
676
|
+
console.log(manager.asset('/css/style.css')); // -> 'https://cdn.example.com/css/style.css?v=2.0.0'
|
|
630
677
|
```
|
|
631
678
|
|
|
632
679
|
**Returns:** `{ app, nunjucksEnv, pluginManager, authMiddleware }` — `authMiddleware` is `null` when `auth` was not passed.
|
|
@@ -1197,6 +1244,82 @@ The SEO Checker panel appears as a floating widget and can be opened via:
|
|
|
1197
1244
|
- Dev toolbar "SEO Check" button
|
|
1198
1245
|
- Floating toggle button (🔍) in bottom-right corner
|
|
1199
1246
|
|
|
1247
|
+
**Basic Auth Plugin:**
|
|
1248
|
+
- Zero-dependency HTTP Basic Authentication (RFC 7617) with timing-safe password verification (`crypto.timingSafeEqual`).
|
|
1249
|
+
- Can protect the entire application globally or expose named route middleware (`middleware: ['basicAuth']`).
|
|
1250
|
+
|
|
1251
|
+
```javascript
|
|
1252
|
+
const { basicAuthPlugin } = require('webspresso/plugins');
|
|
1253
|
+
|
|
1254
|
+
const { app } = createApp({
|
|
1255
|
+
pagesDir: './pages',
|
|
1256
|
+
plugins: [
|
|
1257
|
+
basicAuthPlugin({
|
|
1258
|
+
global: false, // Set to true to protect the whole app
|
|
1259
|
+
users: {
|
|
1260
|
+
admin: 'supersecret',
|
|
1261
|
+
},
|
|
1262
|
+
realm: 'Protected Area',
|
|
1263
|
+
}),
|
|
1264
|
+
],
|
|
1265
|
+
});
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
**Realtime Layer & Plugin:**
|
|
1269
|
+
- Framework-agnostic, plugin-based, and adapter-supported realtime layer (`core/realtime` and `plugins/realtime`).
|
|
1270
|
+
- Adapters for **Generic WebSocket**, **SSE (Server-Sent Events)**, and **Socket.IO**.
|
|
1271
|
+
- Universal API for browsers and SSR safety (never initializes network connections during server-side rendering).
|
|
1272
|
+
- Built-in exponential backoff reconnection with jitter, multi-strategy authentication (token, cookie, custom), auto-recovery of subscriptions on reconnect, and token sanitization.
|
|
1273
|
+
|
|
1274
|
+
```javascript
|
|
1275
|
+
const { createApp } = require('webspresso');
|
|
1276
|
+
const { realtimePlugin, websocket } = require('webspresso/plugins');
|
|
1277
|
+
|
|
1278
|
+
// 1. App Plugin Setup
|
|
1279
|
+
const { app } = createApp({
|
|
1280
|
+
plugins: [
|
|
1281
|
+
realtimePlugin({
|
|
1282
|
+
adapter: websocket({ url: '/realtime' }),
|
|
1283
|
+
auth: { strategy: 'cookie' },
|
|
1284
|
+
autoConnect: true,
|
|
1285
|
+
}),
|
|
1286
|
+
],
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// 2. Subscription & RPC
|
|
1290
|
+
const sub = app.realtime.subscribe('project:123', { role: 'editor' }, {
|
|
1291
|
+
connected() {
|
|
1292
|
+
console.log('Connected to project:123');
|
|
1293
|
+
},
|
|
1294
|
+
received(data) {
|
|
1295
|
+
console.log('Live message:', data);
|
|
1296
|
+
},
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
sub.perform('user_typing', { userId: 42, isTyping: true });
|
|
1300
|
+
sub.send({ text: 'Hello team!' });
|
|
1301
|
+
|
|
1302
|
+
// 3. Standalone Client Usage (with Token Auth & Refresh)
|
|
1303
|
+
const { realtime, websocket } = require('webspresso');
|
|
1304
|
+
|
|
1305
|
+
const client = realtime({
|
|
1306
|
+
adapter: websocket({ url: 'wss://api.example.com/realtime' }),
|
|
1307
|
+
auth: {
|
|
1308
|
+
strategy: 'token',
|
|
1309
|
+
getToken: async () => localStorage.getItem('token'),
|
|
1310
|
+
refreshToken: async () => {
|
|
1311
|
+
const res = await fetch('/api/auth/refresh', { method: 'POST' });
|
|
1312
|
+
const { token } = await res.json();
|
|
1313
|
+
localStorage.setItem('token', token);
|
|
1314
|
+
return token;
|
|
1315
|
+
},
|
|
1316
|
+
onUnauthorized: (err) => {
|
|
1317
|
+
window.location.href = '/login';
|
|
1318
|
+
},
|
|
1319
|
+
},
|
|
1320
|
+
});
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1200
1323
|
### Creating Custom Plugins
|
|
1201
1324
|
|
|
1202
1325
|
```javascript
|
|
@@ -1537,31 +1660,87 @@ module.exports = {
|
|
|
1537
1660
|
};
|
|
1538
1661
|
```
|
|
1539
1662
|
|
|
1540
|
-
## i18n
|
|
1663
|
+
## i18n & Localization
|
|
1664
|
+
|
|
1665
|
+
Webspresso features a zero-dependency, ultra-fast internationalization engine built directly on native Node.js `Intl` APIs (`Intl.PluralRules`, `Intl.NumberFormat`, `Intl.DateTimeFormat`, `Intl.RelativeTimeFormat`).
|
|
1541
1666
|
|
|
1542
|
-
### Global Translations
|
|
1667
|
+
### 1. Global & Route-Specific Translations
|
|
1543
1668
|
|
|
1544
|
-
Add JSON files to `pages/locales
|
|
1669
|
+
Add JSON files to `pages/locales/` for global dictionary entries, or inside any route directory `pages/catalog/locales/` for route-specific overrides:
|
|
1545
1670
|
|
|
1546
1671
|
```json
|
|
1672
|
+
// pages/locales/en.json
|
|
1547
1673
|
{
|
|
1548
1674
|
"nav": {
|
|
1549
1675
|
"home": "Home",
|
|
1550
1676
|
"about": "About"
|
|
1677
|
+
},
|
|
1678
|
+
"cart": {
|
|
1679
|
+
"zero": "Your cart is empty",
|
|
1680
|
+
"one": "You have 1 item in your cart",
|
|
1681
|
+
"other": "You have {{count}} items in your cart"
|
|
1551
1682
|
}
|
|
1552
1683
|
}
|
|
1553
1684
|
```
|
|
1554
1685
|
|
|
1555
|
-
###
|
|
1686
|
+
### 2. Basic Translation & Interpolation
|
|
1556
1687
|
|
|
1557
|
-
|
|
1688
|
+
```nunjucks
|
|
1689
|
+
{# Direct function call #}
|
|
1690
|
+
<h1>{{ t('nav.home') }}</h1>
|
|
1691
|
+
|
|
1692
|
+
{# With parameter interpolation #}
|
|
1693
|
+
<p>{{ t('welcome', { name: currentUser.username }) }}</p>
|
|
1558
1694
|
|
|
1559
|
-
|
|
1695
|
+
{# With Nunjucks filter #}
|
|
1696
|
+
<h1>{{ 'nav.home' | t }}</h1>
|
|
1697
|
+
<p>{{ 'welcome' | t({ name: currentUser.username }) }}</p>
|
|
1698
|
+
```
|
|
1699
|
+
|
|
1700
|
+
### 3. Native Pluralization (`Intl.PluralRules`)
|
|
1560
1701
|
|
|
1561
|
-
|
|
1702
|
+
Pass `count` (or `n`, `cardinal`) to automatically select the correct CLDR plural category (`zero`, `one`, `two`, `few`, `many`, `other`) or exact number (`0`, `1`):
|
|
1562
1703
|
|
|
1563
1704
|
```nunjucks
|
|
1564
|
-
<
|
|
1705
|
+
<p>{{ t('cart', { count: cart.items.length }) }}</p>
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
You can also use the inline `t.plural()` helper:
|
|
1709
|
+
```nunjucks
|
|
1710
|
+
<p>{{ t.plural(items.length, { one: '1 item', other: '{{count}} items' }) }}</p>
|
|
1711
|
+
```
|
|
1712
|
+
|
|
1713
|
+
### 4. Fallback Locales Chain
|
|
1714
|
+
|
|
1715
|
+
If a translation key is missing in the requested locale (e.g. `tr`), Webspresso automatically falls back to `DEFAULT_LOCALE` (default: `en`) before returning the key name or default fallback value. Check key existence with `t.has('key')` or `t.exists('key')`.
|
|
1716
|
+
|
|
1717
|
+
### 5. Built-in `Intl` Formatters on `t`
|
|
1718
|
+
|
|
1719
|
+
Format numbers, currencies, dates, and relative times according to the active locale without external date/number libraries:
|
|
1720
|
+
|
|
1721
|
+
```nunjucks
|
|
1722
|
+
{# Numbers #}
|
|
1723
|
+
<span>{{ t.number(1234567.89) }}</span> {# TR: 1.234.567,89 | EN: 1,234,567.89 #}
|
|
1724
|
+
|
|
1725
|
+
{# Currencies #}
|
|
1726
|
+
<span>{{ t.currency(199.99, 'TRY') }}</span> {# ₺199,99 #}
|
|
1727
|
+
<span>{{ t.currency(49.99, 'USD') }}</span> {# $49.99 #}
|
|
1728
|
+
|
|
1729
|
+
{# Dates #}
|
|
1730
|
+
<span>{{ t.date(post.created_at, { dateStyle: 'long' }) }}</span>
|
|
1731
|
+
|
|
1732
|
+
{# Relative Time #}
|
|
1733
|
+
<span>{{ t.relativeTime(-2, 'day') }}</span> {# "2 days ago" or "2 gün önce" #}
|
|
1734
|
+
```
|
|
1735
|
+
|
|
1736
|
+
### 6. Language Switcher Helpers
|
|
1737
|
+
|
|
1738
|
+
Use `fsy.localeUrl(targetLocale)` to generate a URL with the current page path and query parameters preserved:
|
|
1739
|
+
|
|
1740
|
+
```nunjucks
|
|
1741
|
+
<a href="{{ fsy.localeUrl('en') }}">English</a>
|
|
1742
|
+
<a href="{{ fsy.localeUrl('tr') }}">Türkçe</a>
|
|
1743
|
+
<a href="{{ fsy.localeUrl('de') }}">Deutsch</a>
|
|
1565
1744
|
```
|
|
1566
1745
|
|
|
1567
1746
|
## Template Helpers
|
|
@@ -1635,6 +1814,224 @@ module.exports = {
|
|
|
1635
1814
|
10. Nunjucks render
|
|
1636
1815
|
11. Route `afterRender`
|
|
1637
1816
|
|
|
1817
|
+
## Services Layer
|
|
1818
|
+
|
|
1819
|
+
Webspresso includes a lightweight, framework-agnostic **Services Layer** to organize reusable business logic, query flows, and multi-step mutations under `services/`.
|
|
1820
|
+
|
|
1821
|
+
Services are automatically discovered from the filesystem and can be invoked from SSR data loaders (`ctx.service()`), API routes (`req.service()`), background jobs, cron, CLI tasks, or other services.
|
|
1822
|
+
|
|
1823
|
+
### 1. File Structure & Service Names
|
|
1824
|
+
|
|
1825
|
+
Files inside `services/` map directly to dot-separated service names:
|
|
1826
|
+
|
|
1827
|
+
```text
|
|
1828
|
+
services/
|
|
1829
|
+
user/
|
|
1830
|
+
get.js → user.get
|
|
1831
|
+
create.js → user.create
|
|
1832
|
+
profile/
|
|
1833
|
+
get.js → user.profile.get
|
|
1834
|
+
post/
|
|
1835
|
+
publish.js → post.publish
|
|
1836
|
+
listByUser.js → post.listByUser
|
|
1837
|
+
payment/
|
|
1838
|
+
refund.js → payment.refund
|
|
1839
|
+
```
|
|
1840
|
+
|
|
1841
|
+
### 2. Service Definition
|
|
1842
|
+
|
|
1843
|
+
Services export a handler and an optional input validation schema:
|
|
1844
|
+
|
|
1845
|
+
```javascript
|
|
1846
|
+
// services/user/get.js
|
|
1847
|
+
module.exports = {
|
|
1848
|
+
schema: {
|
|
1849
|
+
id: 'number',
|
|
1850
|
+
},
|
|
1851
|
+
|
|
1852
|
+
async handler({ id }, ctx) {
|
|
1853
|
+
return ctx.db.users.findById(id);
|
|
1854
|
+
},
|
|
1855
|
+
};
|
|
1856
|
+
```
|
|
1857
|
+
|
|
1858
|
+
You can also use the `defineService` helper:
|
|
1859
|
+
|
|
1860
|
+
```javascript
|
|
1861
|
+
// services/payment/refund.js
|
|
1862
|
+
const { defineService } = require('webspresso');
|
|
1863
|
+
|
|
1864
|
+
module.exports = defineService({
|
|
1865
|
+
// Zod functional schema syntax ({ z }) with built-in nanoid support
|
|
1866
|
+
schema: ({ z }) => z.object({
|
|
1867
|
+
transactionId: z.string().min(3),
|
|
1868
|
+
amount: z.number().positive(),
|
|
1869
|
+
reason: z.string().optional(),
|
|
1870
|
+
}),
|
|
1871
|
+
|
|
1872
|
+
async handler({ transactionId, amount, reason }, ctx) {
|
|
1873
|
+
// Process refund logic...
|
|
1874
|
+
return { refundId: 'ref_123', transactionId, amount, status: 'succeeded' };
|
|
1875
|
+
},
|
|
1876
|
+
});
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
### 3. Service-to-Service Composition
|
|
1880
|
+
|
|
1881
|
+
Services can seamlessly call other services via `ctx.service()` while sharing the exact same request context (`ctx.db`, `ctx.auth`, `ctx.session`, `ctx.user`):
|
|
1882
|
+
|
|
1883
|
+
```javascript
|
|
1884
|
+
// services/post/publish.js
|
|
1885
|
+
module.exports = {
|
|
1886
|
+
schema: ({ z }) => z.object({
|
|
1887
|
+
title: z.string(),
|
|
1888
|
+
authorId: z.number(),
|
|
1889
|
+
}),
|
|
1890
|
+
|
|
1891
|
+
async handler({ title, authorId }, ctx) {
|
|
1892
|
+
const author = await ctx.service('user.get', { id: authorId });
|
|
1893
|
+
|
|
1894
|
+
return ctx.db.posts.create({
|
|
1895
|
+
title,
|
|
1896
|
+
author_id: author.id,
|
|
1897
|
+
published_at: new Date(),
|
|
1898
|
+
});
|
|
1899
|
+
},
|
|
1900
|
+
};
|
|
1901
|
+
```
|
|
1902
|
+
|
|
1903
|
+
> **Circular Call Protection**: Webspresso automatically tracks service call stacks and detects recursive/circular dependencies (e.g. `user.get -> profile.get -> user.get`), throwing an informative `Circular service call detected` error.
|
|
1904
|
+
|
|
1905
|
+
### 4. Invoking from SSR & API Routes
|
|
1906
|
+
|
|
1907
|
+
In SSR page data loaders (`pages/users/[id].njk`):
|
|
1908
|
+
|
|
1909
|
+
```javascript
|
|
1910
|
+
// pages/users/[id].js
|
|
1911
|
+
module.exports = {
|
|
1912
|
+
async load(req, ctx) {
|
|
1913
|
+
const user = await ctx.service('user.get', { id: Number(req.params.id) });
|
|
1914
|
+
return { user };
|
|
1915
|
+
},
|
|
1916
|
+
};
|
|
1917
|
+
```
|
|
1918
|
+
|
|
1919
|
+
In API routes or Express middleware:
|
|
1920
|
+
|
|
1921
|
+
```javascript
|
|
1922
|
+
// pages/api/users.post.js
|
|
1923
|
+
module.exports = async function(req, res) {
|
|
1924
|
+
const user = await req.service('user.create', req.body);
|
|
1925
|
+
res.status(201).json(user);
|
|
1926
|
+
};
|
|
1927
|
+
```
|
|
1928
|
+
|
|
1929
|
+
### 5. Service Caching & Memoization (`cache: '5m'`)
|
|
1930
|
+
|
|
1931
|
+
Services can be cached in-memory with automatic TTL expiration and deterministic key serialization:
|
|
1932
|
+
|
|
1933
|
+
```javascript
|
|
1934
|
+
// services/product/stats.js
|
|
1935
|
+
module.exports = {
|
|
1936
|
+
// Cache result for 5 minutes based on input arguments
|
|
1937
|
+
cache: '5m', // supports '30s', '5m', '1h', '1d', ms numbers, or { ttl: '5m', key: (input) => input.id }
|
|
1938
|
+
|
|
1939
|
+
async handler({ productId }, ctx) {
|
|
1940
|
+
return ctx.db.stats.getProductStats(productId);
|
|
1941
|
+
},
|
|
1942
|
+
};
|
|
1943
|
+
```
|
|
1944
|
+
|
|
1945
|
+
You can invalidate cache keys explicitly on mutations:
|
|
1946
|
+
|
|
1947
|
+
```javascript
|
|
1948
|
+
// From any service or route:
|
|
1949
|
+
ctx.service.invalidate('product.stats', { productId: 42 });
|
|
1950
|
+
|
|
1951
|
+
// Or clear all cached entries for a service:
|
|
1952
|
+
ctx.service.clearCache('product.stats');
|
|
1953
|
+
```
|
|
1954
|
+
|
|
1955
|
+
You can also use `memoize` as a standalone utility for any async function:
|
|
1956
|
+
|
|
1957
|
+
```javascript
|
|
1958
|
+
const { memoize } = require('webspresso');
|
|
1959
|
+
|
|
1960
|
+
const fetchRemoteRates = memoize(async () => {
|
|
1961
|
+
return fetchExchangeRates();
|
|
1962
|
+
}, { ttl: '10m', maxSize: 100 });
|
|
1963
|
+
```
|
|
1964
|
+
|
|
1965
|
+
### 6. Timeouts & Database Transactions (`timeout`, `transaction`)
|
|
1966
|
+
|
|
1967
|
+
You can protect services against hanging handlers and ensure automatic ACID transaction rollbacks:
|
|
1968
|
+
|
|
1969
|
+
```javascript
|
|
1970
|
+
// services/order/checkout.js
|
|
1971
|
+
module.exports = {
|
|
1972
|
+
// Abort with 504 Gateway Timeout if execution exceeds 5 seconds
|
|
1973
|
+
timeout: '5s',
|
|
1974
|
+
|
|
1975
|
+
// Wrap execution in an automatic Knex transaction (rolls back on any error)
|
|
1976
|
+
transaction: true,
|
|
1977
|
+
|
|
1978
|
+
async handler({ orderId, items }, ctx) {
|
|
1979
|
+
// ctx.trx and ctx.db are automatically bound to the active transaction
|
|
1980
|
+
await ctx.db('orders').where({ id: orderId }).update({ status: 'paid' });
|
|
1981
|
+
await ctx.service('inventory.deduct', { items }); // Nested services reuse the exact same transaction!
|
|
1982
|
+
|
|
1983
|
+
return { orderId, success: true };
|
|
1984
|
+
},
|
|
1985
|
+
};
|
|
1986
|
+
```
|
|
1987
|
+
|
|
1988
|
+
### 7. Authorization & Role Guards (`auth`)
|
|
1989
|
+
|
|
1990
|
+
Services can enforce authentication and role-based access control (RBAC) automatically:
|
|
1991
|
+
|
|
1992
|
+
```javascript
|
|
1993
|
+
// services/admin/billing.js
|
|
1994
|
+
module.exports = {
|
|
1995
|
+
// Requires authenticated user with 'admin' or 'finance' role
|
|
1996
|
+
auth: ['admin', 'finance'], // or auth: true (any logged in user) or auth: (user, ctx) => boolean
|
|
1997
|
+
|
|
1998
|
+
async handler(input, ctx) {
|
|
1999
|
+
// Guaranteed to have authorized ctx.user
|
|
2000
|
+
return ctx.db('billing_records').select();
|
|
2001
|
+
},
|
|
2002
|
+
};
|
|
2003
|
+
```
|
|
2004
|
+
|
|
2005
|
+
Throws `UnauthorizedError` (401) when unauthenticated or `ForbiddenError` (403) when role permissions are insufficient.
|
|
2006
|
+
|
|
2007
|
+
### 8. Interactive CLI Runner (`webspresso service`)
|
|
2008
|
+
|
|
2009
|
+
You can test and run any service directly from the command line without starting the HTTP server:
|
|
2010
|
+
|
|
2011
|
+
```bash
|
|
2012
|
+
# Interactive selection & prompt mode
|
|
2013
|
+
npx webspresso service
|
|
2014
|
+
|
|
2015
|
+
# Run a specific service with JSON payload
|
|
2016
|
+
npx webspresso service user.get -i '{"id": 42}'
|
|
2017
|
+
|
|
2018
|
+
# Run with key=value query string
|
|
2019
|
+
npx webspresso service payment.refund -i 'transactionId=tx_123 amount=49.99'
|
|
2020
|
+
```
|
|
2021
|
+
|
|
2022
|
+
### 7. Programmatic Usage & Testing
|
|
2023
|
+
|
|
2024
|
+
You can execute services directly in unit tests or background scripts:
|
|
2025
|
+
|
|
2026
|
+
```javascript
|
|
2027
|
+
const { createServiceRegistry } = require('webspresso');
|
|
2028
|
+
|
|
2029
|
+
const services = createServiceRegistry({ servicesDir: './services' });
|
|
2030
|
+
const user = await services.call('user.get', { id: 42 }, { db: mockDb });
|
|
2031
|
+
```
|
|
2032
|
+
|
|
2033
|
+
---
|
|
2034
|
+
|
|
1638
2035
|
## Environment Variables
|
|
1639
2036
|
|
|
1640
2037
|
| Variable | Default | Description |
|