odac 1.0.0 → 1.1.0

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 (61) hide show
  1. package/.github/workflows/auto-pr-description.yml +3 -1
  2. package/CHANGELOG.md +127 -0
  3. package/README.md +39 -36
  4. package/bin/odac.js +1 -31
  5. package/client/odac.js +871 -994
  6. package/docs/backend/01-overview/03-development-server.md +7 -7
  7. package/docs/backend/02-structure/01-typical-project-layout.md +1 -0
  8. package/docs/backend/03-config/00-configuration-overview.md +9 -0
  9. package/docs/backend/03-config/01-database-connection.md +1 -1
  10. package/docs/backend/04-routing/02-controller-less-view-routes.md +9 -3
  11. package/docs/backend/04-routing/09-websocket.md +29 -0
  12. package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +2 -0
  13. package/docs/backend/05-controllers/03-controller-classes.md +27 -41
  14. package/docs/backend/05-forms/01-custom-forms.md +103 -95
  15. package/docs/backend/05-forms/02-automatic-database-insert.md +21 -21
  16. package/docs/backend/07-views/02-rendering-a-view.md +1 -1
  17. package/docs/backend/07-views/03-variables.md +5 -5
  18. package/docs/backend/07-views/04-request-data.md +1 -1
  19. package/docs/backend/07-views/08-backend-javascript.md +1 -1
  20. package/docs/backend/08-database/01-getting-started.md +100 -0
  21. package/docs/backend/08-database/02-basics.md +136 -0
  22. package/docs/backend/08-database/03-advanced.md +84 -0
  23. package/docs/backend/08-database/04-migrations.md +48 -0
  24. package/docs/backend/09-validation/01-the-validator-service.md +1 -0
  25. package/docs/backend/10-authentication/03-register.md +8 -1
  26. package/docs/backend/10-authentication/04-odac-register-forms.md +46 -46
  27. package/docs/backend/10-authentication/05-session-management.md +1 -1
  28. package/docs/backend/10-authentication/06-odac-login-forms.md +48 -48
  29. package/docs/backend/10-authentication/07-magic-links.md +134 -0
  30. package/docs/backend/11-mail/01-the-mail-service.md +118 -28
  31. package/docs/backend/12-streaming/01-streaming-overview.md +2 -2
  32. package/docs/backend/13-utilities/01-odac-var.md +7 -7
  33. package/docs/backend/13-utilities/02-ipc.md +73 -0
  34. package/docs/frontend/01-overview/01-introduction.md +5 -1
  35. package/docs/frontend/02-ajax-navigation/01-quick-start.md +1 -1
  36. package/docs/index.json +16 -124
  37. package/eslint.config.mjs +5 -47
  38. package/package.json +9 -4
  39. package/src/Auth.js +362 -104
  40. package/src/Config.js +7 -2
  41. package/src/Database.js +188 -0
  42. package/src/Ipc.js +330 -0
  43. package/src/Mail.js +408 -37
  44. package/src/Odac.js +65 -9
  45. package/src/Request.js +70 -48
  46. package/src/Route/Cron.js +4 -1
  47. package/src/Route/Internal.js +214 -11
  48. package/src/Route/Middleware.js +7 -2
  49. package/src/Route.js +106 -26
  50. package/src/Server.js +80 -11
  51. package/src/Storage.js +165 -0
  52. package/src/Validator.js +94 -2
  53. package/src/View/Form.js +193 -17
  54. package/src/View.js +46 -1
  55. package/src/WebSocket.js +18 -3
  56. package/template/config.json +1 -1
  57. package/template/route/www.js +12 -10
  58. package/test/core/{Candy.test.js → Odac.test.js} +2 -2
  59. package/docs/backend/08-database/01-database-connection.md +0 -99
  60. package/docs/backend/08-database/02-using-mysql.md +0 -322
  61. package/src/Mysql.js +0 -575
@@ -6,9 +6,9 @@ Forms can automatically insert data into your database without writing any contr
6
6
 
