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
package/src/View/Form.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const nodeCrypto = require('crypto')
2
2
 
3
3
  class Form {
4
- static FORM_TYPES = ['register', 'login', 'form']
4
+ static FORM_TYPES = ['register', 'login', 'magic-login', 'form']
5
5
 
6
6
  static parse(content, Odac) {
7
7
  for (const type of this.FORM_TYPES) {
@@ -33,6 +33,8 @@ class Form {
33
33
  return this.extractRegisterConfig(html, formToken)
34
34
  } else if (type === 'login') {
35
35
  return this.extractLoginConfig(html, formToken)
36
+ } else if (type === 'magic-login') {
37
+ return this.extractMagicLoginConfig(html, formToken)
36
38
  } else if (type === 'form') {
37
39
  return this.extractFormConfig(html, formToken)
38
40
  }
@@ -43,6 +45,8 @@ class Form {
43
45
  this.storeRegisterConfig(token, config, Odac)
44
46
  } else if (type === 'login') {
45
47
  this.storeLoginConfig(token, config, Odac)
48
+ } else if (type === 'magic-login') {
49
+ this.storeMagicLoginConfig(token, config, Odac)
46
50
  } else if (type === 'form') {
47
51
  this.storeFormConfig(token, config, Odac)
48
52
  }
@@ -53,6 +57,8 @@ class Form {
53
57
  return this.generateRegisterForm(originalHtml, config, formToken)
54
58
  } else if (type === 'login') {
55
59
  return this.generateLoginForm(originalHtml, config, formToken)
60
+ } else if (type === 'magic-login') {
61
+ return this.generateMagicLoginForm(originalHtml, config, formToken)
56
62
  } else if (type === 'form') {
57
63
  return this.generateCustomForm(originalHtml, config, formToken)
58
64
  }
@@ -97,10 +103,10 @@ class Form {
97
103
  if (idMatch) config.submitId = idMatch[1]
98
104
  }
99
105
 
100
- const fieldMatches = html.match(/<odac:field[\s\S]*?<\/odac:field>/g)
106
+ const fieldMatches = html.match(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g)
101
107
  if (fieldMatches) {
102
108
  for (const fieldHtml of fieldMatches) {
103
- const field = this.parseField(fieldHtml)
109
+ const field = this.parseInput(fieldHtml)
104
110
  if (field) config.fields.push(field)
105
111
  }
106
112
  }
@@ -116,8 +122,8 @@ class Form {
116
122
  return config
117
123
  }
118
124
 
119
- static parseField(html) {
120
- const fieldTagMatch = html.match(/<odac:field([^>]*?)(?:\/>|>)/)
125
+ static parseInput(html) {
126
+ const fieldTagMatch = html.match(/<odac:input([^>]*?)(?:\/>|>)/)
121
127
  if (!fieldTagMatch) return null
122
128
 
123
129
  const fieldTag = fieldTagMatch[0]
@@ -167,6 +173,26 @@ class Form {
167
173
  }
168
174
  }
169
175
 
176
+ // Capture generic attributes
177
+ const extraAttrs = {}
178
+ const knownAttrs = ['name', 'type', 'placeholder', 'label', 'class', 'id', 'unique', 'skip']
179
+ const attrRegex = /(\w+)(?:=(["'])((?:(?!\2).)*)\2|=([^\s>]+))?/g
180
+ let attrMatch
181
+ // Clean tag to just attributes part for safer regex matching if needed,
182
+ // or just run on fieldTag from start
183
+ const attributesString = fieldTag.replace(/^<odac:input/, '').replace(/\/?>$/, '')
184
+
185
+ while ((attrMatch = attrRegex.exec(attributesString))) {
186
+ const key = attrMatch[1]
187
+ // If value is undefined, it's a boolean attribute (e.g. required, autofocus) -> set as true (or empty string)
188
+ const value = attrMatch[3] !== undefined ? attrMatch[3] : attrMatch[4] !== undefined ? attrMatch[4] : ''
189
+
190
+ if (!knownAttrs.includes(key)) {
191
+ extraAttrs[key] = value
192
+ }
193
+ }
194
+ field.extraAttributes = extraAttrs
195
+
170
196
  return field
171
197
  }
172
198
 
@@ -218,8 +244,8 @@ class Form {
218
244
 
219
245
  let innerContent = originalHtml.replace(/<odac:register[^>]*>/, '').replace(/<\/odac:register>/, '')
220
246
 
221
- innerContent = innerContent.replace(/<odac:field[\s\S]*?<\/odac:field>/g, fieldMatch => {
222
- const field = this.parseField(fieldMatch)
247
+ innerContent = innerContent.replace(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g, fieldMatch => {
248
+ const field = this.parseInput(fieldMatch)
223
249
  if (!field) return fieldMatch
224
250
  return this.generateFieldHtml(field)
225
251
  })
@@ -277,6 +303,23 @@ class Form {
277
303
  return html
278
304
  }
279
305
 
306
+ static appendExtraAttributes(attrs, field) {
307
+ if (field.extraAttributes) {
308
+ for (const key in field.extraAttributes) {
309
+ const val = field.extraAttributes[key]
310
+ // If val is empty string, render as boolean attribute if typical, or key=""
311
+ // For HTML5 boolean attrs like autofocus, required, checked, readonly, disabled, multiple, selected
312
+ // presence is enough.
313
+ if (val === '') {
314
+ attrs += ` ${key}`
315
+ } else {
316
+ attrs += ` ${key}="${val.replace(/"/g, '&quot;')}"`
317
+ }
318
+ }
319
+ }
320
+ return attrs
321
+ }
322
+
280
323
  static buildHtml5Attributes(field) {
281
324
  let attrs = ''
282
325
  const html5Rules = {
@@ -363,6 +406,8 @@ class Form {
363
406
  if (errorMessages.pattern) attrs += ` data-error-pattern="${errorMessages.pattern.replace(/"/g, '&quot;')}"`
364
407
  if (errorMessages.email) attrs += ` data-error-email="${errorMessages.email.replace(/"/g, '&quot;')}"`
365
408
 
409
+ attrs = this.appendExtraAttributes(attrs, field)
410
+
366
411
  return attrs
367
412
  }
368
413
 
@@ -401,10 +446,10 @@ class Form {
401
446
  if (idMatch) config.submitId = idMatch[1]
402
447
  }
403
448
 
404
- const fieldMatches = html.match(/<odac:field[\s\S]*?<\/odac:field>/g)
449
+ const fieldMatches = html.match(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g)
405
450
  if (fieldMatches) {
406
451
  for (const fieldHtml of fieldMatches) {
407
- const field = this.parseField(fieldHtml)
452
+ const field = this.parseInput(fieldHtml)
408
453
  if (field) config.fields.push(field)
409
454
  }
410
455
  }
@@ -435,8 +480,8 @@ class Form {
435
480
 
436
481
  let innerContent = originalHtml.replace(/<odac:login[^>]*>/, '').replace(/<\/odac:login>/, '')
437
482
 
438
- innerContent = innerContent.replace(/<odac:field[\s\S]*?<\/odac:field>/g, fieldMatch => {
439
- const field = this.parseField(fieldMatch)
483
+ innerContent = innerContent.replace(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g, fieldMatch => {
484
+ const field = this.parseInput(fieldMatch)
440
485
  if (!field) return fieldMatch
441
486
  return this.generateFieldHtml(field)
442
487
  })
@@ -519,10 +564,10 @@ class Form {
519
564
  if (idMatch) config.submitId = idMatch[1]
520
565
  }
521
566
 
522
- const fieldMatches = html.match(/<odac:field[\s\S]*?<\/odac:field>/g)
567
+ const fieldMatches = html.match(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g)
523
568
  if (fieldMatches) {
524
569
  for (const fieldHtml of fieldMatches) {
525
- const field = this.parseField(fieldHtml)
570
+ const field = this.parseInput(fieldHtml)
526
571
  if (field) config.fields.push(field)
527
572
  }
528
573
  }
@@ -558,13 +603,14 @@ class Form {
558
603
  static generateCustomForm(originalHtml, config, formToken) {
559
604
  const submitText = config.submitText || 'Submit'
560
605
  const submitLoading = config.submitLoading || 'Processing...'
561
- const action = config.action || '/_odac/form'
606
+ // Always post to internal handler, real action is in session config
607
+ const formAction = '/_odac/form'
562
608
  const method = config.method || 'POST'
563
609
 
564
610
  let innerContent = originalHtml.replace(/<odac:form[^>]*>/, '').replace(/<\/odac:form>/, '')
565
611
 
566
- innerContent = innerContent.replace(/<odac:field[\s\S]*?<\/odac:field>/g, fieldMatch => {
567
- const field = this.parseField(fieldMatch)
612
+ innerContent = innerContent.replace(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g, fieldMatch => {
613
+ const field = this.parseInput(fieldMatch)
568
614
  if (!field) return fieldMatch
569
615
  return this.generateFieldHtml(field)
570
616
  })
@@ -584,7 +630,7 @@ class Form {
584
630
 
585
631
  innerContent = innerContent.replace(/<odac:set[^>]*\/?>/g, '')
586
632
 
587
- let formAttrs = `class="odac-custom-form${config.class ? ' ' + escapeHtml(config.class) : ''}" data-odac-form="${escapeHtml(formToken)}" method="${escapeHtml(method)}" action="${escapeHtml(action)}" novalidate`
633
+ let formAttrs = `class="odac-custom-form${config.class ? ' ' + escapeHtml(config.class) : ''}" data-odac-form="${escapeHtml(formToken)}" method="${escapeHtml(method)}" action="${escapeHtml(formAction)}" novalidate`
588
634
  if (config.id) formAttrs += ` id="${escapeHtml(config.id)}"`
589
635
 
590
636
  let html = `<form ${formAttrs}>\n`
@@ -595,6 +641,136 @@ class Form {
595
641
 
596
642
  return html
597
643
  }
644
+
645
+ static extractMagicLoginConfig(html, formToken) {
646
+ const config = {
647
+ token: formToken,
648
+ redirect: null,
649
+ submitText: 'Send Magic Link',
650
+ submitLoading: 'Sending...',
651
+ fields: []
652
+ }
653
+
654
+ const tagMatch = html.match(/<odac:magic-login([^>]*)>/)
655
+ if (!tagMatch) return config
656
+
657
+ const tag = tagMatch[0]
658
+ const redirectMatch = tag.match(/redirect=["']([^"']+)["']/)
659
+ const emailLabelMatch = tag.match(/email-label=["']([^"']+)["']/)
660
+
661
+ if (redirectMatch) config.redirect = redirectMatch[1]
662
+
663
+ // Auto-add email field if not manually specified (simplified usage)
664
+ const fieldMatches = html.match(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g)
665
+
666
+ if (fieldMatches) {
667
+ // Custom fields included
668
+ for (const fieldHtml of fieldMatches) {
669
+ const field = this.parseInput(fieldHtml)
670
+ if (field) config.fields.push(field)
671
+ }
672
+ } else {
673
+ // Default Email Field
674
+ config.fields.push({
675
+ name: 'email',
676
+ type: 'email',
677
+ placeholder: 'e.g. user@example.com',
678
+ label: emailLabelMatch ? emailLabelMatch[1] : 'Email Address',
679
+ class: '',
680
+ id: null,
681
+ unique: false,
682
+ skip: false,
683
+ validations: [
684
+ {rule: 'required', message: 'Email is required'},
685
+ {rule: 'email', message: 'Invalid email format'}
686
+ ]
687
+ })
688
+ }
689
+
690
+ const submitMatch = html.match(/<odac:submit([^>/]*)(?:\/?>|>(.*?)<\/odac:submit>)/)
691
+ if (submitMatch) {
692
+ const submitTag = submitMatch[1]
693
+ const textMatch = submitTag.match(/text=["']([^"']+)["']/)
694
+ const loadingMatch = submitTag.match(/loading=["']([^"']+)["']/)
695
+ const classMatch = submitTag.match(/class=["']([^"']+)["']/)
696
+ const styleMatch = submitTag.match(/style=["']([^"']+)["']/)
697
+ const idMatch = submitTag.match(/id=["']([^"']+)["']/)
698
+
699
+ if (textMatch) config.submitText = textMatch[1]
700
+ else if (submitMatch[2]) config.submitText = submitMatch[2].trim()
701
+
702
+ if (loadingMatch) config.submitLoading = loadingMatch[1]
703
+ if (classMatch) config.submitClass = classMatch[1]
704
+ if (styleMatch) config.submitStyle = styleMatch[1]
705
+ if (idMatch) config.submitId = idMatch[1]
706
+ } else {
707
+ // Check for submit-text attribute on main tag if no submit tag
708
+ const submitTextAttr = tag.match(/submit-text=["']([^"']+)["']/)
709
+ if (submitTextAttr) config.submitText = submitTextAttr[1]
710
+ }
711
+
712
+ return config
713
+ }
714
+
715
+ static storeMagicLoginConfig(token, config, Odac) {
716
+ if (!Odac.View) Odac.View = {}
717
+ if (!Odac.View.magicLoginForms) Odac.View.magicLoginForms = {}
718
+
719
+ const formData = {
720
+ config: config,
721
+ created: Date.now(),
722
+ expires: Date.now() + 30 * 60 * 1000,
723
+ sessionId: Odac.Request.session('_client'),
724
+ userAgent: Odac.Request.header('user-agent'),
725
+ ip: Odac.Request.ip
726
+ }
727
+
728
+ Odac.View.magicLoginForms[token] = formData
729
+ Odac.Request.session(`_magic_login_form_${token}`, formData)
730
+ }
731
+
732
+ static generateMagicLoginForm(originalHtml, config, formToken) {
733
+ const submitText = config.submitText || 'Send Magic Link'
734
+ const submitLoading = config.submitLoading || 'Sending...'
735
+
736
+ let innerContent = originalHtml.replace(/<odac:magic-login[^>]*>/, '').replace(/<\/odac:magic-login>/, '')
737
+
738
+ // If no custom fields were present in HTML but we added default email in config
739
+ if (!originalHtml.includes('<odac:input')) {
740
+ const emailField = config.fields.find(f => f.name === 'email')
741
+ if (emailField) {
742
+ innerContent += this.generateFieldHtml(emailField)
743
+ }
744
+ } else {
745
+ innerContent = innerContent.replace(/<odac:input([^>]*?)(?:\/>|>(?:[\s\S]*?)<\/odac:input>)/g, fieldMatch => {
746
+ const field = this.parseInput(fieldMatch)
747
+ if (!field) return fieldMatch
748
+ return this.generateFieldHtml(field)
749
+ })
750
+ }
751
+
752
+ const submitMatch = innerContent.match(/<odac:submit[\s\S]*?(?:<\/odac:submit>|\/?>)/)
753
+ if (submitMatch) {
754
+ let submitAttrs = `type="submit" data-submit-text="${submitText}" data-loading-text="${submitLoading}"`
755
+ if (config.submitClass) submitAttrs += ` class="${config.submitClass}"`
756
+ if (config.submitStyle) submitAttrs += ` style="${config.submitStyle}"`
757
+ if (config.submitId) submitAttrs += ` id="${config.submitId}"`
758
+ const submitButton = `<button ${submitAttrs}>${submitText}</button>`
759
+ innerContent = innerContent.replace(submitMatch[0], submitButton)
760
+ } else if (!innerContent.includes('type="submit"')) {
761
+ // Auto add submit button if missing
762
+ const submitButton = `<button type="submit" data-submit-text="${submitText}" data-loading-text="${submitLoading}">${submitText}</button>`
763
+ innerContent += `\n${submitButton}`
764
+ }
765
+
766
+ let html = `<form class="odac-magic-login-form" data-odac-magic-login="${formToken}" method="POST" action="/_odac/magic-login" novalidate>\n`
767
+ html += ` <input type="hidden" name="_odac_magic_login_token" value="${formToken}">\n`
768
+ html += innerContent
769
+ html += `\n <span class="odac-form-success" style="display:none;"></span>\n`
770
+ html += `</form>`
771
+
772
+ return html
773
+ }
598
774
  }
599
775
 
600
776
  module.exports = Form
package/src/View.js CHANGED
@@ -3,6 +3,8 @@ const fs = require('fs')
3
3
  const Form = require('./View/Form')
4
4
  const EarlyHints = require('./View/EarlyHints')
5
5
 
6
+ const TITLE_REGEX = /<title[^>]*>([^<]*)<\/title>/i
7
+
6
8
  const CACHE_DIR = './storage/.cache'
7
9
 
8
10
  class View {
@@ -140,12 +142,45 @@ class View {
140
142
  }
141
143
 
142
144
  // Render requested elements
145
+ let title = null
143
146
  for (let element of this.#odac.Request.ajaxLoad) {
144
147
  if (this.#part[element]) {
145
148
  let viewPath = this.#part[element]
146
149
  if (viewPath.includes('.')) viewPath = viewPath.replace(/\./g, '/')
147
150
  if (fs.existsSync(`./view/${element}/${viewPath}.html`)) {
148
- output[element] = await this.#render(`./view/${element}/${viewPath}.html`)
151
+ const html = await this.#render(`./view/${element}/${viewPath}.html`)
152
+ output[element] = html
153
+
154
+ // Extract title if present inside the part
155
+ const titleMatch = html.match(TITLE_REGEX)
156
+ if (titleMatch && titleMatch[1]) {
157
+ title = titleMatch[1]
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ // If title not found in parts, try to extract from 'head', 'header' or 'meta' parts
164
+ if (!title) {
165
+ const priorityParts = ['head', 'header', 'meta']
166
+ for (const key of priorityParts) {
167
+ if (this.#part[key] && !this.#odac.Request.ajaxLoad.includes(key)) {
168
+ let viewPath = this.#part[key]
169
+ if (viewPath.includes('.')) viewPath = viewPath.replace(/\./g, '/')
170
+ if (fs.existsSync(`./view/${key}/${viewPath}.html`)) {
171
+ try {
172
+ const partHtml = await this.#render(`./view/${key}/${viewPath}.html`)
173
+ const titleMatch = partHtml.match(TITLE_REGEX)
174
+ if (titleMatch && titleMatch[1]) {
175
+ title = titleMatch[1]
176
+ break
177
+ }
178
+ } catch (e) {
179
+ if (this.#odac.Config?.debug) {
180
+ console.warn(`Odac: Failed to render part '${key}' while searching for title:`, e)
181
+ }
182
+ }
183
+ }
149
184
  }
150
185
  }
151
186
  }
@@ -161,6 +196,8 @@ class View {
161
196
  this.#odac.Request.end({
162
197
  output: output,
163
198
  variables: variables,
199
+ data: this.#odac.Request.sharedData,
200
+ title: title,
164
201
  skeletonChanged: skeletonChanged
165
202
  })
166
203
  return
@@ -207,6 +244,14 @@ class View {
207
244
  this.#earlyHints.cacheHints(routePath, detectedResources)
208
245
  }
209
246
  }
247
+
248
+ // Inject Shared Data
249
+ const sharedScript = `<script type="application/json" id="odac-data">${JSON.stringify(this.#odac.Request.sharedData || {})}</script>`
250
+ if (result.includes('</body>')) {
251
+ result = result.replace('</body>', `${sharedScript}</body>`)
252
+ } else {
253
+ result += sharedScript
254
+ }
210
255
  }
211
256
 
212
257
  this.#odac.Request.header('Content-Type', 'text/html')
package/src/WebSocket.js CHANGED
@@ -22,11 +22,16 @@ class WebSocketClient {
22
22
 
23
23
  constructor(socket, server, id) {
24
24
  this.#socket = socket
25
+ this.#socket.pause()
25
26
  this.#server = server
26
27
  this.#id = id
27
28
  this.#setupListeners()
28
29
  }
29
30
 
31
+ resume() {
32
+ this.#socket.resume()
33
+ }
34
+
30
35
  get id() {
31
36
  return this.#id
32
37
  }
@@ -129,6 +134,7 @@ class WebSocketClient {
129
134
  break
130
135
  case OPCODE.PING:
131
136
  this.#sendFrame(OPCODE.PONG, frame.payload)
137
+ this.#emit('ping', frame.payload)
132
138
  break
133
139
  case OPCODE.PONG:
134
140
  this.#emit('pong')
@@ -152,6 +158,8 @@ class WebSocketClient {
152
158
  if (this.#closed) return
153
159
  this.#closed = true
154
160
 
161
+ this.#socket.removeAllListeners()
162
+
155
163
  for (const room of this.#rooms) {
156
164
  this.#server.leaveRoom(this.#id, room)
157
165
  }
@@ -239,6 +247,7 @@ class WebSocketClient {
239
247
 
240
248
  this.#sendFrame(OPCODE.CLOSE, payload)
241
249
  this.#socket.end()
250
+ this.#socket.removeAllListeners()
242
251
 
243
252
  for (const room of this.#rooms) {
244
253
  this.#server.leaveRoom(this.#id, room)
@@ -330,13 +339,19 @@ class WebSocketServer {
330
339
  'HTTP/1.1 101 Switching Protocols',
331
340
  'Upgrade: websocket',
332
341
  'Connection: Upgrade',
333
- `Sec-WebSocket-Accept: ${acceptKey}`,
334
- '',
335
- ''
342
+ `Sec-WebSocket-Accept: ${acceptKey}`
336
343
  ]
337
344
 
345
+ if (req.headers['sec-websocket-protocol']) {
346
+ responseHeaders.push(`Sec-WebSocket-Protocol: ${req.headers['sec-websocket-protocol']}`)
347
+ }
348
+
349
+ responseHeaders.push('', '')
350
+
338
351
  socket.write(responseHeaders.join('\r\n'))
339
352
 
353
+ if (head && head.length > 0) socket.unshift(head)
354
+
340
355
  const clientId = nodeCrypto.randomUUID()
341
356
  const client = new WebSocketClient(socket, this, clientId)
342
357
  this.#clients.set(clientId, client)
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "route": {
3
- "/assets/js/odac.js": "${odac}/framework/web/odac.js"
3
+ "/assets/js/odac.js": "${odac}/odac.js"
4
4
  }
5
5
  }
@@ -5,15 +5,17 @@
5
5
  // Controllers are located in controller/page/ directory
6
6
 
7
7
  // Home page - displays welcome message, features, and interactive demos
8
- Odac.Route.page('/', 'index')
8
+ module.exports = function (Odac) {
9
+ Odac.Route.page('/', 'index')
9
10
 
10
- // About page - provides information about Odac
11
- Odac.Route.page('/about', 'about')
11
+ // About page - provides information about Odac
12
+ Odac.Route.page('/about', 'about')
12
13
 
13
- // ============================================
14
- // API Routes
15
- // ============================================
16
- // Add your API routes here
17
- // Example:
18
- // Odac.Route.post('/api/contact', 'contact')
19
- // Odac.Route.get('/api/data', 'data')
14
+ // ============================================
15
+ // API Routes
16
+ // ============================================
17
+ // Add your API routes here
18
+ // Example:
19
+ // Odac.Route.post('/api/contact', 'contact')
20
+ // Odac.Route.get('/api/data', 'data')
21
+ }
@@ -34,13 +34,13 @@ describe('Odac', () => {
34
34
  })
35
35
 
36
36
  it('should not recreate global if already exists', () => {
37
- const existingCandy = global.Odac
37
+ const existingOdac = global.Odac
38
38
 
39
39
  // Clear module cache and re-require
40
40
  delete require.cache[require.resolve('../../core/Odac.js')]
41
41
  require('../../core/Odac.js')
42
42
 
43
- expect(global.Odac).toBe(existingCandy)
43
+ expect(global.Odac).toBe(existingOdac)
44
44
  })
45
45
  })
46
46
 
@@ -1,99 +0,0 @@
1
- ## 🔌 Database Connection
2
-
3
- Odac automatically connects to your MySQL database when you provide the configuration.
4
-
5
- ### Configuration
6
-
7
- Add your database credentials to `config.json`:
8
-
9
- ```json
10
- {
11
- "database": {
12
- "host": "localhost",
13
- "user": "your_username",
14
- "password": "your_password",
15
- "database": "your_database_name"
16
- }
17
- }
18
- ```
19
-
20
- ### Multiple Databases
21
-
22
- You can configure multiple database connections:
23
-
24
- ```json
25
- {
26
- "database": {
27
- "default": {
28
- "host": "localhost",
29
- "user": "user1",
30
- "password": "pass1",
31
- "database": "main_db"
32
- },
33
- "analytics": {
34
- "host": "analytics.example.com",
35
- "user": "user2",
36
- "password": "pass2",
37
- "database": "analytics_db"
38
- }
39
- }
40
- }
41
- ```
42
-
43
- Access different databases:
44
-
45
- ```javascript
46
- // Default database
47
- const users = await Odac.Mysql.table('users').get()
48
-
49
- // Specific database
50
- const stats = await Odac.Mysql.database('analytics').table('stats').get()
51
- ```
52
-
53
- ### Environment Variables
54
-
55
- For security, use environment variables for sensitive data:
56
-
57
- **.env file:**
58
- ```
59
- DB_HOST=localhost
60
- DB_USER=myuser
61
- DB_PASSWORD=mypassword
62
- DB_NAME=mydatabase
63
- ```
64
-
65
- **config.json:**
66
- ```json
67
- {
68
- "database": {
69
- "host": "${DB_HOST}",
70
- "user": "${DB_USER}",
71
- "password": "${DB_PASSWORD}",
72
- "database": "${DB_NAME}"
73
- }
74
- }
75
- ```
76
-
77
- ### Connection Options
78
-
79
- Available configuration options:
80
-
81
- - `host` - Database server hostname (default: `localhost`)
82
- - `user` - Database username
83
- - `password` - Database password
84
- - `database` - Database name
85
- - `type` - Database type (currently only `mysql` is supported)
86
-
87
- ### Automatic Connection
88
-
89
- The connection is established automatically when your application starts. You don't need to write any connection code - just use `Odac.Mysql` in your controllers.
90
-
91
- ```javascript
92
- module.exports = async function (Odac) {
93
- // Connection is already established
94
- const users = await Odac.Mysql.table('users').get()
95
-
96
- Odac.set('users', users)
97
- Odac.View.set({ skeleton: 'main', content: 'users' })
98
- }
99
- ```