odac 1.4.11 โ 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 +16 -0
- package/docs/ai/skills/backend/config.md +21 -1
- package/docs/ai/skills/backend/database.md +40 -0
- package/docs/backend/03-config/00-configuration-overview.md +12 -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/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/package.json +1 -1
- package/src/Database/Migration.js +32 -11
- package/src/Mail.js +61 -0
- package/src/Route/Cron.js +10 -0
- package/src/Route.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
### ๐ Documentation
|
|
2
|
+
|
|
3
|
+
- refactor database configuration to a unified structure and document multi-connection support
|
|
4
|
+
|
|
5
|
+
### ๐ ๏ธ Fixes & Improvements
|
|
6
|
+
|
|
7
|
+
- **mail:** add line wrapping for email content to comply with SMTP standards
|
|
8
|
+
- **mail:** use 990-char wrap for HTML, 76 for text; trim migration defaults
|
|
9
|
+
- **route:** hot-reload cron fix
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Powered by [โก ODAC](https://odac.run)
|
|
16
|
+
|
|
1
17
|
### โจ What's New
|
|
2
18
|
|
|
3
19
|
- add esbuild-powered JS/TS frontend pipeline
|
|
@@ -30,10 +30,30 @@ const apiKey = Odac.env('API_KEY');
|
|
|
30
30
|
### 2. odac.json Structure
|
|
31
31
|
```json
|
|
32
32
|
{
|
|
33
|
-
"
|
|
33
|
+
"database": {
|
|
34
|
+
"type": "mysql",
|
|
34
35
|
"host": "${DB_HOST}",
|
|
35
36
|
"password": "${DB_PASSWORD}"
|
|
36
37
|
},
|
|
37
38
|
"debug": true
|
|
38
39
|
}
|
|
39
40
|
```
|
|
41
|
+
|
|
42
|
+
### 3. Multiple Databases
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"database": {
|
|
46
|
+
"default": {
|
|
47
|
+
"type": "mysql",
|
|
48
|
+
"database": "main_app"
|
|
49
|
+
},
|
|
50
|
+
"analytics": {
|
|
51
|
+
"type": "postgres",
|
|
52
|
+
"host": "analytics.local",
|
|
53
|
+
"database": "events"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Access via: `Odac.DB.analytics.tableName`
|
|
@@ -16,6 +16,46 @@ High-performance database operations using the ODAC Query Builder, Read-Through
|
|
|
16
16
|
4. **Read Caching**: Use `cache()` for frequently-read, rarely-changed data.
|
|
17
17
|
5. **Write Coalescing**: Use `buffer` for high-frequency writes to avoid DB saturation.
|
|
18
18
|
|
|
19
|
+
## Database Configuration (`odac.json`)
|
|
20
|
+
|
|
21
|
+
ODAC connects to databases automatically based on the `database` key in `odac.json`.
|
|
22
|
+
|
|
23
|
+
### 1. Single Connection
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"database": {
|
|
27
|
+
"type": "mysql",
|
|
28
|
+
"host": "${DB_HOST}",
|
|
29
|
+
"user": "${DB_USER}",
|
|
30
|
+
"password": "${DB_PASSWORD}",
|
|
31
|
+
"database": "myapp"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Multiple Connections
|
|
37
|
+
Define named objects. The one named `default` (or the first one) is used by default.
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"database": {
|
|
41
|
+
"default": {
|
|
42
|
+
"type": "mysql",
|
|
43
|
+
"database": "app_db"
|
|
44
|
+
},
|
|
45
|
+
"analytics": {
|
|
46
|
+
"type": "postgres",
|
|
47
|
+
"host": "remote-stats.db",
|
|
48
|
+
"database": "events"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Access named connections via: `Odac.DB.analytics.tableName`
|
|
55
|
+
|
|
56
|
+
### 3. Environment Variables
|
|
57
|
+
Always use `${VAR_NAME}` for sensitive credentials. Map them in `.env`.
|
|
58
|
+
|
|
19
59
|
## Query Builder Patterns
|
|
20
60
|
```javascript
|
|
21
61
|
const user = await Odac.DB.users
|
|
@@ -12,7 +12,8 @@ The main configuration file located in your website's root directory. This file
|
|
|
12
12
|
"request": {
|
|
13
13
|
"timeout": 30000
|
|
14
14
|
},
|
|
15
|
-
"
|
|
15
|
+
"database": {
|
|
16
|
+
"type": "mysql",
|
|
16
17
|
"host": "localhost",
|
|
17
18
|
"user": "root",
|
|
18
19
|
"password": "secret123",
|
|
@@ -38,7 +39,8 @@ Perfect for development or non-sensitive settings:
|
|
|
38
39
|
|
|
39
40
|
```json
|
|
40
41
|
{
|
|
41
|
-
"
|
|
42
|
+
"database": {
|
|
43
|
+
"type": "mysql",
|
|
42
44
|
"host": "localhost",
|
|
43
45
|
"password": "dev123"
|
|
44
46
|
}
|
|
@@ -50,7 +52,8 @@ Use `${VARIABLE}` syntax in `odac.json` to reference `.env` values:
|
|
|
50
52
|
|
|
51
53
|
```json
|
|
52
54
|
{
|
|
53
|
-
"
|
|
55
|
+
"database": {
|
|
56
|
+
"type": "mysql",
|
|
54
57
|
"host": "${MYSQL_HOST}",
|
|
55
58
|
"password": "${MYSQL_PASSWORD}"
|
|
56
59
|
}
|
|
@@ -68,7 +71,8 @@ Combine both methods - use direct values for non-sensitive data and environment
|
|
|
68
71
|
|
|
69
72
|
```json
|
|
70
73
|
{
|
|
71
|
-
"
|
|
74
|
+
"database": {
|
|
75
|
+
"type": "mysql",
|
|
72
76
|
"host": "localhost",
|
|
73
77
|
"user": "root",
|
|
74
78
|
"password": "${MYSQL_PASSWORD}",
|
|
@@ -88,7 +92,7 @@ You can access configuration values in three ways:
|
|
|
88
92
|
|
|
89
93
|
```javascript
|
|
90
94
|
// 1. From Odac.Config (recommended for structured config)
|
|
91
|
-
const dbHost = Odac.Config.
|
|
95
|
+
const dbHost = Odac.Config.database.host
|
|
92
96
|
|
|
93
97
|
// 2. Using Odac.env() helper
|
|
94
98
|
const apiKey = Odac.env('API_KEY')
|
|
@@ -130,6 +134,7 @@ const nodeEnv = process.env.NODE_ENV
|
|
|
130
134
|
```json
|
|
131
135
|
{
|
|
132
136
|
"database": {
|
|
137
|
+
"type": "mysql",
|
|
133
138
|
"host": "localhost",
|
|
134
139
|
"user": "root",
|
|
135
140
|
"password": "${MYSQL_PASSWORD}",
|
|
@@ -195,7 +200,8 @@ See individual documentation sections for detailed configuration options.
|
|
|
195
200
|
"request": {
|
|
196
201
|
"timeout": 30000
|
|
197
202
|
},
|
|
198
|
-
"
|
|
203
|
+
"database": {
|
|
204
|
+
"type": "mysql",
|
|
199
205
|
"host": "${MYSQL_HOST}",
|
|
200
206
|
"user": "${MYSQL_USER}",
|
|
201
207
|
"password": "${MYSQL_PASSWORD}",
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
## ๐ Database Connection
|
|
2
2
|
|
|
3
|
-
When you add a `
|
|
3
|
+
When you add a `database` object to your `odac.json`, the system will automatically connect to your database. No separate connection setup is needed in your code.
|
|
4
4
|
|
|
5
5
|
### Basic Configuration
|
|
6
6
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
9
|
-
"
|
|
9
|
+
"database": {
|
|
10
|
+
"type": "mysql",
|
|
10
11
|
"host": "localhost",
|
|
11
12
|
"user": "your_user",
|
|
12
13
|
"password": "your_password",
|
|
@@ -24,7 +25,8 @@ For better security, especially in production, you can use environment variables
|
|
|
24
25
|
**odac.json:**
|
|
25
26
|
```json
|
|
26
27
|
{
|
|
27
|
-
"
|
|
28
|
+
"database": {
|
|
29
|
+
"type": "mysql",
|
|
28
30
|
"host": "${MYSQL_HOST}",
|
|
29
31
|
"user": "${MYSQL_USER}",
|
|
30
32
|
"password": "${MYSQL_PASSWORD}",
|
|
@@ -48,7 +50,8 @@ You can also mix direct values with environment variables:
|
|
|
48
50
|
|
|
49
51
|
```json
|
|
50
52
|
{
|
|
51
|
-
"
|
|
53
|
+
"database": {
|
|
54
|
+
"type": "mysql",
|
|
52
55
|
"host": "localhost",
|
|
53
56
|
"user": "root",
|
|
54
57
|
"password": "${MYSQL_PASSWORD}",
|
|
@@ -58,3 +61,37 @@ You can also mix direct values with environment variables:
|
|
|
58
61
|
```
|
|
59
62
|
|
|
60
63
|
This way, non-sensitive values are directly in the config while passwords remain in the `.env` file.
|
|
64
|
+
|
|
65
|
+
### Multiple Database Connections
|
|
66
|
+
|
|
67
|
+
ODAC supports multiple simultaneous database connections. You can define them as named objects within the `database` configuration:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"database": {
|
|
72
|
+
"default": {
|
|
73
|
+
"type": "mysql",
|
|
74
|
+
"host": "localhost",
|
|
75
|
+
"database": "app_db"
|
|
76
|
+
},
|
|
77
|
+
"analytics": {
|
|
78
|
+
"type": "postgres",
|
|
79
|
+
"host": "remote-stats.db",
|
|
80
|
+
"database": "events"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Usage in Code
|
|
87
|
+
|
|
88
|
+
To use a specific connection, access it by its name via `Odac.DB`:
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
// Uses 'default' connection
|
|
92
|
+
const users = await Odac.DB.users.select('*')
|
|
93
|
+
|
|
94
|
+
// Uses 'analytics' connection
|
|
95
|
+
const events = await Odac.DB.analytics.pageviews.select('*')
|
|
96
|
+
```
|
|
97
|
+
|
|
@@ -43,7 +43,8 @@ Reference environment variables using `${VARIABLE_NAME}` syntax:
|
|
|
43
43
|
|
|
44
44
|
```json
|
|
45
45
|
{
|
|
46
|
-
"
|
|
46
|
+
"database": {
|
|
47
|
+
"type": "mysql",
|
|
47
48
|
"host": "${MYSQL_HOST}",
|
|
48
49
|
"user": "${MYSQL_USER}",
|
|
49
50
|
"password": "${MYSQL_PASSWORD}",
|
|
@@ -95,7 +96,7 @@ module.exports = function() {
|
|
|
95
96
|
|
|
96
97
|
```javascript
|
|
97
98
|
module.exports = function() {
|
|
98
|
-
const dbHost = Odac.Config.
|
|
99
|
+
const dbHost = Odac.Config.database.host
|
|
99
100
|
const apiKey = Odac.Config.api.stripe.key
|
|
100
101
|
}
|
|
101
102
|
```
|
|
@@ -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/package.json
CHANGED
|
@@ -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/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') {
|