7
7
  ```html
8
8
  <odac:form table="waitlist">
9
- <odac:field name="email" type="email" label="Email">
9
+ <odac:input name="email" type="email" label="Email">
10
10
  <odac:validate rule="required|email|unique"/>
11
- </odac:field>
11
+ </odac:input>
12
12
 
13
13
  <odac:submit text="Join"/>
14
14
  </odac:form>
@@ -45,13 +45,13 @@ CREATE TABLE `waitlist` (
45
45
  <h1>Join Our Waitlist</h1>
46
46
 
47
47
  <odac:form table="waitlist" redirect="/" success="Thank you for joining!">
48
- <odac:field name="email" type="email" label="Email" placeholder="your@email.com">
48
+ <odac:input name="email" type="email" label="Email" placeholder="your@email.com">
49
49
  <odac:validate rule="required|email|unique" message="Please enter a valid email"/>
50
- </odac:field>
50
+ </odac:input>
51
51
 
52
- <odac:field name="name" type="text" label="Name" placeholder="Your name">
52
+ <odac:input name="name" type="text" label="Name" placeholder="Your name">
53
53
  <odac:validate rule="required|minlen:2" message="Name is required"/>
54
- </odac:field>
54
+ </odac:input>
55
55
 
56
56
  <odac:set name="created_at" compute="now"/>
57
57
  <odac:set name="ip" compute="ip"/>
@@ -110,9 +110,9 @@ Custom success message to display.
110
110
  Use `unique` rule to prevent duplicate entries:
111
111
 
112
112
  ```html
113
- <odac:field name="email" type="email">
113
+ <odac:input name="email" type="email">
114
114
  <odac:validate rule="required|email|unique" message="This email is already registered"/>
115
- </odac:field>
115
+ </odac:input>
116
116
  ```
117
117
 
118
118
  The system will:
@@ -163,9 +163,9 @@ Only set if field is empty:
163
163
 
164
164
  ```html
165
165
  <odac:form table="newsletter" success="Thanks for subscribing!">
166
- <odac:field name="email" type="email">
166
+ <odac:input name="email" type="email">
167
167
  <odac:validate rule="required|email|unique"/>
168
- </odac:field>
168
+ </odac:input>
169
169
 
170
170
  <odac:set name="subscribed_at" compute="now"/>
171
171
  <odac:set name="status" value="active"/>
@@ -178,13 +178,13 @@ Only set if field is empty:
178
178
 
179
179
  ```html
180
180
  <odac:form table="feedback" redirect="/" success="Thank you for your feedback!">
181
- <odac:field name="rating" type="number" label="Rating (1-5)">
181
+ <odac:input name="rating" type="number" label="Rating (1-5)">
182
182
  <odac:validate rule="required|min:1|max:5"/>
183
- </odac:field>
183
+ </odac:input>
184
184
 
185
- <odac:field name="comment" type="textarea" label="Comment">
185
+ <odac:input name="comment" type="textarea" label="Comment">
186
186
  <odac:validate rule="required|minlen:10"/>
187
- </odac:field>
187
+ </odac:input>
188
188
 
189
189
  <odac:set name="created_at" compute="now"/>
190
190
  <odac:set name="ip" compute="ip"/>
@@ -197,17 +197,17 @@ Only set if field is empty:
197
197
 
198
198
  ```html
199
199
  <odac:form table="beta_requests" success="You're on the list!">
200
- <odac:field name="email" type="email">
200
+ <odac:input name="email" type="email">
201
201
  <odac:validate rule="required|email|unique"/>
202
- </odac:field>
202
+ </odac:input>
203
203
 
204
- <odac:field name="company" type="text">
204
+ <odac:input name="company" type="text">
205
205
  <odac:validate rule="required"/>
206
- </odac:field>
206
+ </odac:input>
207
207
 
208
- <odac:field name="use_case" type="textarea">
208
+ <odac:input name="use_case" type="textarea">
209
209
  <odac:validate rule="required|minlen:20"/>
210
- </odac:field>
210
+ </odac:input>
211
211
 
212
212
  <odac:set name="requested_at" compute="now"/>
213
213
  <odac:set name="status" value="pending"/>
