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.
- package/.github/workflows/auto-pr-description.yml +3 -1
- package/CHANGELOG.md +127 -0
- package/README.md +39 -36
- package/bin/odac.js +1 -31
- package/client/odac.js +871 -994
- package/docs/backend/01-overview/03-development-server.md +7 -7
- package/docs/backend/02-structure/01-typical-project-layout.md +1 -0
- package/docs/backend/03-config/00-configuration-overview.md +9 -0
- package/docs/backend/03-config/01-database-connection.md +1 -1
- package/docs/backend/04-routing/02-controller-less-view-routes.md +9 -3
- package/docs/backend/04-routing/09-websocket.md +29 -0
- package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +2 -0
- package/docs/backend/05-controllers/03-controller-classes.md +27 -41
- package/docs/backend/05-forms/01-custom-forms.md +103 -95
- package/docs/backend/05-forms/02-automatic-database-insert.md +21 -21
- package/docs/backend/07-views/02-rendering-a-view.md +1 -1
- package/docs/backend/07-views/03-variables.md +5 -5
- package/docs/backend/07-views/04-request-data.md +1 -1
- package/docs/backend/07-views/08-backend-javascript.md +1 -1
- package/docs/backend/08-database/01-getting-started.md +100 -0
- package/docs/backend/08-database/02-basics.md +136 -0
- package/docs/backend/08-database/03-advanced.md +84 -0
- package/docs/backend/08-database/04-migrations.md +48 -0
- package/docs/backend/09-validation/01-the-validator-service.md +1 -0
- package/docs/backend/10-authentication/03-register.md +8 -1
- package/docs/backend/10-authentication/04-odac-register-forms.md +46 -46
- package/docs/backend/10-authentication/05-session-management.md +1 -1
- package/docs/backend/10-authentication/06-odac-login-forms.md +48 -48
- package/docs/backend/10-authentication/07-magic-links.md +134 -0
- package/docs/backend/11-mail/01-the-mail-service.md +118 -28
- package/docs/backend/12-streaming/01-streaming-overview.md +2 -2
- package/docs/backend/13-utilities/01-odac-var.md +7 -7
- package/docs/backend/13-utilities/02-ipc.md +73 -0
- package/docs/frontend/01-overview/01-introduction.md +5 -1
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +1 -1
- package/docs/index.json +16 -124
- package/eslint.config.mjs +5 -47
- package/package.json +9 -4
- package/src/Auth.js +362 -104
- package/src/Config.js +7 -2
- package/src/Database.js +188 -0
- package/src/Ipc.js +330 -0
- package/src/Mail.js +408 -37
- package/src/Odac.js +65 -9
- package/src/Request.js +70 -48
- package/src/Route/Cron.js +4 -1
- package/src/Route/Internal.js +214 -11
- package/src/Route/Middleware.js +7 -2
- package/src/Route.js +106 -26
- package/src/Server.js +80 -11
- package/src/Storage.js +165 -0
- package/src/Validator.js +94 -2
- package/src/View/Form.js +193 -17
- package/src/View.js +46 -1
- package/src/WebSocket.js +18 -3
- package/template/config.json +1 -1
- package/template/route/www.js +12 -10
- package/test/core/{Candy.test.js → Odac.test.js} +2 -2
- package/docs/backend/08-database/01-database-connection.md +0 -99
- package/docs/backend/08-database/02-using-mysql.md +0 -322
- 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:
|
|
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.
|
|
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
|
|
120
|
-
const fieldTagMatch = html.match(/<odac:
|
|
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:
|
|
222
|
-
const field = this.
|
|
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, '"')}"`
|
|
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, '"')}"`
|
|
364
407
|
if (errorMessages.email) attrs += ` data-error-email="${errorMessages.email.replace(/"/g, '"')}"`
|
|
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:
|
|
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.
|
|
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:
|
|
439
|
-
const field = this.
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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:
|
|
567
|
-
const field = this.
|
|
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(
|
|
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
|
-
|
|
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)
|
package/template/config.json
CHANGED
package/template/route/www.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
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
|
-
```
|