odac 0.9.0 → 1.0.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 +0 -2
- package/.github/workflows/codeql.yml +46 -0
- package/.github/workflows/release.yml +13 -6
- package/.github/workflows/test-coverage.yml +10 -9
- package/.releaserc.js +9 -6
- package/CHANGELOG.md +62 -150
- package/CODE_OF_CONDUCT.md +1 -1
- package/CONTRIBUTING.md +8 -8
- package/LICENSE +21 -661
- package/README.md +12 -12
- package/SECURITY.md +4 -4
- package/bin/odac.js +101 -0
- package/{framework/web/candy.js → client/odac.js} +310 -44
- package/docs/backend/01-overview/{01-whats-in-the-candy-box.md → 01-whats-in-the-odac-box.md} +4 -2
- package/docs/backend/01-overview/02-super-handy-helper-functions.md +29 -1
- package/docs/backend/01-overview/03-development-server.md +11 -11
- package/docs/backend/02-structure/01-typical-project-layout.md +4 -4
- package/docs/backend/03-config/00-configuration-overview.md +6 -6
- package/docs/backend/03-config/01-database-connection.md +1 -1
- package/docs/backend/03-config/02-static-route-mapping-optional.md +4 -4
- package/docs/backend/03-config/04-environment-variables.md +20 -20
- package/docs/backend/03-config/05-early-hints.md +4 -4
- package/docs/backend/04-routing/01-basic-page-routes.md +4 -4
- package/docs/backend/04-routing/02-controller-less-view-routes.md +5 -5
- package/docs/backend/04-routing/03-api-and-data-routes.md +3 -3
- package/docs/backend/04-routing/04-authentication-aware-routes.md +5 -5
- package/docs/backend/04-routing/05-advanced-routing.md +3 -3
- package/docs/backend/04-routing/06-error-pages.md +17 -17
- package/docs/backend/04-routing/07-cron-jobs.md +13 -13
- package/docs/backend/04-routing/08-middleware.md +214 -0
- package/docs/backend/04-routing/09-websocket-auth-middleware.md +292 -0
- package/docs/backend/04-routing/09-websocket-examples.md +381 -0
- package/docs/backend/04-routing/09-websocket-quick-reference.md +211 -0
- package/docs/backend/04-routing/09-websocket.md +298 -0
- package/docs/backend/05-controllers/01-how-to-build-a-controller.md +3 -3
- package/docs/backend/05-controllers/02-your-trusty-odac-assistant.md +41 -0
- package/docs/backend/05-controllers/03-controller-classes.md +19 -19
- package/docs/backend/05-forms/01-custom-forms.md +114 -114
- package/docs/backend/05-forms/02-automatic-database-insert.md +82 -82
- package/docs/backend/06-request-and-response/01-the-request-object-what-is-the-user-asking-for.md +26 -26
- package/docs/backend/06-request-and-response/02-sending-a-response-replying-to-the-user.md +10 -10
- package/docs/backend/07-views/01-the-view-directory.md +1 -1
- package/docs/backend/07-views/02-rendering-a-view.md +22 -22
- package/docs/backend/07-views/03-template-syntax.md +52 -52
- package/docs/backend/07-views/03-variables.md +84 -84
- package/docs/backend/07-views/04-request-data.md +57 -57
- package/docs/backend/07-views/05-conditionals.md +78 -78
- package/docs/backend/07-views/06-loops.md +114 -114
- package/docs/backend/07-views/07-translations.md +66 -66
- package/docs/backend/07-views/08-backend-javascript.md +103 -103
- package/docs/backend/07-views/09-comments.md +71 -71
- package/docs/backend/08-database/01-database-connection.md +8 -8
- package/docs/backend/08-database/02-using-mysql.md +49 -49
- package/docs/backend/09-validation/01-the-validator-service.md +38 -38
- package/docs/backend/10-authentication/01-user-logins-with-authjs.md +15 -15
- package/docs/backend/10-authentication/02-foiling-villains-with-csrf-protection.md +10 -10
- package/docs/backend/10-authentication/03-register.md +12 -12
- package/docs/backend/10-authentication/{04-candy-register-forms.md → 04-odac-register-forms.md} +141 -141
- package/docs/backend/10-authentication/05-session-management.md +10 -10
- package/docs/backend/10-authentication/{06-candy-login-forms.md → 06-odac-login-forms.md} +125 -125
- package/docs/backend/11-mail/01-the-mail-service.md +5 -5
- package/docs/backend/12-streaming/01-streaming-overview.md +96 -54
- package/docs/backend/13-utilities/{01-candy-var.md → 01-odac-var.md} +109 -109
- package/docs/frontend/01-overview/01-introduction.md +30 -30
- package/docs/frontend/02-ajax-navigation/01-quick-start.md +45 -45
- package/docs/frontend/02-ajax-navigation/02-configuration.md +14 -14
- package/docs/frontend/02-ajax-navigation/03-advanced-usage.md +36 -36
- package/docs/frontend/03-forms/01-form-handling.md +32 -32
- package/docs/frontend/04-api-requests/01-get-post.md +33 -33
- package/docs/frontend/05-streaming/01-client-streaming.md +15 -15
- package/docs/frontend/06-websocket/00-overview.md +76 -0
- package/docs/frontend/06-websocket/01-websocket-client.md +139 -0
- package/docs/frontend/06-websocket/02-shared-websocket.md +149 -0
- package/docs/index.json +49 -11
- package/eslint.config.mjs +6 -6
- package/{framework/index.js → index.js} +1 -1
- package/package.json +14 -39
- package/{framework/src → src}/Auth.js +59 -59
- package/{framework/src → src}/Config.js +3 -3
- package/{framework/src → src}/Lang.js +7 -7
- package/{framework/src → src}/Mail.js +5 -5
- package/{framework/src → src}/Mysql.js +42 -42
- package/src/Odac.js +112 -0
- package/{framework/src → src}/Request.js +38 -36
- package/{framework/src → src}/Route/Internal.js +116 -116
- package/src/Route/Middleware.js +75 -0
- package/src/Route.js +621 -0
- package/src/Server.js +22 -0
- package/{framework/src → src}/Stream.js +11 -3
- package/{framework/src → src}/Validator.js +21 -21
- package/{framework/src → src}/Var.js +5 -5
- package/{framework/src → src}/View/EarlyHints.js +1 -1
- package/{framework/src → src}/View/Form.js +69 -69
- package/{framework/src → src}/View.js +78 -81
- package/src/WebSocket.js +403 -0
- package/template/config.json +5 -0
- package/{web → template}/controller/page/about.js +6 -6
- package/{web → template}/controller/page/index.js +9 -9
- package/{web → template}/package.json +4 -5
- package/{web → template}/public/assets/css/style.css +4 -4
- package/{web → template}/public/assets/js/app.js +6 -6
- package/{web → template}/route/www.js +6 -6
- package/{web → template}/skeleton/main.html +1 -1
- package/{web → template}/view/content/about.html +5 -5
- package/{web → template}/view/content/home.html +12 -12
- package/template/view/footer/main.html +11 -0
- package/{web → template}/view/head/main.html +1 -1
- package/{web → template}/view/header/main.html +2 -2
- package/test/core/Candy.test.js +58 -58
- package/test/core/Commands.test.js +7 -7
- package/test/core/Config.test.js +82 -85
- package/test/core/Lang.test.js +2 -2
- package/test/core/Process.test.js +6 -6
- package/test/framework/Route.test.js +56 -37
- package/test/framework/View/EarlyHints.test.js +2 -2
- package/test/framework/WebSocket.test.js +100 -0
- package/test/framework/middleware.test.js +85 -0
- package/test/server/Api.test.js +31 -31
- package/test/server/DNS.test.js +11 -11
- package/test/server/Hub.test.js +497 -0
- package/test/server/Mail.account.test_.js +3 -3
- package/test/server/Mail.init.test_.js +10 -10
- package/test/server/Mail.test_.js +20 -20
- package/test/server/SSL.test_.js +54 -54
- package/test/server/Server.test.js +39 -39
- package/test/server/Service.test_.js +7 -7
- package/test/server/Subdomain.test.js +7 -7
- package/test/server/Web/Firewall.test.js +87 -87
- package/test/server/Web/Proxy.test.js +397 -0
- package/test/server/{Web.test_.js → Web.test.js} +137 -205
- package/test/server/__mocks__/fs.js +2 -2
- package/test/server/__mocks__/{globalCandy.js → globalOdac.js} +5 -5
- package/test/server/__mocks__/index.js +6 -6
- package/test/server/__mocks__/testFactories.js +1 -1
- package/test/server/__mocks__/testHelpers.js +7 -7
- package/.husky/pre-commit +0 -2
- package/.kiro/steering/code-style.md +0 -56
- package/.kiro/steering/product.md +0 -20
- package/.kiro/steering/structure.md +0 -77
- package/.kiro/steering/tech.md +0 -87
- package/AGENTS.md +0 -84
- package/bin/candy +0 -10
- package/bin/candypack +0 -10
- package/cli/index.js +0 -3
- package/cli/src/Cli.js +0 -348
- package/cli/src/Connector.js +0 -93
- package/cli/src/Monitor.js +0 -416
- package/core/Candy.js +0 -87
- package/core/Commands.js +0 -239
- package/core/Config.js +0 -1094
- package/core/Lang.js +0 -52
- package/core/Log.js +0 -43
- package/core/Process.js +0 -26
- package/docs/backend/05-controllers/02-your-trusty-candy-assistant.md +0 -20
- package/docs/server/01-installation/01-quick-install.md +0 -19
- package/docs/server/01-installation/02-manual-installation-via-npm.md +0 -9
- package/docs/server/02-get-started/01-core-concepts.md +0 -7
- package/docs/server/02-get-started/02-basic-commands.md +0 -57
- package/docs/server/02-get-started/03-cli-reference.md +0 -276
- package/docs/server/02-get-started/04-cli-quick-reference.md +0 -102
- package/docs/server/03-service/01-start-a-new-service.md +0 -57
- package/docs/server/03-service/02-delete-a-service.md +0 -48
- package/docs/server/04-web/01-create-a-website.md +0 -36
- package/docs/server/04-web/02-list-websites.md +0 -9
- package/docs/server/04-web/03-delete-a-website.md +0 -29
- package/docs/server/05-subdomain/01-create-a-subdomain.md +0 -32
- package/docs/server/05-subdomain/02-list-subdomains.md +0 -33
- package/docs/server/05-subdomain/03-delete-a-subdomain.md +0 -41
- package/docs/server/06-ssl/01-renew-an-ssl-certificate.md +0 -34
- package/docs/server/07-mail/01-create-a-mail-account.md +0 -23
- package/docs/server/07-mail/02-delete-a-mail-account.md +0 -20
- package/docs/server/07-mail/03-list-mail-accounts.md +0 -20
- package/docs/server/07-mail/04-change-account-password.md +0 -23
- package/framework/src/Candy.js +0 -81
- package/framework/src/Route.js +0 -455
- package/framework/src/Server.js +0 -15
- package/locale/de-DE.json +0 -80
- package/locale/en-US.json +0 -79
- package/locale/es-ES.json +0 -80
- package/locale/fr-FR.json +0 -80
- package/locale/pt-BR.json +0 -80
- package/locale/ru-RU.json +0 -80
- package/locale/tr-TR.json +0 -85
- package/locale/zh-CN.json +0 -80
- package/server/index.js +0 -5
- package/server/src/Api.js +0 -88
- package/server/src/DNS.js +0 -940
- package/server/src/Hub.js +0 -535
- package/server/src/Mail.js +0 -571
- package/server/src/SSL.js +0 -180
- package/server/src/Server.js +0 -27
- package/server/src/Service.js +0 -248
- package/server/src/Subdomain.js +0 -64
- package/server/src/Web/Firewall.js +0 -170
- package/server/src/Web/Proxy.js +0 -134
- package/server/src/Web.js +0 -451
- package/server/src/mail/imap.js +0 -1091
- package/server/src/mail/server.js +0 -32
- package/server/src/mail/smtp.js +0 -786
- package/test/server/Client.test.js +0 -338
- package/test/server/__mocks__/http-proxy.js +0 -105
- package/watchdog/index.js +0 -3
- package/watchdog/src/Watchdog.js +0 -156
- package/web/config.json +0 -5
- package/web/view/footer/main.html +0 -11
- /package/{framework/src → src}/Env.js +0 -0
- /package/{framework/src → src}/Route/Cron.js +0 -0
- /package/{framework/src → src}/Token.js +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
class
|
|
1
|
+
class Odac {
|
|
2
2
|
actions = {}
|
|
3
3
|
#data = null
|
|
4
4
|
fn = {}
|
|
@@ -52,7 +52,7 @@ class candy {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
document.dispatchEvent(
|
|
55
|
-
new CustomEvent('
|
|
55
|
+
new CustomEvent('odac:ajaxSuccess', {
|
|
56
56
|
detail: {response: responseData, status: xhr.statusText, xhr, requestUrl: url}
|
|
57
57
|
})
|
|
58
58
|
)
|
|
@@ -245,14 +245,14 @@ class candy {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
client() {
|
|
248
|
-
if (!document.cookie.includes('
|
|
249
|
-
return document.cookie.split('
|
|
248
|
+
if (!document.cookie.includes('odac_client=')) return null
|
|
249
|
+
return document.cookie.split('odac_client=')[1].split(';')[0]
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
data() {
|
|
253
253
|
if (this.#data) return this.#data
|
|
254
|
-
if (!document.cookie.includes('
|
|
255
|
-
return JSON.parse(unescape(document.cookie.split('
|
|
254
|
+
if (!document.cookie.includes('odac_data=')) return null
|
|
255
|
+
return JSON.parse(unescape(document.cookie.split('odac_data=')[1].split(';')[0]))
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
form(obj, callback) {
|
|
@@ -284,11 +284,11 @@ class candy {
|
|
|
284
284
|
|
|
285
285
|
const customMessage = input.getAttribute(`data-error-${errorType}`)
|
|
286
286
|
if (customMessage) {
|
|
287
|
-
let errorSpan = formElement.querySelector(`[
|
|
287
|
+
let errorSpan = formElement.querySelector(`[odac-form-error="${input.name}"]`)
|
|
288
288
|
|
|
289
289
|
if (!errorSpan) {
|
|
290
290
|
errorSpan = document.createElement('span')
|
|
291
|
-
errorSpan.setAttribute('
|
|
291
|
+
errorSpan.setAttribute('odac-form-error', input.name)
|
|
292
292
|
|
|
293
293
|
if ((input.type === 'checkbox' || input.type === 'radio') && input.id) {
|
|
294
294
|
const label = formElement.querySelector(`label[for="${input.id}"]`)
|
|
@@ -309,7 +309,7 @@ class candy {
|
|
|
309
309
|
|
|
310
310
|
for (const input of inputs) {
|
|
311
311
|
input.style.borderColor = ''
|
|
312
|
-
const errorSpan = formElement.querySelector(`[
|
|
312
|
+
const errorSpan = formElement.querySelector(`[odac-form-error="${input.name}"]`)
|
|
313
313
|
if (errorSpan) {
|
|
314
314
|
errorSpan.style.display = 'none'
|
|
315
315
|
errorSpan.textContent = ''
|
|
@@ -355,13 +355,13 @@ class candy {
|
|
|
355
355
|
|
|
356
356
|
let actions = this.actions
|
|
357
357
|
if (
|
|
358
|
-
actions.
|
|
359
|
-
actions.
|
|
360
|
-
actions.
|
|
361
|
-
actions.
|
|
362
|
-
actions.
|
|
358
|
+
actions.odac &&
|
|
359
|
+
actions.odac.form &&
|
|
360
|
+
actions.odac.form.input &&
|
|
361
|
+
actions.odac.form.input.class &&
|
|
362
|
+
actions.odac.form.input.class.invalid
|
|
363
363
|
) {
|
|
364
|
-
const invalidClass = actions.
|
|
364
|
+
const invalidClass = actions.odac.form.input.class.invalid
|
|
365
365
|
formElement
|
|
366
366
|
.querySelectorAll(`select.${invalidClass},input.${invalidClass},textarea.${invalidClass}`)
|
|
367
367
|
.forEach(el => el.classList.remove(invalidClass))
|
|
@@ -369,10 +369,10 @@ class candy {
|
|
|
369
369
|
|
|
370
370
|
if (obj.messages !== false) {
|
|
371
371
|
if (obj.messages == undefined || obj.messages == true || obj.messages.includes('error')) {
|
|
372
|
-
formElement.querySelectorAll('*[
|
|
372
|
+
formElement.querySelectorAll('*[odac-form-error]').forEach(el => (el.style.display = 'none'))
|
|
373
373
|
}
|
|
374
374
|
if (obj.messages == undefined || obj.messages == true || obj.messages.includes('success')) {
|
|
375
|
-
formElement.querySelectorAll('*[
|
|
375
|
+
formElement.querySelectorAll('*[odac-form-success]').forEach(el => (el.style.display = 'none'))
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
378
|
|
|
@@ -414,17 +414,20 @@ class candy {
|
|
|
414
414
|
if (!data.result) return false
|
|
415
415
|
if (obj.messages == undefined || obj.messages) {
|
|
416
416
|
if (data.result.success && (obj.messages == undefined || obj.messages.includes('success') || obj.messages == true)) {
|
|
417
|
-
const successEl = formElement.querySelector('*[
|
|
417
|
+
const successEl = formElement.querySelector('*[odac-form-success]')
|
|
418
418
|
if (successEl) {
|
|
419
419
|
successEl.innerHTML = data.result.message
|
|
420
420
|
this.#fadeIn(successEl)
|
|
421
421
|
} else {
|
|
422
|
-
|
|
422
|
+
const span = document.createElement('span')
|
|
423
|
+
span.setAttribute('odac-form-success', obj.form)
|
|
424
|
+
span.textContent = data.result.message
|
|
425
|
+
formElement.appendChild(span)
|
|
423
426
|
}
|
|
424
427
|
} else if (!data.result.success && data.errors) {
|
|
425
428
|
Object.entries(data.errors).forEach(([name, message]) => {
|
|
426
429
|
if (message) {
|
|
427
|
-
let errorEl = formElement.querySelector(`[
|
|
430
|
+
let errorEl = formElement.querySelector(`[odac-form-error="${name}"]`)
|
|
428
431
|
if (errorEl) {
|
|
429
432
|
errorEl.textContent = message
|
|
430
433
|
errorEl.style.cssText = 'display:block;color:#dc3545;font-size:0.875rem;margin-top:0.25rem'
|
|
@@ -432,7 +435,7 @@ class candy {
|
|
|
432
435
|
const inputEl = formElement.querySelector(`*[name="${name}"]`)
|
|
433
436
|
if (inputEl) {
|
|
434
437
|
errorEl = document.createElement('span')
|
|
435
|
-
errorEl.setAttribute('
|
|
438
|
+
errorEl.setAttribute('odac-form-error', name)
|
|
436
439
|
errorEl.textContent = message
|
|
437
440
|
errorEl.style.cssText = 'display:block;color:#dc3545;font-size:0.875rem;margin-top:0.25rem'
|
|
438
441
|
|
|
@@ -446,9 +449,9 @@ class candy {
|
|
|
446
449
|
} else {
|
|
447
450
|
inputEl.parentNode.insertBefore(errorEl, inputEl.nextSibling)
|
|
448
451
|
}
|
|
449
|
-
} else if (name === '
|
|
452
|
+
} else if (name === '_odac_form') {
|
|
450
453
|
errorEl = document.createElement('div')
|
|
451
|
-
errorEl.setAttribute('
|
|
454
|
+
errorEl.setAttribute('odac-form-error', name)
|
|
452
455
|
errorEl.textContent = message
|
|
453
456
|
errorEl.style.cssText =
|
|
454
457
|
'display:block;color:#dc3545;background-color:#f8d7da;border:1px solid #f5c2c7;border-radius:0.375rem;padding:0.75rem 1rem;margin-bottom:1rem;font-size:0.875rem'
|
|
@@ -465,7 +468,7 @@ class candy {
|
|
|
465
468
|
'focus',
|
|
466
469
|
function handler() {
|
|
467
470
|
inputEl.style.borderColor = ''
|
|
468
|
-
const errorEl = formElement.querySelector(`[
|
|
471
|
+
const errorEl = formElement.querySelector(`[odac-form-error="${name}"]`)
|
|
469
472
|
if (errorEl) {
|
|
470
473
|
errorEl.style.display = 'none'
|
|
471
474
|
errorEl.textContent = ''
|
|
@@ -500,7 +503,7 @@ class candy {
|
|
|
500
503
|
return xhr
|
|
501
504
|
},
|
|
502
505
|
error: () => {
|
|
503
|
-
console.error('
|
|
506
|
+
console.error('Odac:', 'Somethings went wrong...', '\nForm: ' + obj.form + '\nRequest: ' + formElement.getAttribute('action'))
|
|
504
507
|
},
|
|
505
508
|
complete: () => {
|
|
506
509
|
const submitButtons = formElement.querySelectorAll('button[type="submit"], input[type="submit"]')
|
|
@@ -541,12 +544,12 @@ class candy {
|
|
|
541
544
|
|
|
542
545
|
token() {
|
|
543
546
|
if (!this.#token.listener) {
|
|
544
|
-
document.addEventListener('
|
|
547
|
+
document.addEventListener('odac:ajaxSuccess', event => {
|
|
545
548
|
const {detail} = event
|
|
546
549
|
const {xhr, requestUrl} = detail
|
|
547
550
|
if (requestUrl.includes('://')) return false
|
|
548
551
|
try {
|
|
549
|
-
const token = xhr.getResponseHeader('X-
|
|
552
|
+
const token = xhr.getResponseHeader('X-Odac-Token')
|
|
550
553
|
if (token) this.#token.hash.push(token)
|
|
551
554
|
if (this.#token.hash.length > 2) this.#token.hash.shift()
|
|
552
555
|
} catch (e) {
|
|
@@ -558,8 +561,8 @@ class candy {
|
|
|
558
561
|
if (!this.#token.hash.length) {
|
|
559
562
|
var req = new XMLHttpRequest()
|
|
560
563
|
req.open('GET', '/', false)
|
|
561
|
-
req.setRequestHeader('X-
|
|
562
|
-
req.setRequestHeader('X-
|
|
564
|
+
req.setRequestHeader('X-Odac', 'token')
|
|
565
|
+
req.setRequestHeader('X-Odac-Client', this.client())
|
|
563
566
|
req.send(null)
|
|
564
567
|
var req_data = JSON.parse(req.response)
|
|
565
568
|
if (req_data.token) this.#token.hash.push(req_data.token)
|
|
@@ -570,7 +573,7 @@ class candy {
|
|
|
570
573
|
this.#ajax({
|
|
571
574
|
url: '/',
|
|
572
575
|
type: 'GET',
|
|
573
|
-
headers: {'X-
|
|
576
|
+
headers: {'X-Odac': 'token', 'X-Odac-Client': this.client()},
|
|
574
577
|
success: data => {
|
|
575
578
|
var result = JSON.parse(JSON.stringify(data))
|
|
576
579
|
if (result.token) this.#token.hash.push(result.token)
|
|
@@ -622,7 +625,7 @@ class candy {
|
|
|
622
625
|
window.history.pushState(null, document.title, finalUrl)
|
|
623
626
|
}
|
|
624
627
|
|
|
625
|
-
const newPage = ajaxXhr.getResponseHeader('X-
|
|
628
|
+
const newPage = ajaxXhr.getResponseHeader('X-Odac-Page')
|
|
626
629
|
if (newPage !== null) {
|
|
627
630
|
this.#page = newPage
|
|
628
631
|
document.documentElement.dataset.candyPage = newPage
|
|
@@ -668,9 +671,9 @@ class candy {
|
|
|
668
671
|
url: url,
|
|
669
672
|
type: 'GET',
|
|
670
673
|
headers: {
|
|
671
|
-
'X-
|
|
672
|
-
'X-
|
|
673
|
-
'X-
|
|
674
|
+
'X-Odac': 'ajaxload',
|
|
675
|
+
'X-Odac-Load': Object.keys(this.#loader.elements).join(','),
|
|
676
|
+
'X-Odac-Skeleton': currentSkeleton || ''
|
|
674
677
|
},
|
|
675
678
|
dataType: 'json',
|
|
676
679
|
success: (data, status, xhr) => {
|
|
@@ -799,16 +802,279 @@ class candy {
|
|
|
799
802
|
}
|
|
800
803
|
}
|
|
801
804
|
}
|
|
805
|
+
|
|
806
|
+
ws(path, options = {}) {
|
|
807
|
+
const {autoReconnect = true, reconnectDelay = 3000, maxReconnectAttempts = 10, shared = false, token = true} = options
|
|
808
|
+
|
|
809
|
+
if (shared && typeof SharedWorker !== 'undefined') {
|
|
810
|
+
return this.#createSharedWebSocket(path, {autoReconnect, reconnectDelay, maxReconnectAttempts, token})
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
let socket = null
|
|
814
|
+
let reconnectTimer = null
|
|
815
|
+
let reconnectAttempts = 0
|
|
816
|
+
let isClosed = false
|
|
817
|
+
const handlers = {}
|
|
818
|
+
|
|
819
|
+
const emit = (event, ...args) => {
|
|
820
|
+
if (handlers[event]) {
|
|
821
|
+
handlers[event].forEach(fn => fn(...args))
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const connect = () => {
|
|
826
|
+
if (isClosed) return
|
|
827
|
+
|
|
828
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
829
|
+
const wsUrl = `${protocol}//${window.location.host}${path}`
|
|
830
|
+
|
|
831
|
+
const protocols = []
|
|
832
|
+
if (token) {
|
|
833
|
+
const csrfToken = this.token()
|
|
834
|
+
if (csrfToken) {
|
|
835
|
+
protocols.push(`odac-token-${csrfToken}`)
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
socket = protocols.length > 0 ? new WebSocket(wsUrl, protocols) : new WebSocket(wsUrl)
|
|
840
|
+
|
|
841
|
+
socket.onopen = () => {
|
|
842
|
+
reconnectAttempts = 0
|
|
843
|
+
emit('open')
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
socket.onmessage = e => {
|
|
847
|
+
try {
|
|
848
|
+
const data = JSON.parse(e.data)
|
|
849
|
+
emit('message', data)
|
|
850
|
+
} catch {
|
|
851
|
+
emit('message', e.data)
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
socket.onclose = e => {
|
|
856
|
+
emit('close', e)
|
|
857
|
+
|
|
858
|
+
if (autoReconnect && !isClosed && reconnectAttempts < maxReconnectAttempts) {
|
|
859
|
+
reconnectAttempts++
|
|
860
|
+
reconnectTimer = setTimeout(connect, reconnectDelay)
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
socket.onerror = e => {
|
|
865
|
+
emit('error', e)
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
connect()
|
|
870
|
+
|
|
871
|
+
return {
|
|
872
|
+
on: (event, handler) => {
|
|
873
|
+
if (!handlers[event]) handlers[event] = []
|
|
874
|
+
handlers[event].push(handler)
|
|
875
|
+
return this
|
|
876
|
+
},
|
|
877
|
+
off: (event, handler) => {
|
|
878
|
+
if (!handlers[event]) return
|
|
879
|
+
if (handler) {
|
|
880
|
+
handlers[event] = handlers[event].filter(h => h !== handler)
|
|
881
|
+
} else {
|
|
882
|
+
delete handlers[event]
|
|
883
|
+
}
|
|
884
|
+
return this
|
|
885
|
+
},
|
|
886
|
+
send: data => {
|
|
887
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
888
|
+
socket.send(typeof data === 'object' ? JSON.stringify(data) : data)
|
|
889
|
+
}
|
|
890
|
+
return this
|
|
891
|
+
},
|
|
892
|
+
close: () => {
|
|
893
|
+
isClosed = true
|
|
894
|
+
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
895
|
+
if (socket) socket.close()
|
|
896
|
+
},
|
|
897
|
+
get state() {
|
|
898
|
+
return socket ? socket.readyState : WebSocket.CLOSED
|
|
899
|
+
},
|
|
900
|
+
get connected() {
|
|
901
|
+
return socket && socket.readyState === WebSocket.OPEN
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
#createSharedWebSocket(path, options) {
|
|
907
|
+
const workerUrl = this.#createWorkerBlob()
|
|
908
|
+
const worker = new SharedWorker(workerUrl, `odac-ws-${path}`)
|
|
909
|
+
const handlers = {}
|
|
910
|
+
let isConnected = false
|
|
911
|
+
|
|
912
|
+
const emit = (event, ...args) => {
|
|
913
|
+
if (handlers[event]) {
|
|
914
|
+
handlers[event].forEach(fn => fn(...args))
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
worker.port.onmessage = e => {
|
|
919
|
+
const {type, data} = e.data
|
|
920
|
+
|
|
921
|
+
switch (type) {
|
|
922
|
+
case 'open':
|
|
923
|
+
isConnected = true
|
|
924
|
+
emit('open')
|
|
925
|
+
break
|
|
926
|
+
case 'message':
|
|
927
|
+
emit('message', data)
|
|
928
|
+
break
|
|
929
|
+
case 'close':
|
|
930
|
+
isConnected = false
|
|
931
|
+
emit('close', data)
|
|
932
|
+
break
|
|
933
|
+
case 'error':
|
|
934
|
+
emit('error', data)
|
|
935
|
+
break
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
worker.port.start()
|
|
940
|
+
|
|
941
|
+
const token = options.token ? this.token() : null
|
|
942
|
+
|
|
943
|
+
worker.port.postMessage({
|
|
944
|
+
type: 'connect',
|
|
945
|
+
path,
|
|
946
|
+
host: window.location.host,
|
|
947
|
+
protocol: window.location.protocol === 'https:' ? 'wss:' : 'ws:',
|
|
948
|
+
token,
|
|
949
|
+
options
|
|
950
|
+
})
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
on: (event, handler) => {
|
|
954
|
+
if (!handlers[event]) handlers[event] = []
|
|
955
|
+
handlers[event].push(handler)
|
|
956
|
+
return this
|
|
957
|
+
},
|
|
958
|
+
off: (event, handler) => {
|
|
959
|
+
if (!handlers[event]) return
|
|
960
|
+
if (handler) {
|
|
961
|
+
handlers[event] = handlers[event].filter(h => h !== handler)
|
|
962
|
+
} else {
|
|
963
|
+
delete handlers[event]
|
|
964
|
+
}
|
|
965
|
+
return this
|
|
966
|
+
},
|
|
967
|
+
send: data => {
|
|
968
|
+
worker.port.postMessage({
|
|
969
|
+
type: 'send',
|
|
970
|
+
data: typeof data === 'object' ? JSON.stringify(data) : data
|
|
971
|
+
})
|
|
972
|
+
return this
|
|
973
|
+
},
|
|
974
|
+
close: () => {
|
|
975
|
+
worker.port.postMessage({type: 'close'})
|
|
976
|
+
worker.port.close()
|
|
977
|
+
},
|
|
978
|
+
get connected() {
|
|
979
|
+
return isConnected
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
#createWorkerBlob() {
|
|
985
|
+
const workerCode = `
|
|
986
|
+
let socket = null
|
|
987
|
+
let reconnectTimer = null
|
|
988
|
+
let reconnectAttempts = 0
|
|
989
|
+
let options = {}
|
|
990
|
+
let protocols = []
|
|
991
|
+
const ports = new Set()
|
|
992
|
+
|
|
993
|
+
function broadcast(type, data) {
|
|
994
|
+
ports.forEach(port => {
|
|
995
|
+
port.postMessage({type, data})
|
|
996
|
+
})
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function connect(wsUrl, protocols) {
|
|
1000
|
+
if (socket && socket.readyState !== WebSocket.CLOSED) return
|
|
1001
|
+
|
|
1002
|
+
socket = protocols && protocols.length > 0 ? new WebSocket(wsUrl, protocols) : new WebSocket(wsUrl)
|
|
1003
|
+
|
|
1004
|
+
socket.onopen = () => {
|
|
1005
|
+
reconnectAttempts = 0
|
|
1006
|
+
broadcast('open')
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
socket.onmessage = e => {
|
|
1010
|
+
try {
|
|
1011
|
+
const data = JSON.parse(e.data)
|
|
1012
|
+
broadcast('message', data)
|
|
1013
|
+
} catch {
|
|
1014
|
+
broadcast('message', e.data)
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
socket.onclose = e => {
|
|
1019
|
+
broadcast('close', e)
|
|
1020
|
+
|
|
1021
|
+
if (options.autoReconnect && reconnectAttempts < options.maxReconnectAttempts) {
|
|
1022
|
+
reconnectAttempts++
|
|
1023
|
+
reconnectTimer = setTimeout(() => connect(wsUrl, protocols), options.reconnectDelay)
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
socket.onerror = e => {
|
|
1028
|
+
broadcast('error', e)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
self.onconnect = e => {
|
|
1033
|
+
const port = e.ports[0]
|
|
1034
|
+
ports.add(port)
|
|
1035
|
+
|
|
1036
|
+
port.onmessage = event => {
|
|
1037
|
+
const {type, path, host, protocol, token, options: opts, data} = event.data
|
|
1038
|
+
|
|
1039
|
+
switch (type) {
|
|
1040
|
+
case 'connect':
|
|
1041
|
+
options = opts
|
|
1042
|
+
const wsUrl = protocol + '//' + host + path
|
|
1043
|
+
protocols = token ? ['odac-token-' + token] : []
|
|
1044
|
+
connect(wsUrl, protocols)
|
|
1045
|
+
break
|
|
1046
|
+
case 'send':
|
|
1047
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
1048
|
+
socket.send(data)
|
|
1049
|
+
}
|
|
1050
|
+
break
|
|
1051
|
+
case 'close':
|
|
1052
|
+
ports.delete(port)
|
|
1053
|
+
if (ports.size === 0 && socket) {
|
|
1054
|
+
socket.close()
|
|
1055
|
+
socket = null
|
|
1056
|
+
}
|
|
1057
|
+
break
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
port.start()
|
|
1062
|
+
}
|
|
1063
|
+
`
|
|
1064
|
+
|
|
1065
|
+
const blob = new Blob([workerCode], {type: 'application/javascript'})
|
|
1066
|
+
return URL.createObjectURL(blob)
|
|
1067
|
+
}
|
|
802
1068
|
}
|
|
803
1069
|
|
|
804
|
-
window.
|
|
1070
|
+
window.Odac = new Odac()
|
|
805
1071
|
|
|
806
|
-
// Auto-initialize navigation from data-
|
|
1072
|
+
// Auto-initialize navigation from data-odac-navigate attribute
|
|
807
1073
|
;(function initAutoNavigate() {
|
|
808
1074
|
const init = () => {
|
|
809
|
-
const contentEl = document.querySelector('[data-
|
|
1075
|
+
const contentEl = document.querySelector('[data-odac-navigate="content"]')
|
|
810
1076
|
if (contentEl) {
|
|
811
|
-
window.
|
|
1077
|
+
window.Odac.loader('a[href^="/"]:not([data-navigate="false"]):not(.no-navigate)', {content: '[data-odac-navigate="content"]'}, null)
|
|
812
1078
|
}
|
|
813
1079
|
}
|
|
814
1080
|
|
|
@@ -823,16 +1089,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
823
1089
|
const formTypes = ['register', 'login']
|
|
824
1090
|
|
|
825
1091
|
formTypes.forEach(type => {
|
|
826
|
-
const forms = document.querySelectorAll(`form.
|
|
1092
|
+
const forms = document.querySelectorAll(`form.odac-${type}-form[data-odac-${type}]`)
|
|
827
1093
|
forms.forEach(form => {
|
|
828
|
-
const token = form.getAttribute(`data-
|
|
829
|
-
window.
|
|
1094
|
+
const token = form.getAttribute(`data-odac-${type}`)
|
|
1095
|
+
window.Odac.form({form: `form[data-odac-${type}="${token}"]`})
|
|
830
1096
|
})
|
|
831
1097
|
})
|
|
832
1098
|
|
|
833
|
-
const customForms = document.querySelectorAll('form.
|
|
1099
|
+
const customForms = document.querySelectorAll('form.odac-custom-form[data-odac-form]')
|
|
834
1100
|
customForms.forEach(form => {
|
|
835
|
-
const token = form.getAttribute('data-
|
|
836
|
-
window.
|
|
1101
|
+
const token = form.getAttribute('data-odac-form')
|
|
1102
|
+
window.Odac.form({form: `form[data-odac-form="${token}"]`})
|
|
837
1103
|
})
|
|
838
1104
|
})
|
package/docs/backend/01-overview/{01-whats-in-the-candy-box.md → 01-whats-in-the-odac-box.md}
RENAMED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
## 🧰 What's in the `
|
|
1
|
+
## 🧰 What's in the `Odac` box?
|
|
2
2
|
|
|
3
|
-
Your `
|
|
3
|
+
Your `Odac` assistant comes with a bunch of handy services:
|
|
4
4
|
|
|
5
5
|
* `Request`: All the details about the user's incoming request.
|
|
6
6
|
* `View`: A tool to render your beautiful HTML pages.
|
|
7
7
|
* `Auth`: Your friendly security guard for managing the current user's login.
|
|
8
8
|
* `Token`: Helps protect the user's forms from nasty CSRF attacks.
|
|
9
9
|
* `Lang`: A helper for translating your app into the user's language.
|
|
10
|
+
* `setInterval()` / `setTimeout()`: Memory-safe timers with automatic cleanup.
|
|
11
|
+
* `stream()`: Create streaming responses (Server-Sent Events).
|
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
## ✨ Super-Handy Helper Functions
|
|
2
2
|
|
|
3
|
-
On top of that, `
|
|
3
|
+
On top of that, `Odac` has some quick-and-easy helper functions:
|
|
4
4
|
|
|
5
5
|
* `return(data)`: Quickly send a response back to the user and you're done.
|
|
6
6
|
* `direct(url)`: Need to send the user to another page? This is your tool.
|
|
7
7
|
* `cookie(key, value)`: Leave a little cookie in the user's browser.
|
|
8
8
|
* `env(key, defaultValue)`: Access environment variables with an optional default value.
|
|
9
9
|
* `validator()`: A powerful tool to check the user's submitted data.
|
|
10
|
+
* `setInterval(callback, delay)`: Schedule repeating tasks with automatic cleanup.
|
|
11
|
+
* `setTimeout(callback, delay)`: Schedule one-time tasks with automatic cleanup.
|
|
12
|
+
* `clearInterval(id)`: Manually clear an interval.
|
|
13
|
+
* `clearTimeout(id)`: Manually clear a timeout.
|
|
14
|
+
|
|
15
|
+
### Memory-Safe Timers
|
|
16
|
+
|
|
17
|
+
Odac provides memory-safe timer functions that automatically clean up when the request ends:
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
module.exports = async (Odac) => {
|
|
21
|
+
// ✅ Automatically cleaned up when request ends
|
|
22
|
+
Odac.setInterval(() => {
|
|
23
|
+
console.log('This will stop when the request ends')
|
|
24
|
+
}, 1000)
|
|
25
|
+
|
|
26
|
+
// ❌ NOT automatically cleaned up - causes memory leaks
|
|
27
|
+
setInterval(() => {
|
|
28
|
+
console.log('This keeps running forever!')
|
|
29
|
+
}, 1000)
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Why use Odac timers?**
|
|
34
|
+
- Prevents memory leaks
|
|
35
|
+
- No orphaned intervals after request ends
|
|
36
|
+
- Especially important for streaming/SSE endpoints
|
|
37
|
+
- Automatic cleanup on connection close
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## 🚀 Development Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Odac provides a built-in development server that allows you to test your website locally without running the full Odac server infrastructure.
|
|
4
4
|
|
|
5
5
|
### Quick Start
|
|
6
6
|
|
|
@@ -10,8 +10,8 @@ Navigate to your website directory and run one of these commands:
|
|
|
10
10
|
# Using npm script
|
|
11
11
|
npm start
|
|
12
12
|
|
|
13
|
-
# Using
|
|
14
|
-
|
|
13
|
+
# Using Odac directly
|
|
14
|
+
odac framework run
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Both commands will start a local development server on port `1071` by default, and your website will be accessible at `http://127.0.0.1:1071`.
|
|
@@ -24,8 +24,8 @@ You can specify a custom port by adding it as an argument:
|
|
|
24
24
|
# Using npm script with custom port
|
|
25
25
|
npm start 8080
|
|
26
26
|
|
|
27
|
-
# Using
|
|
28
|
-
|
|
27
|
+
# Using Odac directly with custom port
|
|
28
|
+
odac framework run 8080
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
This will start the server on your specified port (e.g., `http://127.0.0.1:8080`).
|
|
@@ -41,13 +41,13 @@ The development server (`npm start`) is designed for:
|
|
|
41
41
|
|
|
42
42
|
**Important**: The development server does NOT provide DNS, SSL, or other production services.
|
|
43
43
|
|
|
44
|
-
For production deployment with full
|
|
44
|
+
For production deployment with full Odac server features, create your website using:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
|
|
47
|
+
odac web create
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
This registers your website with the
|
|
50
|
+
This registers your website with the Odac server and provides:
|
|
51
51
|
|
|
52
52
|
- **Automatic SSL** certificate management
|
|
53
53
|
- **DNS handling** for your domain
|
|
@@ -57,12 +57,12 @@ This registers your website with the CandyPack server and provides:
|
|
|
57
57
|
|
|
58
58
|
### Package.json Scripts
|
|
59
59
|
|
|
60
|
-
When you create a new website,
|
|
60
|
+
When you create a new website, Odac automatically generates a `package.json` with these useful scripts:
|
|
61
61
|
|
|
62
62
|
```json
|
|
63
63
|
{
|
|
64
64
|
"scripts": {
|
|
65
|
-
"start": "
|
|
65
|
+
"start": "odac framework run",
|
|
66
66
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -76,4 +76,4 @@ When you create a new website, CandyPack automatically generates a `package.json
|
|
|
76
76
|
- The development server automatically detects changes in your code
|
|
77
77
|
- Use `Ctrl+C` to stop the development server
|
|
78
78
|
- The server will show helpful error messages in the console
|
|
79
|
-
- All
|
|
79
|
+
- All Odac framework features are available in development mode
|
|
@@ -4,7 +4,7 @@ Let's take a look at a typical project layout:
|
|
|
4
4
|
|
|
5
5
|
- `project_root/`
|
|
6
6
|
- `config.json`: This is where you'll keep your app's secrets and settings, like database passwords or API keys.
|
|
7
|
-
- `index.js`: The starting pistol for your web application! This file kicks off the
|
|
7
|
+
- `index.js`: The starting pistol for your web application! This file kicks off the Odac framework.
|
|
8
8
|
- `package.json`: Contains project metadata and npm scripts for development. Automatically generated when creating a new website.
|
|
9
9
|
- `public/`: All files placed in this folder are directly accessible from the outside. This is the perfect place for your images, stylesheets, and client-side JavaScript.
|
|
10
10
|
- `route/`: This folder holds all your route definitions. The filename of the route file corresponds to the subdomain it serves.
|
|
@@ -27,13 +27,13 @@ npm start
|
|
|
27
27
|
npm start 8080
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
You can also use
|
|
30
|
+
You can also use Odac commands directly:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
# Development server (local testing only)
|
|
34
|
-
|
|
34
|
+
odac framework run [port]
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
**Note**: For production websites with DNS and SSL, use `
|
|
37
|
+
**Note**: For production websites with DNS and SSL, use `odac web create` to register with Odac server.
|
|
38
38
|
|
|
39
39
|
Following this structure helps keep your code's responsibilities separate. Your routing logic lives in one place, your app logic in another, and your presentation files in a third. It's a recipe for success!
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
## ⚙️ Configuration Overview
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Odac uses a simple and flexible configuration system based on `config.json` and optional `.env` files. You can choose the approach that best fits your needs.
|
|
4
4
|
|
|
5
5
|
### Configuration Files
|
|
6
6
|
|
|
@@ -87,12 +87,12 @@ Combine both methods - use direct values for non-sensitive data and environment
|
|
|
87
87
|
You can access configuration values in three ways:
|
|
88
88
|
|
|
89
89
|
```javascript
|
|
90
|
-
// 1. From
|
|
91
|
-
const dbHost =
|
|
90
|
+
// 1. From Odac.Config (recommended for structured config)
|
|
91
|
+
const dbHost = Odac.Config.mysql.host
|
|
92
92
|
|
|
93
|
-
// 2. Using
|
|
94
|
-
const apiKey =
|
|
95
|
-
const debug =
|
|
93
|
+
// 2. Using Odac.env() helper
|
|
94
|
+
const apiKey = Odac.env('API_KEY')
|
|
95
|
+
const debug = Odac.env('DEBUG', 'false')
|
|
96
96
|
|
|
97
97
|
// 3. Direct process.env access
|
|
98
98
|
const nodeEnv = process.env.NODE_ENV
|