@@ -270,7 +270,7 @@ Odac.Route.post('/contact/submit', async Odac => {
270
270
  await sendEmail(Odac.formData.email, 'Thank you!')
271
271
 
272
272
  // Manually insert to database if needed
273
- await Odac.Mysql.query('INSERT INTO contacts SET ?', Odac.formData)
273
+ await Odac.DB.contacts.insert(Odac.formData)
274
274
 
275
275
  return Odac.return({
276
276
  result: {success: true, message: 'Message sent!'}
@@ -118,7 +118,7 @@ Create a separate view part for the `<head>` section:
118
118
  ```javascript
119
119
  module.exports = async function (Odac) {
120
120
  const productId = Odac.Request.get('id')
121
- const product = await Odac.Mysql.table('products')
121
+ const product = await Odac.DB.table('products')
122
122
  .where('id', productId)
123
123
  .first()
124
124
 
@@ -141,7 +141,7 @@ You have full access to the `Odac` object within templates:
141
141
  module.exports = async function(Odac) {
142
142
  // Fetch user from database
143
143
  const userId = Odac.Request.get('id')
144
- const user = await Odac.Mysql.table('users')
144
+ const user = await Odac.DB.users
145
145
  .where('id', userId)
146
146
  .first()
147
147
 
@@ -179,7 +179,7 @@ module.exports = async function(Odac) {
179
179
  // Controller: controller/product.js
180
180
  module.exports = async function(Odac) {
181
181
  const productId = Odac.Request.get('id')
182
- const product = await Odac.Mysql.table('products')
182
+ const product = await Odac.DB.products
183
183
  .where('id', productId)
184
184
  .first()
185
185
 
@@ -221,7 +221,7 @@ module.exports = async function(Odac) {
221
221
  ```javascript
222
222
  // Controller: controller/products.js
223
223
  module.exports = async function(Odac) {
224
- const products = await Odac.Mysql.table('products')
224
+ const products = await Odac.DB.products
225
225
  .where('active', true)
226
226
  .get()
227
227
 
@@ -259,7 +259,7 @@ module.exports = async function(Odac) {
259
259
  **Good:**
260
260
  ```javascript
261
261
  // Controller
262
- const user = await Odac.Mysql.table('users').first()
262
+ const user = await Odac.DB.users.first()
263
263
  const isAdmin = user.role === 'admin'
264
264
 
265
265
  Odac.set({
@@ -284,7 +284,7 @@ Always handle cases where data might not exist:
284
284
  // Controller
285
285
  module.exports = async function(Odac) {
286
286
  const productId = Odac.Request.get('id')
287
- const product = await Odac.Mysql.table('products')
287
+ const product = await Odac.DB.products
288
288
  .where('id', productId)
289
289
  .first()
290
290
 
@@ -69,7 +69,7 @@ module.exports = async function(Odac) {
69
69
  const validatedPage = Math.max(1, page)
70
70
 
71
71
  // Fetch results
72
- const results = await Odac.Mysql.table('products')
72
+ const results = await Odac.DB.products
73
73
  .where('name', 'like', `%${validatedQuery}%`)
74
74
  .limit(20)
75
75
  .offset((validatedPage - 1) * 20)
@@ -379,7 +379,7 @@ You can use multiple `<script:odac>` blocks in the same view:
379
379
  ```html
380
380
  <script:odac>
381
381
  // Don't do this - should be in controller
382
- const users = await Odac.Mysql.query('SELECT * FROM users');
382
+ const users = await Odac.DB.users.get();
383
383
  const apiData = await fetch('https://api.example.com/data');
384
384
  </script:odac>
385
385
  ```
@@ -0,0 +1,100 @@
1
+ # Getting Started
2
+
3
+ ODAC supports multiple database connections including **MySQL**, **PostgreSQL** (beta), and **SQLite**. It uses a robust and secure connection pooling mechanism.
4
+
5
+ ## Configuration
6
+
7
+ Add your database credentials to `config.json`.
8
+
9
+ Supported configuration options:
10
+
11
+ - `host` - Database server hostname (default: `localhost`)
12
+ - `user` - Database username
13
+ - `password` - Database password
14
+ - `database` - Database name
15
+ - `port` - Database port
16
+ - `type` - Database type (`mysql`, `postgres`, `sqlite`)
17
+ - `filename` - Database file path (only for `sqlite`, default: `./dev.sqlite3`)
18
+
19
+ ### Single Connection (MySQL Default)
20
+
21
+ ```json
22
+ {
23
+ "database": {
24
+ "type": "mysql",
25
+ "host": "localhost",
26
+ "user": "root",
27
+ "password": "password",
28
+ "database": "odac_app"
29
+ }
30
+ }
31
+ ```
32
+
33
+ ### PostgreSQL
34
+
35
+ ```json
36
+ {
37
+ "database": {
38
+ "type": "postgres",
39
+ "host": "localhost",
40
+ "user": "postgres",
41
+ "password": "password",
42
+ "database": "odac_app",
43
+ "port": 5432
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### Multiple Databases
49
+
50
+ You can configure multiple database connections. The connection named `default` (or the first one) is used automatically.
51
+
52
+ ```json
53
+ {
54
+ "database": {
55
+ "default": {
56
+ "type": "mysql",
57
+ "host": "localhost",
58
+ "database": "main_db"
59
+ },
60
+ "analytics": {
61
+ "type": "postgres",
62
+ "host": "analytics.example.com",
63
+ "database": "analytics_db"
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Environment Variables
72
+
73
+ For security, **always** use environment variables for sensitive data.
74
+
75
+ **.env file:**
76
+ ```
77
+ DB_HOST=localhost
78
+ DB_USER=myuser
79
+ DB_PASSWORD=mypassword
80
+ ```
81
+
82
+ **config.json:**
83
+ ```json
84
+ {
85
+ "database": {
86
+ "type": "mysql",
87
+ "host": "${DB_HOST}",
88
+ "user": "${DB_USER}",
89
+ "password": "${DB_PASSWORD}"
90
+ }
91
+ }
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Automatic Connection
97
+
98
+ The connection is established automatically when your application starts. You don't need to write any connection code.
99
+
100
+ **Next Step:** Check out [Query Basics](./02-basics.md) to start using your database.
@@ -0,0 +1,136 @@
1
+ # Query Basics
2
+
3
+ ODAC features a powerful, efficient, and "magic" Query Builder. It allows you to interact with your database using simple, chainable methods without writing raw SQL.
4
+
5
+ ## Accessing Tables
6
+
7
+ You can access any table directly as a property of `Odac.DB`.
8
+
9
+ ```javascript
10
+ // Access the 'users' table
11
+ const query = Odac.DB.users;
12
+ ```
13
+
14
+ If you have multiple database connections defined in your config:
15
+
16
+ ```javascript
17
+ // Access 'visits' table on 'analytics' connection
18
+ const visits = Odac.DB.analytics.visits;
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Retrieving Data (Select)
24
+
25
+ ### Fetch All Rows
26
+
27
+ ```javascript
28
+ const users = await Odac.DB.users.select();
29
+ ```
30
+
31
+ ### Fetch a Single Row
32
+
33
+ Use `.first()` to get a single object instead of an array.
34
+
35
+ ```javascript
36
+ const user = await Odac.DB.users.where('id', 1).first();
37
+ ```
38
+
39
+ ### Filtering (Where)
40
+
41
+ ```javascript
42
+ // Simple equals
43
+ const users = await Odac.DB.users.where('email', 'john@example.com').select();
44
+
45
+ // Comparison operators
46
+ const products = await Odac.DB.products.where('price', '>', 100).select();
47
+ const activeUsers = await Odac.DB.users.where('status', '!=', 'banned').select();
48
+
49
+ // OR statements
50
+ const staff = await Odac.DB.users
51
+ .where('role', 'admin')
52
+ .orWhere('role', 'editor')
53
+ .select();
54
+ ```
55
+
56
+ ### Ordering and Limiting
57
+
58
+ ```javascript
59
+ const latestPosts = await Odac.DB.posts
60
+ .orderBy('created_at', 'desc')
61
+ .limit(5);
62
+ ```
63
+
64
+ ### Counting Rows
65
+
66
+ ODAC simplifies counting rows. Unlike standard Knex behavior which might return objects or strings, `count()` directly returns a `Number` for simple queries.
67
+
68
+ ```javascript
69
+ const totalUsers = await Odac.DB.users.count(); // Returns: 150 (Number)
70
+
71
+ const activeAdmins = await Odac.DB.users
72
+ .where('role', 'admin')
73
+ .where('active', true)
74
+ .count(); // Returns: 5 (Number)
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Inserting Data
80
+
81
+ ```javascript
82
+ // Insert a single record
83
+ await Odac.DB.users.insert({
84
+ name: 'John Doe',
85
+ email: 'john@example.com'
86
+ });
87
+
88
+ // Insert multiple records
89
+ await Odac.DB.tags.insert([
90
+ { name: 'javascript' },
91
+ { name: 'nodejs' }
92
+ ]);
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Updating Data
98
+
99
+ ```javascript
100
+ await Odac.DB.users
101
+ .where('id', 1)
102
+ .update({
103
+ status: 'active',
104
+ last_login: new Date()
105
+ });
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Deleting Data
111
+
112
+ await Odac.DB.users.where('id', 1).delete();
113
+ ```
114
+
115
+ ---
116
+
117
+ ## ID Generation (NanoID)
118
+
119
+ ODAC includes a built-in helper for generating robust, unique string IDs (NanoID) without needing external packages. Secure, URL-friendly, and collision-resistant.
120
+
121
+ ```javascript
122
+ // Generate a standard 21-character ID (e.g., "V1StGXR8_Z5jdHi6B-myT")
123
+ const id = Odac.DB.nanoid();
124
+
125
+ // Generate a custom length ID
126
+ const shortId = Odac.DB.nanoid(10);
127
+ ```
128
+
129
+ This is particularly useful when inserting records into tables that use string-based Primary Keys instead of auto-increment integers.
130
+
131
+ ```javascript
132
+ await Odac.DB.posts.insert({
133
+ id: Odac.DB.nanoid(),
134
+ title: 'My First Post'
135
+ });
136
+ ```
@@ -0,0 +1,84 @@
1
+ # Advanced Queries
2
+
3
+ For complex applications, ODAC provides advanced query capabilities like nested constraints, joins, transactions, and raw SQL execution.
4
+
5
+ ## Nested Where Clauses
6
+
7
+ To create complex `AND / OR` logic (like parenthesis in SQL), use a callback function with `.where()` or `.andWhere()`.
8
+
9
+ **Example:**
10
+ `SELECT * FROM users WHERE status = 'active' AND (role = 'admin' OR role = 'editor')`
11
+
12
+ **ODAC Code:**
13
+ ```javascript
14
+ await Odac.DB.users
15
+ .where('status', 'active')
16
+ .andWhere(builder => {
17
+ builder.where('role', 'admin').orWhere('role', 'editor');
18
+ })
19
+ .select();
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Joins
25
+
26
+ You can join multiple tables using `.join()`, `.leftJoin()`, etc.
27
+
28
+ ```javascript
29
+ const posts = await Odac.DB.posts
30
+ .join('users', 'posts.user_id', '=', 'users.id')
31
+ .select('posts.title', 'users.name as author');
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Transactions
37
+
38
+ Transactions allow you to ensure multiple database operations succeed or fail together.
39
+
40
+ ```javascript
41
+ await Odac.DB.transaction(async (trx) => {
42
+
43
+ const [userId] = await trx('users').insert({ name: 'Alice' });
44
+
45
+ await trx('accounts').insert({ user_id: userId, balance: 100 });
46
+
47
+ // If anything throws an error here, both inserts are rolled back.
48
+ });
49
+ ```
50
+
51
+ > **Note:** Use `Odac.DB.connectionName.transaction(...)` for non-default connections.
52
+
53
+ ---
54
+
55
+ ## Raw Queries & Values
56
+
57
+ ### Raw SQL Execution
58
+ If you need to execute a completely raw SQL query:
59
+
60
+ ```javascript
61
+ const result = await Odac.DB.run('SELECT email FROM users WHERE id = ?', [1]);
62
+ ```
63
+
64
+ ### Raw Values in Updates
65
+ Sometimes you need to call SQL functions (like `NOW()` or `COUNT()`) inside an update or insert.
66
+
67
+ ```javascript
68
+ await Odac.DB.users.where('id', 1).update({
69
+ updated_at: Odac.DB.raw('NOW()'),
70
+ visits: Odac.DB.raw('visits + 1')
71
+ });
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Safe Table Access
77
+
78
+ If your table name conflicts with a reserved ODAC method (e.g., `transaction`, `schema`, `run`), use the `.table()` method to access it safely.
79
+
80
+ ```javascript
81
+ // Access a table named 'transaction'
82
+ const logs = await Odac.DB.table('transaction').select();
83
+ ```
84
+
@@ -0,0 +1,48 @@
1
+ # Code-First Migrations
2
+
3
+ Migration files are great, but sometimes (especially in rapid development or zero-config apps) you want dependencies to define their own table structures automatically.
4
+
5
+ ODAC uses the `.schema()` helper for this logic.
6
+
7
+ ## Ensuring Tables Exist
8
+
9
+ The `.schema()` method checks if a table exists. If it **does not exist**, it runs the provided callback to create it. If it **already exists**, it does nothing.
10
+
11
+ ```javascript
12
+ // Ensure 'products' table exists on the fly
13
+ await Odac.DB.products.schema(t => {
14
+ t.increments('id');
15
+ t.string('name').notNullable();
16
+ t.decimal('price', 10, 2);
17
+ t.boolean('is_active').defaultTo(true);
18
+
19
+ // Automatic timestamps (created_at, updated_at)
20
+ t.timestamps(true, true);
21
+ });
22
+ ```
23
+
24
+ The `t` argument is a Schema Builder. You can define columns using standard types like:
25
+ - `t.string()`
26
+ - `t.integer()`
27
+ - `t.boolean()`
28
+ - `t.text()`
29
+ - `t.date()`
30
+ - `t.json()`
31
+
32
+ ## Usage Example
33
+
34
+ A typical pattern is to define schemas in your module's initialization or before the first insert.
35
+
36
+ ```javascript
37
+ // In your controller or module
38
+ async function init() {
39
+ await Odac.DB.logs.schema(t => {
40
+ t.string('level');
41
+ t.text('message');
42
+ t.timestamps();
43
+ });
44
+ }
45
+
46
+ // Later...
47
+ await Odac.DB.logs.insert({ level: 'info', message: 'App started' });
48
+ ```
@@ -70,6 +70,7 @@ if (await validator.error()) {
70
70
  - `in:substring` - Must contain substring
71
71
  - `notin:substring` - Must not contain substring
72
72
  - `regex:pattern` - Must match regex pattern
73
+ - `!disposable` - Block disposable/temporary email providers (List is automatically updated daily)
73
74
 
74
75
  **Security:**
75
76
  - `xss` - Check for HTML tags (XSS protection)
@@ -120,11 +120,18 @@ Make sure your `config.json` has the auth configuration:
120
120
  "auth": {
121
121
  "table": "users",
122
122
  "key": "id",
123
- "token": "user_tokens"
123
+ "token": "user_tokens",
124
+ "idType": "nanoid" // Options: "nanoid" (default, string) or "int" (auto-increment)
124
125
  }
125
126
  }
126
127
  ```
127
128
 
129
+ ### ID Generation Strategy
130
+ ODAC automatically detects your preferred ID strategy:
131
+ 1. **NanoID (Default)**: Generates secure, URL-friendly 21-character string IDs. Recommended for modern apps.
132
+ 2. **Auto-Increment**: If your database table uses `INTEGER` or `SERIAL` primary keys, ODAC detects this and lets the database handle ID generation.
133
+ 3. **Manual Override**: You can force a specific behavior using the `idType` config setting.
134
+
128
135
  ## Security Notes
129
136
 
130
137
  - Passwords are automatically hashed with bcrypt before storage