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.
Files changed (102) hide show
  1. package/README.md +450 -53
  2. package/bin/commands/service.js +193 -0
  3. package/bin/webspresso.js +2 -0
  4. package/core/auth/manager.js +36 -26
  5. package/core/auth/middleware.js +21 -2
  6. package/core/compression/compressible.js +51 -0
  7. package/core/compression/compression-stream.js +59 -0
  8. package/core/compression/index.js +25 -0
  9. package/core/compression/middleware.js +301 -0
  10. package/core/compression/negotiate.js +153 -0
  11. package/core/errors/base.js +31 -0
  12. package/core/errors/domain.js +193 -0
  13. package/core/errors/http.js +133 -0
  14. package/core/errors/index.js +77 -0
  15. package/core/errors/middleware.js +162 -0
  16. package/core/errors/normalize.js +171 -0
  17. package/core/orm/complexity.js +120 -0
  18. package/core/orm/events.js +64 -14
  19. package/core/orm/index.js +45 -10
  20. package/core/orm/model.js +10 -2
  21. package/core/orm/query-builder.js +30 -4
  22. package/core/orm/repository.js +57 -25
  23. package/core/orm/scopes.js +2 -1
  24. package/core/orm/transaction.js +39 -5
  25. package/core/orm/utils.js +6 -3
  26. package/core/queue/adapters/database.js +284 -0
  27. package/core/queue/adapters/memory.js +155 -0
  28. package/core/queue/adapters/redis.js +336 -0
  29. package/core/queue/index.js +21 -0
  30. package/core/queue/job.js +103 -0
  31. package/core/queue/manager.js +387 -0
  32. package/core/realtime/adapters/redis.js +431 -0
  33. package/core/realtime/adapters/socket-io.js +203 -0
  34. package/core/realtime/adapters/sse.js +210 -0
  35. package/core/realtime/adapters/websocket.js +337 -0
  36. package/core/realtime/auth-manager.js +140 -0
  37. package/core/realtime/core.js +359 -0
  38. package/core/realtime/emitter.js +98 -0
  39. package/core/realtime/errors.js +71 -0
  40. package/core/realtime/index.js +79 -0
  41. package/core/realtime/reconnect-manager.js +118 -0
  42. package/core/realtime/registry.js +243 -0
  43. package/core/shutdown/http-adapter.js +134 -0
  44. package/core/shutdown/index.js +12 -0
  45. package/core/shutdown/shutdown-manager.js +312 -0
  46. package/core/ssr/index.js +13 -0
  47. package/core/ssr/stream.js +174 -0
  48. package/index.d.ts +829 -52
  49. package/index.js +103 -0
  50. package/package.json +22 -2
  51. package/plugins/admin-panel/api.js +37 -4
  52. package/plugins/admin-panel/app.js +67 -17
  53. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
  54. package/plugins/admin-panel/client/parts/03-pagination-intro.js +25 -18
  55. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  56. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
  57. package/plugins/admin-panel/client/parts/08-record-list.js +27 -34
  58. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  59. package/plugins/admin-panel/core/admin-module.js +2 -2
  60. package/plugins/admin-panel/core/api-extensions.js +149 -0
  61. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  62. package/plugins/admin-panel/index.js +131 -18
  63. package/plugins/admin-panel/modules/admin-users.js +770 -0
  64. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  65. package/plugins/admin-panel/modules/menu.js +85 -0
  66. package/plugins/admin-panel/modules/user-management.js +5 -2
  67. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  68. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  69. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  70. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  71. package/plugins/basic-auth/index.js +293 -0
  72. package/plugins/content/client/inline-edit.js +41 -17
  73. package/plugins/cors.js +12 -8
  74. package/plugins/csrf/index.js +8 -8
  75. package/plugins/data-exchange/export-xlsx.js +5 -1
  76. package/plugins/data-exchange/import.js +16 -2
  77. package/plugins/data-exchange/index.js +14 -0
  78. package/plugins/email/index.js +14 -1
  79. package/plugins/index.js +29 -0
  80. package/plugins/queue/index.js +108 -0
  81. package/plugins/realtime/index.js +150 -0
  82. package/plugins/recaptcha/index.js +11 -0
  83. package/plugins/redirect/index.js +9 -1
  84. package/plugins/rest-resources/index.js +280 -11
  85. package/plugins/upload/index.js +16 -0
  86. package/src/app-context.js +46 -6
  87. package/src/file-router.js +220 -28
  88. package/src/helpers.js +24 -5
  89. package/src/plugin-manager.js +134 -7
  90. package/src/server.js +470 -44
  91. package/src/services/builtins/auth.js +273 -0
  92. package/src/services/builtins/exchange.js +229 -0
  93. package/src/services/builtins/mail.js +134 -0
  94. package/src/services/builtins/media.js +196 -0
  95. package/src/services/builtins/system.js +221 -0
  96. package/src/services/discovery.js +140 -0
  97. package/src/services/executor.js +292 -0
  98. package/src/services/index.js +70 -0
  99. package/src/services/memoize.js +256 -0
  100. package/src/services/registry.js +288 -0
  101. package/src/services/validator.js +281 -0
  102. 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
- // Option 1: Custom handler function
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
- // Option 2: Template path (rendered with Nunjucks)
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 error page (503)
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 templates receive these variables:
546
- - `404.njk`: `{ url, method }`
547
- - `500.njk`: `{ error, status, isDev }`
548
- - `503.njk`: `{ url, method, isDev }`
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 with versioning and manifest support:
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
- // Option 1: Simple versioning (cache busting)
585
- version: '1.2.3', // or process.env.APP_VERSION
586
-
587
- // Option 2: Manifest file (Vite, Webpack, etc.)
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 prefix for assets
591
- prefix: '/static'
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
- Use asset helpers in templates:
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
- {# Using fsy helpers (auto-resolved) #}
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
- {# Or generate full HTML tags #}
645
+ {# 2. CSS link tag helper #}
603
646
  {{ fsy.css('/css/style.css') | safe }}
604
- {{ fsy.js('/js/app.js', { defer: true, type: 'module' }) | safe }}
605
- {{ fsy.img('/images/logo.png', 'Site Logo', { class: 'logo', loading: 'lazy' }) | safe }}
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
- Asset helpers available in `fsy`:
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
- **Manifest Support:**
664
+ You can also import and use `AssetManager` directly:
615
665
 
616
- Works with Vite and Webpack manifest formats:
666
+ ```javascript
667
+ const { AssetManager, configureAssets, getAssetManager } = require('webspresso');
617
668
 
618
- ```json
619
- // Vite manifest format (.vite/manifest.json)
620
- {
621
- "css/style.css": { "file": "assets/style-abc123.css" },
622
- "js/app.js": { "file": "assets/app-xyz789.js" }
623
- }
669
+ // Configure global manager
670
+ configureAssets({
671
+ version: '2.0.0',
672
+ prefix: 'https://cdn.example.com'
673
+ });
624
674
 
625
- // Webpack manifest format
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
- ### Route-Specific Translations
1686
+ ### 2. Basic Translation & Interpolation
1556
1687
 
1557
- Add a `locales/` folder inside any route directory to override global translations.
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
- ### Using Translations
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
- In templates:
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
- <h1>{{ t('nav.home') }}</h1>
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 |