odac 1.4.10 โ 1.4.12
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/CHANGELOG.md +31 -0
- package/bin/odac.js +230 -1
- package/docs/ai/skills/SKILL.md +3 -1
- package/docs/ai/skills/backend/config.md +21 -1
- package/docs/ai/skills/backend/database.md +40 -0
- package/docs/ai/skills/backend/request_response.md +21 -1
- package/docs/ai/skills/frontend/scripts.md +36 -0
- package/docs/backend/03-config/00-configuration-overview.md +25 -6
- package/docs/backend/03-config/01-database-connection.md +41 -4
- package/docs/backend/03-config/04-environment-variables.md +3 -2
- package/docs/backend/06-request-and-response/03-proxy-cache.md +77 -0
- package/docs/backend/07-views/12-scripts-and-typescript.md +156 -0
- package/docs/backend/08-database/01-getting-started.md +11 -0
- package/docs/backend/10-authentication/04-odac-register-forms.md +7 -4
- package/docs/backend/10-authentication/06-odac-login-forms.md +7 -4
- package/docs/index.json +8 -0
- package/eslint.config.mjs +20 -1
- package/package.json +2 -1
- package/src/Config.js +7 -0
- package/src/Database/Migration.js +32 -11
- package/src/Mail.js +61 -0
- package/src/Odac.js +3 -0
- package/src/Request.js +21 -0
- package/src/Route/Cron.js +10 -0
- package/src/Route.js +1 -0
- package/template/public/.gitkeep +0 -0
- package/template/{public/assets โ view}/js/app.js +8 -2
- package/test/Odac/cache.test.js +54 -0
- package/test/Request/cache.test.js +95 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
## ๐ Proxy Cache
|
|
2
|
+
|
|
3
|
+
`Odac.cache()` lets you tell the ODAC Proxy to cache the current page's HTML response, so repeat visitors get a near-instant response without hitting your application server at all.
|
|
4
|
+
|
|
5
|
+
> **ODAC Ecosystem Only:** This feature works exclusively within the ODAC ecosystem. It relies on the `X-ODAC-Cache` header that only the ODAC Proxy understands and acts upon.
|
|
6
|
+
|
|
7
|
+
### Basic Usage
|
|
8
|
+
|
|
9
|
+
Call `Odac.cache(seconds)` at the top of your controller with a TTL (time-to-live) in seconds:
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
module.exports = function (Odac) {
|
|
13
|
+
Odac.cache(3600) // Cache this page for 1 hour
|
|
14
|
+
|
|
15
|
+
Odac.set('title', 'About Us')
|
|
16
|
+
Odac.View.skeleton('main').set('content', 'about')
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That's it. The ODAC Proxy handles the rest.
|
|
21
|
+
|
|
22
|
+
### How It Works
|
|
23
|
+
|
|
24
|
+
When `Odac.cache(seconds)` is called, ODAC sets two response headers:
|
|
25
|
+
|
|
26
|
+
| Header | Value | Purpose |
|
|
27
|
+
|--------|-------|---------|
|
|
28
|
+
| `X-ODAC-Cache` | `3600` | Tells the ODAC Proxy to cache this response for the given TTL |
|
|
29
|
+
| `Cache-Control` | `public, max-age=3600` | Standard browser/CDN cache directive |
|
|
30
|
+
|
|
31
|
+
The ODAC Proxy intercepts the response, stores it, and serves it directly on subsequent requests โ bypassing your application entirely until the TTL expires.
|
|
32
|
+
|
|
33
|
+
### Smart Cache Invalidation
|
|
34
|
+
|
|
35
|
+
The ODAC Proxy is intelligent about cache invalidation. You don't need to manually clear the cache in most cases:
|
|
36
|
+
|
|
37
|
+
- **Content changes:** If the underlying page content changes (e.g. a file is updated or a deployment happens), the Proxy detects this and automatically invalidates the cache on the next request.
|
|
38
|
+
- **Dynamic content detection:** If the Proxy detects that a response contains dynamic or user-specific content, it cancels the cache for that response automatically.
|
|
39
|
+
|
|
40
|
+
### When to Use It
|
|
41
|
+
|
|
42
|
+
`Odac.cache()` is designed for pages where the HTML output is **identical for all visitors**:
|
|
43
|
+
|
|
44
|
+
โ
**Good candidates:**
|
|
45
|
+
- Marketing and landing pages
|
|
46
|
+
- Blog posts and articles
|
|
47
|
+
- Documentation pages
|
|
48
|
+
- Product listing pages (without personalization)
|
|
49
|
+
- Static "About", "Contact", "FAQ" pages
|
|
50
|
+
|
|
51
|
+
โ **Do not use on:**
|
|
52
|
+
- Pages with user-specific content (dashboards, profiles, account pages)
|
|
53
|
+
- Pages that display session data or authentication state
|
|
54
|
+
- Pages with per-user pricing, recommendations, or notifications
|
|
55
|
+
- Any page where the HTML output differs between users
|
|
56
|
+
|
|
57
|
+
> Even though the ODAC Proxy can detect dynamic content and cancel caching, you should not rely on this as a safety net. If a page is user-specific, simply don't call `Odac.cache()`.
|
|
58
|
+
|
|
59
|
+
### TTL Reference
|
|
60
|
+
|
|
61
|
+
| Scenario | Recommended TTL |
|
|
62
|
+
|----------|----------------|
|
|
63
|
+
| Frequently updated content (news, blog) | `300` โ `900` (5โ15 min) |
|
|
64
|
+
| Semi-static content (docs, product pages) | `3600` โ `86400` (1โ24 hrs) |
|
|
65
|
+
| Fully static pages (about, landing) | `86400` โ `604800` (1โ7 days) |
|
|
66
|
+
|
|
67
|
+
### Error Handling
|
|
68
|
+
|
|
69
|
+
`Odac.cache()` throws a `TypeError` if the argument is not a positive integer:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
Odac.cache(3600) // โ
Valid
|
|
73
|
+
Odac.cache(0) // โ TypeError
|
|
74
|
+
Odac.cache(-1) // โ TypeError
|
|
75
|
+
Odac.cache('3600') // โ TypeError
|
|
76
|
+
Odac.cache(3.5) // โ TypeError
|
|
77
|
+
```
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# ๐ฆ Scripts & TypeScript
|
|
2
|
+
|
|
3
|
+
ODAC comes with built-in, **Zero-Config** support for frontend JavaScript and TypeScript. Write your scripts in `view/js/`, and ODAC handles transpilation, bundling, minification, and tree-shaking automatically โ just like the Tailwind CSS pipeline.
|
|
4
|
+
|
|
5
|
+
## How it Works
|
|
6
|
+
|
|
7
|
+
The framework uses [esbuild](https://esbuild.github.io/) under the hood for blazing-fast builds:
|
|
8
|
+
|
|
9
|
+
1. **Development (`npm run dev`)**:
|
|
10
|
+
* ODAC watches all `.ts`, `.js`, `.mts`, and `.mjs` files in `view/js/`.
|
|
11
|
+
* Changes trigger instant rebuilds (sub-millisecond).
|
|
12
|
+
* Source maps are enabled for easy debugging.
|
|
13
|
+
|
|
14
|
+
2. **Production (`npm run build`)**:
|
|
15
|
+
* All entry points are bundled, minified, and tree-shaken.
|
|
16
|
+
* Output goes to `public/assets/js/{name}.js`.
|
|
17
|
+
|
|
18
|
+
3. **Serving (`npm start`)**:
|
|
19
|
+
* The compiled JS files are served statically. No runtime overhead.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
Create a file at **`view/js/app.ts`** (or `app.js` for plain JavaScript):
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// view/js/app.ts
|
|
27
|
+
interface User {
|
|
28
|
+
id: number
|
|
29
|
+
name: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const greet = (user: User): string => {
|
|
33
|
+
return `Hello, ${user.name}!`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
37
|
+
console.log(greet({ id: 1, name: 'World' }))
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it. Run `npm run dev` and ODAC compiles it to `public/assets/js/app.js`.
|
|
42
|
+
|
|
43
|
+
## Entry Points & Imports
|
|
44
|
+
|
|
45
|
+
Each file in `view/js/` becomes a separate entry point (bundle). You can use standard ES module imports between files:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
view/js/
|
|
49
|
+
โโโ app.ts โ public/assets/js/app.js
|
|
50
|
+
โโโ admin.ts โ public/assets/js/admin.js
|
|
51
|
+
โโโ _utils.ts (ignored โ partial/import only)
|
|
52
|
+
โโโ _api.ts (ignored โ partial/import only)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> **Convention:** Files starting with `_` (underscore) are **not** compiled as entry points. Use them as shared modules that get imported by your entry points.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// view/js/_api.ts (shared module โ not compiled on its own)
|
|
59
|
+
export const fetchUsers = async (): Promise<unknown> => {
|
|
60
|
+
const res = await fetch('/api/users')
|
|
61
|
+
return res.json()
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// view/js/admin.ts (entry point โ compiled to admin.js)
|
|
67
|
+
import { fetchUsers } from './_api'
|
|
68
|
+
|
|
69
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
70
|
+
const users = await fetchUsers()
|
|
71
|
+
console.log(users)
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
esbuild bundles the imported code into the final output โ no extra network requests.
|
|
76
|
+
|
|
77
|
+
## TypeScript or JavaScript โ Your Choice
|
|
78
|
+
|
|
79
|
+
ODAC doesn't force TypeScript on you. Both work equally well:
|
|
80
|
+
|
|
81
|
+
| Extension | Behavior |
|
|
82
|
+
|-----------|----------|
|
|
83
|
+
| `.ts` | TypeScript with full type-checking support |
|
|
84
|
+
| `.js` | Plain JavaScript, passed through as-is |
|
|
85
|
+
| `.mts` | TypeScript with ES module syntax |
|
|
86
|
+
| `.mjs` | JavaScript with ES module syntax |
|
|
87
|
+
|
|
88
|
+
## HTML Integration
|
|
89
|
+
|
|
90
|
+
In your skeleton or layout files, reference the compiled output:
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<script src="/assets/js/app.js"></script>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The default project template already includes this in `skeleton/main.html`.
|
|
97
|
+
|
|
98
|
+
## Configuration (Optional)
|
|
99
|
+
|
|
100
|
+
ODAC works with zero configuration, but you can customize the JS pipeline in `odac.json`:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"js": {
|
|
105
|
+
"target": "es2020",
|
|
106
|
+
"minify": true,
|
|
107
|
+
"sourcemap": false,
|
|
108
|
+
"bundle": true,
|
|
109
|
+
"obfuscate": false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Option | Default | Description |
|
|
115
|
+
|-------------|------------|-------------|
|
|
116
|
+
| `target` | `"es2020"` | JavaScript target version (`es2015`, `es2020`, `esnext`, etc.) |
|
|
117
|
+
| `minify` | `true` | Enable minification in production builds |
|
|
118
|
+
| `sourcemap` | `false` | Generate source maps in production (always enabled in dev) |
|
|
119
|
+
| `bundle` | `true` | Bundle imported modules into a single file |
|
|
120
|
+
| `obfuscate` | `false` | Code obfuscation level (`false`, `true`/`"low"`, `"medium"`, `"high"`) |
|
|
121
|
+
|
|
122
|
+
## Obfuscation
|
|
123
|
+
|
|
124
|
+
ODAC supports three levels of code obfuscation for production builds. Obfuscation is disabled by default and only applied during `odac build` โ development mode is never obfuscated.
|
|
125
|
+
|
|
126
|
+
### Levels
|
|
127
|
+
|
|
128
|
+
| Level | What it does |
|
|
129
|
+
|----------|-------------|
|
|
130
|
+
| `false` | No obfuscation. Standard minification only. |
|
|
131
|
+
| `true` / `"low"` | Mangles properties starting with `_` (private-by-convention). |
|
|
132
|
+
| `"medium"` | Low + drops `debugger` statements + removes `console.debug` and `console.trace`. |
|
|
133
|
+
| `"high"` | Maximum โ mangles `_` and `$` prefixed properties, drops all `console.*` calls and `debugger` statements. |
|
|
134
|
+
|
|
135
|
+
### Example
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"js": {
|
|
140
|
+
"obfuscate": "medium"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
> **Tip:** Start with `"low"` or `"medium"`. The `"high"` level mangles `$`-prefixed properties which may break code that interacts with external libraries using `$` conventions (e.g., jQuery, some frameworks). Test thoroughly before deploying with `"high"`.
|
|
146
|
+
|
|
147
|
+
## What You Get
|
|
148
|
+
|
|
149
|
+
- **TypeScript Support** โ Write type-safe frontend code without any setup
|
|
150
|
+
- **Bundling** โ `import`/`export` between files, everything merged into one output
|
|
151
|
+
- **Minification** โ Whitespace removal, variable shortening, dead code elimination
|
|
152
|
+
- **Tree-Shaking** โ Unused exports are automatically removed
|
|
153
|
+
- **Obfuscation** โ Optional property mangling and console stripping (3 levels)
|
|
154
|
+
- **Source Maps** โ Enabled in development for easy debugging
|
|
155
|
+
- **Multiple Entry Points** โ Separate bundles for different pages/sections
|
|
156
|
+
- **Sub-millisecond Rebuilds** โ esbuild's native speed keeps your dev loop instant
|
|
@@ -66,6 +66,17 @@ You can configure multiple database connections. The connection named `default`
|
|
|
66
66
|
}
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
To use a named connection in your code, simply access it through `Odac.DB`:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
// Primary database (default)
|
|
73
|
+
const users = await Odac.DB.users.where('active', true)
|
|
74
|
+
|
|
75
|
+
// Analytics database
|
|
76
|
+
const logs = await Odac.DB.analytics.events.insert({ type: 'login' })
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
|
|
69
80
|
---
|
|
70
81
|
|
|
71
82
|
## Environment Variables
|
|
@@ -8,7 +8,8 @@ The `<odac:register>` component provides a zero-configuration way to create secu
|
|
|
8
8
|
|
|
9
9
|
```json
|
|
10
10
|
{
|
|
11
|
-
"
|
|
11
|
+
"database": {
|
|
12
|
+
"type": "mysql",
|
|
12
13
|
"host": "localhost",
|
|
13
14
|
"user": "root",
|
|
14
15
|
"password": "",
|
|
@@ -54,7 +55,8 @@ If you want to customize table names or primary key:
|
|
|
54
55
|
|
|
55
56
|
```json
|
|
56
57
|
{
|
|
57
|
-
"
|
|
58
|
+
"database": {
|
|
59
|
+
"type": "mysql",
|
|
58
60
|
"host": "localhost",
|
|
59
61
|
"user": "root",
|
|
60
62
|
"password": "",
|
|
@@ -468,11 +470,12 @@ This provides instant feedback to users before form submission.
|
|
|
468
470
|
|
|
469
471
|
### Required Configuration
|
|
470
472
|
|
|
471
|
-
Only
|
|
473
|
+
Only database configuration is required:
|
|
472
474
|
|
|
473
475
|
```json
|
|
474
476
|
{
|
|
475
|
-
"
|
|
477
|
+
"database": {
|
|
478
|
+
"type": "mysql",
|
|
476
479
|
"host": "localhost",
|
|
477
480
|
"user": "root",
|
|
478
481
|
"password": "",
|
|
@@ -8,7 +8,8 @@ The `<odac:login>` component provides a zero-configuration way to create secure
|
|
|
8
8
|
|
|
9
9
|
```json
|
|
10
10
|
{
|
|
11
|
-
"
|
|
11
|
+
"database": {
|
|
12
|
+
"type": "mysql",
|
|
12
13
|
"host": "localhost",
|
|
13
14
|
"user": "root",
|
|
14
15
|
"password": "",
|
|
@@ -47,7 +48,8 @@ If you want to customize table names or primary key:
|
|
|
47
48
|
|
|
48
49
|
```json
|
|
49
50
|
{
|
|
50
|
-
"
|
|
51
|
+
"database": {
|
|
52
|
+
"type": "mysql",
|
|
51
53
|
"host": "localhost",
|
|
52
54
|
"user": "root",
|
|
53
55
|
"password": "",
|
|
@@ -428,11 +430,12 @@ input._odac_error {
|
|
|
428
430
|
|
|
429
431
|
### Required Configuration
|
|
430
432
|
|
|
431
|
-
Only
|
|
433
|
+
Only database configuration is required:
|
|
432
434
|
|
|
433
435
|
```json
|
|
434
436
|
{
|
|
435
|
-
"
|
|
437
|
+
"database": {
|
|
438
|
+
"type": "mysql",
|
|
436
439
|
"host": "localhost",
|
|
437
440
|
"user": "root",
|
|
438
441
|
"password": "",
|
package/docs/index.json
CHANGED
|
@@ -165,6 +165,10 @@
|
|
|
165
165
|
{
|
|
166
166
|
"file": "02-sending-a-response-replying-to-the-user.md",
|
|
167
167
|
"title": "Response Object"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"file": "03-proxy-cache.md",
|
|
171
|
+
"title": "Proxy Cache"
|
|
168
172
|
}
|
|
169
173
|
]
|
|
170
174
|
},
|
|
@@ -215,6 +219,10 @@
|
|
|
215
219
|
{
|
|
216
220
|
"file": "10-styling-and-tailwind.md",
|
|
217
221
|
"title": "Styling & Tailwind CSS"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"file": "12-scripts-and-typescript.md",
|
|
225
|
+
"title": "Scripts & TypeScript"
|
|
218
226
|
}
|
|
219
227
|
]
|
|
220
228
|
},
|
package/eslint.config.mjs
CHANGED
|
@@ -38,7 +38,7 @@ export default defineConfig([
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
files: ['template/**/*.js'],
|
|
41
|
-
ignores: ['template/public/**/*.js'],
|
|
41
|
+
ignores: ['template/public/**/*.js', 'template/view/js/**/*.js'],
|
|
42
42
|
languageOptions: {
|
|
43
43
|
globals: {
|
|
44
44
|
...globals.node,
|
|
@@ -56,6 +56,25 @@ export default defineConfig([
|
|
|
56
56
|
'prettier/prettier': 'error'
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
files: ['template/view/js/**/*.js'],
|
|
61
|
+
languageOptions: {
|
|
62
|
+
globals: {
|
|
63
|
+
...globals.browser,
|
|
64
|
+
Odac: 'readonly'
|
|
65
|
+
},
|
|
66
|
+
sourceType: 'script'
|
|
67
|
+
},
|
|
68
|
+
plugins: {
|
|
69
|
+
js,
|
|
70
|
+
prettier: prettierPlugin
|
|
71
|
+
},
|
|
72
|
+
rules: {
|
|
73
|
+
...js.configs.recommended.rules,
|
|
74
|
+
...prettierConfig.rules,
|
|
75
|
+
'prettier/prettier': 'error'
|
|
76
|
+
}
|
|
77
|
+
},
|
|
59
78
|
{
|
|
60
79
|
files: ['template/public/**/*.js'],
|
|
61
80
|
languageOptions: {
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"email": "mail@emre.red",
|
|
8
8
|
"url": "https://emre.red"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.4.
|
|
10
|
+
"version": "1.4.12",
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=18.0.0"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@tailwindcss/cli": "^4.1.18",
|
|
25
|
+
"esbuild": "^0.25.12",
|
|
25
26
|
"knex": "^3.1.0",
|
|
26
27
|
"lmdb": "^3.4.4",
|
|
27
28
|
"tailwindcss": "^4.1.18"
|
package/src/Config.js
CHANGED
|
@@ -27,6 +27,13 @@ module.exports = {
|
|
|
27
27
|
driver: 'memory',
|
|
28
28
|
redis: 'default'
|
|
29
29
|
},
|
|
30
|
+
js: {
|
|
31
|
+
target: 'es2020',
|
|
32
|
+
minify: true,
|
|
33
|
+
sourcemap: false,
|
|
34
|
+
bundle: true,
|
|
35
|
+
obfuscate: false
|
|
36
|
+
},
|
|
30
37
|
debug: process.env.NODE_ENV !== 'production',
|
|
31
38
|
|
|
32
39
|
init: function () {
|
|
@@ -681,7 +681,7 @@ class Migration {
|
|
|
681
681
|
*/
|
|
682
682
|
async _createTable(knex, tableName, schema) {
|
|
683
683
|
await knex.schema.createTable(tableName, table => {
|
|
684
|
-
this._buildColumns(table, schema.columns)
|
|
684
|
+
this._buildColumns(knex, table, schema.columns)
|
|
685
685
|
this._buildIndexes(table, schema.indexes)
|
|
686
686
|
})
|
|
687
687
|
}
|
|
@@ -716,13 +716,13 @@ class Migration {
|
|
|
716
716
|
for (const op of batchOps) {
|
|
717
717
|
switch (op.type) {
|
|
718
718
|
case 'add_column':
|
|
719
|
-
this._addColumn(table, op.column, op.definition)
|
|
719
|
+
this._addColumn(knex, table, op.column, op.definition)
|
|
720
720
|
break
|
|
721
721
|
case 'drop_column':
|
|
722
722
|
table.dropColumn(op.column)
|
|
723
723
|
break
|
|
724
724
|
case 'alter_column':
|
|
725
|
-
this._alterColumn(table, op.column, op.definition, op.currentNullable)
|
|
725
|
+
this._alterColumn(knex, table, op.column, op.definition, op.currentNullable)
|
|
726
726
|
break
|
|
727
727
|
}
|
|
728
728
|
}
|
|
@@ -739,8 +739,9 @@ class Migration {
|
|
|
739
739
|
|
|
740
740
|
// Apply default value change if specified
|
|
741
741
|
if (op.definition.default !== undefined) {
|
|
742
|
-
|
|
743
|
-
|
|
742
|
+
const lower = String(op.definition.default).toLowerCase().trim()
|
|
743
|
+
if (lower === 'now()' || lower === 'current_timestamp' || lower === 'current_timestamp()') {
|
|
744
|
+
await knex.raw(`ALTER TABLE ?? ALTER COLUMN ?? SET DEFAULT ${lower}`, [tableName, op.column])
|
|
744
745
|
} else {
|
|
745
746
|
await knex.raw(`ALTER TABLE ?? ALTER COLUMN ?? SET DEFAULT ?`, [tableName, op.column, op.definition.default])
|
|
746
747
|
}
|
|
@@ -915,7 +916,27 @@ class Migration {
|
|
|
915
916
|
* @param {object} table - Knex TableBuilder instance
|
|
916
917
|
* @param {object} columns - Column definition map
|
|
917
918
|
*/
|
|
918
|
-
|
|
919
|
+
/**
|
|
920
|
+
* Resolves a column default value, wrapping special SQL keywords in knex.raw().
|
|
921
|
+
* Why: Knex.defaultTo() quotes string values by default. For keywords like
|
|
922
|
+
* CURRENT_TIMESTAMP, this results in 'CURRENT_TIMESTAMP' which MySQL rejects.
|
|
923
|
+
* Wrapping in knex.raw() ensures the keyword is emitted without quotes.
|
|
924
|
+
* @param {object} knex - Knex instance
|
|
925
|
+
* @param {*} value - Raw default value from schema
|
|
926
|
+
* @returns {*} Resolved value (possibly knex.raw)
|
|
927
|
+
*/
|
|
928
|
+
_resolveDefault(knex, value) {
|
|
929
|
+
if (typeof value !== 'string') return value
|
|
930
|
+
|
|
931
|
+
const lower = value.toLowerCase().trim()
|
|
932
|
+
if (lower === 'current_timestamp' || lower === 'current_timestamp()' || lower === 'now()') {
|
|
933
|
+
return knex.raw(value)
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return value
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
_buildColumns(knex, table, columns) {
|
|
919
940
|
if (!columns) return
|
|
920
941
|
|
|
921
942
|
for (const [colName, def] of Object.entries(columns)) {
|
|
@@ -930,7 +951,7 @@ class Migration {
|
|
|
930
951
|
if (def.nullable === false) col.notNullable()
|
|
931
952
|
else if (def.nullable === true) col.nullable()
|
|
932
953
|
|
|
933
|
-
if (def.default !== undefined) col.defaultTo(def.default)
|
|
954
|
+
if (def.default !== undefined) col.defaultTo(this._resolveDefault(knex, def.default))
|
|
934
955
|
if (def.unsigned) col.unsigned()
|
|
935
956
|
// Column-level unique is handled via _normalizeSchema โ _buildIndexes.
|
|
936
957
|
// Applying it here as well would create duplicate constraints.
|
|
@@ -1019,14 +1040,14 @@ class Migration {
|
|
|
1019
1040
|
* @param {string} colName - Column name
|
|
1020
1041
|
* @param {object} def - Column definition
|
|
1021
1042
|
*/
|
|
1022
|
-
_addColumn(table, colName, def) {
|
|
1043
|
+
_addColumn(knex, table, colName, def) {
|
|
1023
1044
|
const col = this._createColumnBuilder(table, colName, def)
|
|
1024
1045
|
if (!col) return
|
|
1025
1046
|
|
|
1026
1047
|
if (def.nullable === false) col.notNullable()
|
|
1027
1048
|
else col.nullable()
|
|
1028
1049
|
|
|
1029
|
-
if (def.default !== undefined) col.defaultTo(def.default)
|
|
1050
|
+
if (def.default !== undefined) col.defaultTo(this._resolveDefault(knex, def.default))
|
|
1030
1051
|
if (def.unsigned) col.unsigned()
|
|
1031
1052
|
if (def.references) col.references(def.references.column).inTable(def.references.table)
|
|
1032
1053
|
if (def.onDelete) col.onDelete(def.onDelete)
|
|
@@ -1039,7 +1060,7 @@ class Migration {
|
|
|
1039
1060
|
* @param {string} colName - Column name
|
|
1040
1061
|
* @param {object} def - Column definition
|
|
1041
1062
|
*/
|
|
1042
|
-
_alterColumn(table, colName, def, currentNullable) {
|
|
1063
|
+
_alterColumn(knex, table, colName, def, currentNullable) {
|
|
1043
1064
|
const col = this._createColumnBuilder(table, colName, def)
|
|
1044
1065
|
if (!col) return
|
|
1045
1066
|
|
|
@@ -1052,7 +1073,7 @@ class Migration {
|
|
|
1052
1073
|
else if (currentNullable === false) col.notNullable()
|
|
1053
1074
|
else if (currentNullable === true) col.nullable()
|
|
1054
1075
|
|
|
1055
|
-
if (def.default !== undefined) col.defaultTo(def.default)
|
|
1076
|
+
if (def.default !== undefined) col.defaultTo(this._resolveDefault(knex, def.default))
|
|
1056
1077
|
|
|
1057
1078
|
col.alter()
|
|
1058
1079
|
}
|
package/src/Mail.js
CHANGED
|
@@ -317,6 +317,62 @@ class Mail {
|
|
|
317
317
|
return '=?UTF-8?B?' + Buffer.from(text).toString('base64') + '?='
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
#wrapLines(content, limit = 76) {
|
|
321
|
+
if (!content) return ''
|
|
322
|
+
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
323
|
+
const out = []
|
|
324
|
+
for (const line of normalized.split('\n')) {
|
|
325
|
+
if (Buffer.byteLength(line, 'utf8') <= limit) {
|
|
326
|
+
out.push(line)
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
let buf = ''
|
|
330
|
+
let bufBytes = 0
|
|
331
|
+
const flush = () => {
|
|
332
|
+
if (buf.length) out.push(buf)
|
|
333
|
+
buf = ''
|
|
334
|
+
bufBytes = 0
|
|
335
|
+
}
|
|
336
|
+
for (const word of line.split(/(\s+)/)) {
|
|
337
|
+
if (!word) continue
|
|
338
|
+
const wordBytes = Buffer.byteLength(word, 'utf8')
|
|
339
|
+
if (bufBytes + wordBytes <= limit) {
|
|
340
|
+
buf += word
|
|
341
|
+
bufBytes += wordBytes
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
if (buf.length && /^\s+$/.test(word)) {
|
|
345
|
+
flush()
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
if (buf.length) flush()
|
|
349
|
+
if (wordBytes <= limit) {
|
|
350
|
+
buf = word
|
|
351
|
+
bufBytes = wordBytes
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
let chunk = ''
|
|
355
|
+
let chunkBytes = 0
|
|
356
|
+
for (const ch of word) {
|
|
357
|
+
const chBytes = Buffer.byteLength(ch, 'utf8')
|
|
358
|
+
if (chunkBytes + chBytes > limit) {
|
|
359
|
+
out.push(chunk)
|
|
360
|
+
chunk = ''
|
|
361
|
+
chunkBytes = 0
|
|
362
|
+
}
|
|
363
|
+
chunk += ch
|
|
364
|
+
chunkBytes += chBytes
|
|
365
|
+
}
|
|
366
|
+
if (chunk.length) {
|
|
367
|
+
buf = chunk
|
|
368
|
+
bufBytes = chunkBytes
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
flush()
|
|
372
|
+
}
|
|
373
|
+
return out.join('\r\n')
|
|
374
|
+
}
|
|
375
|
+
|
|
320
376
|
#stripHtml(html) {
|
|
321
377
|
if (!html) return ''
|
|
322
378
|
|
|
@@ -376,6 +432,11 @@ class Mail {
|
|
|
376
432
|
}
|
|
377
433
|
}
|
|
378
434
|
|
|
435
|
+
// RFC 5321 ยง4.5.3.1.6: SMTP lines must be โค1000 octets including CRLF.
|
|
436
|
+
// Wrap to 990 chars for HTML and 76 for text to prevent SMTP rejection.
|
|
437
|
+
htmlContent = this.#wrapLines(htmlContent, 990)
|
|
438
|
+
textContent = this.#wrapLines(textContent)
|
|
439
|
+
|
|
379
440
|
if (!this.#header['From']) this.#header['From'] = `${this.#encode(this.#from.name)} <${this.#from.email}>`
|
|
380
441
|
if (!this.#header['To']) {
|
|
381
442
|
const t = this.#to.value[0]
|
package/src/Odac.js
CHANGED
|
@@ -156,6 +156,9 @@ module.exports = {
|
|
|
156
156
|
_odac.write = function (value) {
|
|
157
157
|
return _odac.Request.write(value)
|
|
158
158
|
}
|
|
159
|
+
_odac.cache = function (seconds) {
|
|
160
|
+
return _odac.Request.cache(seconds)
|
|
161
|
+
}
|
|
159
162
|
_odac.stream = function (input) {
|
|
160
163
|
_odac.Request.clearTimeout()
|
|
161
164
|
return new (require('./Stream'))(_odac.Request.req, _odac.Request.res, input, _odac)
|
package/src/Request.js
CHANGED
|
@@ -310,6 +310,27 @@ class OdacRequest {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
// - SET PROXY CACHE
|
|
314
|
+
/**
|
|
315
|
+
* Enables ODAC Proxy caching for the current response.
|
|
316
|
+
* Sets the X-ODAC-Cache header with the specified TTL (in seconds)
|
|
317
|
+
* and updates Cache-Control to allow proxy caching.
|
|
318
|
+
*
|
|
319
|
+
* Why: Allows controllers to declaratively opt-in to proxy-level
|
|
320
|
+
* caching for static or semi-static HTML responses, offloading
|
|
321
|
+
* repeated rendering from the application server.
|
|
322
|
+
*
|
|
323
|
+
* @param {number} seconds - Cache TTL in seconds (must be a positive integer)
|
|
324
|
+
* @throws {TypeError} If seconds is not a positive integer
|
|
325
|
+
*/
|
|
326
|
+
cache(seconds) {
|
|
327
|
+
if (!Number.isInteger(seconds) || seconds < 1) {
|
|
328
|
+
throw new TypeError('Odac.cache() requires a positive integer (seconds)')
|
|
329
|
+
}
|
|
330
|
+
this.header('X-ODAC-Cache', seconds)
|
|
331
|
+
this.header('Cache-Control', `public, max-age=${seconds}`)
|
|
332
|
+
}
|
|
333
|
+
|
|
313
334
|
// - HTTP CODE
|
|
314
335
|
status(code) {
|
|
315
336
|
this.#status = code
|
package/src/Route/Cron.js
CHANGED
|
@@ -104,6 +104,15 @@ class Cron {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Removes all cron jobs registered from a given route file.
|
|
109
|
+
* Called before hot-reloading a route file to prevent duplicate cron registrations.
|
|
110
|
+
*/
|
|
111
|
+
clear(route) {
|
|
112
|
+
if (!route) return
|
|
113
|
+
this.#jobs = this.#jobs.filter(job => job.route !== route)
|
|
114
|
+
}
|
|
115
|
+
|
|
107
116
|
job(controller) {
|
|
108
117
|
let path
|
|
109
118
|
if (typeof controller !== 'function') {
|
|
@@ -114,6 +123,7 @@ class Cron {
|
|
|
114
123
|
}
|
|
115
124
|
}
|
|
116
125
|
this.#jobs.push({
|
|
126
|
+
route: global.Odac?.Route?.buff || null,
|
|
117
127
|
controller: typeof controller === 'function' ? null : controller,
|
|
118
128
|
lastRun: null,
|
|
119
129
|
condition: [],
|
package/src/Route.js
CHANGED
|
@@ -452,6 +452,7 @@ class Route {
|
|
|
452
452
|
|
|
453
453
|
if (!routes2[Odac.Route.buff] || routes2[Odac.Route.buff] < mtime - 1000) {
|
|
454
454
|
delete require.cache[require.resolve(filePath)]
|
|
455
|
+
Cron.clear(Odac.Route.buff)
|
|
455
456
|
routes2[Odac.Route.buff] = mtime
|
|
456
457
|
const routeModule = require(filePath)
|
|
457
458
|
if (typeof routeModule === 'function') {
|
|
File without changes
|