create-rebe 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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -0
  3. package/bin/create-rebe.js +310 -0
  4. package/package.json +40 -0
  5. package/template/.env.example +141 -0
  6. package/template/.gitattributes +16 -0
  7. package/template/.nvmrc +1 -0
  8. package/template/.prettierignore +10 -0
  9. package/template/.prettierrc +12 -0
  10. package/template/LICENSE +21 -0
  11. package/template/README.md +150 -0
  12. package/template/SECURITY.md +133 -0
  13. package/template/app/hooks/register.hook.js +22 -0
  14. package/template/app/http/controllers/auth.controller.js +34 -0
  15. package/template/app/http/middlewares/auth.middleware.js +25 -0
  16. package/template/app/http/middlewares/register.middleware.js +18 -0
  17. package/template/app/http/validators/auth.validator.js +8 -0
  18. package/template/app/jobs/register.job.js +17 -0
  19. package/template/app/queue/register.queue.js +11 -0
  20. package/template/app/routes/api.route.js +18 -0
  21. package/template/app/routes/register.route.js +8 -0
  22. package/template/app/routes/web.route.js +7 -0
  23. package/template/app/socket/register.socket.js +15 -0
  24. package/template/config/app.config.js +12 -0
  25. package/template/config/bcrypt.config.js +7 -0
  26. package/template/config/cache.config.js +7 -0
  27. package/template/config/cors.config.js +37 -0
  28. package/template/config/cron.config.js +9 -0
  29. package/template/config/database.config.js +51 -0
  30. package/template/config/express.config.js +37 -0
  31. package/template/config/index.js +19 -0
  32. package/template/config/jwt.config.js +10 -0
  33. package/template/config/logger.config.js +10 -0
  34. package/template/config/mail.config.js +14 -0
  35. package/template/config/queue.config.js +11 -0
  36. package/template/config/redis.config.js +32 -0
  37. package/template/config/runtime.config.js +11 -0
  38. package/template/config/socket.config.js +15 -0
  39. package/template/config/storage.config.js +10 -0
  40. package/template/core/bootstrap.core.js +92 -0
  41. package/template/core/common/array.js +144 -0
  42. package/template/core/common/cache.js +100 -0
  43. package/template/core/common/collection.js +173 -0
  44. package/template/core/common/crypt.js +69 -0
  45. package/template/core/common/date.js +254 -0
  46. package/template/core/common/hash.js +61 -0
  47. package/template/core/common/object.js +155 -0
  48. package/template/core/common/path.js +80 -0
  49. package/template/core/common/storage.js +97 -0
  50. package/template/core/common/string.js +137 -0
  51. package/template/core/common/url.js +81 -0
  52. package/template/core/common.core.js +93 -0
  53. package/template/core/cron.core.js +141 -0
  54. package/template/core/database.core.js +113 -0
  55. package/template/core/error.core.js +83 -0
  56. package/template/core/express.core.js +161 -0
  57. package/template/core/hooks.core.js +47 -0
  58. package/template/core/jwt.core.js +81 -0
  59. package/template/core/logger.core.js +100 -0
  60. package/template/core/mailer.core.js +65 -0
  61. package/template/core/queue.core.js +226 -0
  62. package/template/core/redis.core.js +75 -0
  63. package/template/core/register.core.js +91 -0
  64. package/template/core/runtime.core.js +27 -0
  65. package/template/core/socket.core.js +93 -0
  66. package/template/core/validator.core.js +34 -0
  67. package/template/database/.gitkeep +0 -0
  68. package/template/database/models/post.model.js +26 -0
  69. package/template/database/models/user.model.js +30 -0
  70. package/template/docs/Bootstrap.md +50 -0
  71. package/template/docs/Common.md +48 -0
  72. package/template/docs/Cron.md +47 -0
  73. package/template/docs/Database.md +61 -0
  74. package/template/docs/Express.md +63 -0
  75. package/template/docs/Hooks.md +48 -0
  76. package/template/docs/Jwt.md +63 -0
  77. package/template/docs/Logger.md +60 -0
  78. package/template/docs/Mailer.md +50 -0
  79. package/template/docs/Queue.md +57 -0
  80. package/template/docs/README.md +32 -0
  81. package/template/docs/Redis.md +54 -0
  82. package/template/docs/Register.md +52 -0
  83. package/template/docs/Socket.md +55 -0
  84. package/template/docs/Validator.md +45 -0
  85. package/template/ecosystem.config.js +54 -0
  86. package/template/index.js +6 -0
  87. package/template/jest.config.js +27 -0
  88. package/template/jsconfig.json +37 -0
  89. package/template/logs/.gitkeep +0 -0
  90. package/template/nodemon.json +23 -0
  91. package/template/package-lock.json +7033 -0
  92. package/template/package.json +82 -0
  93. package/template/scripts/cli.js +258 -0
  94. package/template/storage/.gitkeep +0 -0
  95. package/template/tests/common.test.js +45 -0
  96. package/template/tests/crypt-hash-storage.test.js +44 -0
  97. package/template/tests/jwt.test.js +45 -0
  98. package/template/tests/register.test.js +55 -0
  99. package/template/tests/setup.js +11 -0
  100. package/template/tests/validator.test.js +65 -0
@@ -0,0 +1,137 @@
1
+ 'use strict'
2
+
3
+ const crypto = require('crypto')
4
+
5
+ // String helpers: casing, slugs, truncation and random tokens.
6
+ class Str {
7
+ // Uppercase the first character only.
8
+ static ucfirst(str) {
9
+ str = String(str ?? '')
10
+ return str.charAt(0).toUpperCase() + str.slice(1)
11
+ }
12
+
13
+ // Lowercase the first character only.
14
+ static lcfirst(str) {
15
+ str = String(str ?? '')
16
+ return str.charAt(0).toLowerCase() + str.slice(1)
17
+ }
18
+
19
+ // Capitalize every word.
20
+ static title(str) {
21
+ return String(str ?? '')
22
+ .toLowerCase()
23
+ .replace(/\b\w/g, (c) => c.toUpperCase())
24
+ }
25
+
26
+ // Split words from camelCase, snake_case, kebab-case and spaces.
27
+ static words(str) {
28
+ return String(str ?? '')
29
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
30
+ .replace(/[_\-]+/g, ' ')
31
+ .trim()
32
+ .split(/\s+/)
33
+ .filter(Boolean)
34
+ }
35
+
36
+ static camel(str) {
37
+ const words = Str.words(str)
38
+ return words.map((w, i) => (i === 0 ? w.toLowerCase() : Str.ucfirst(w.toLowerCase()))).join('')
39
+ }
40
+
41
+ static pascal(str) {
42
+ return Str.words(str)
43
+ .map((w) => Str.ucfirst(w.toLowerCase()))
44
+ .join('')
45
+ }
46
+
47
+ static snake(str) {
48
+ return Str.words(str)
49
+ .map((w) => w.toLowerCase())
50
+ .join('_')
51
+ }
52
+
53
+ static kebab(str) {
54
+ return Str.words(str)
55
+ .map((w) => w.toLowerCase())
56
+ .join('-')
57
+ }
58
+
59
+ // URL-friendly slug.
60
+ static slug(str, separator = '-') {
61
+ return String(str ?? '')
62
+ .normalize('NFKD')
63
+ .replace(/[̀-ͯ]/g, '')
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9]+/g, separator)
66
+ .replace(new RegExp(`^${separator}+|${separator}+$`, 'g'), '')
67
+ }
68
+
69
+ // Cut a string to length, appending a suffix when truncated.
70
+ static truncate(str, length = 100, suffix = '...') {
71
+ str = String(str ?? '')
72
+ if (str.length <= length) return str
73
+ return str.slice(0, length - suffix.length).trimEnd() + suffix
74
+ }
75
+
76
+ static limitWords(str, count = 10, suffix = '...') {
77
+ const words = String(str ?? '').split(/\s+/)
78
+ if (words.length <= count) return str
79
+ return words.slice(0, count).join(' ') + suffix
80
+ }
81
+
82
+ static startsWith(str, prefix) {
83
+ return String(str ?? '').startsWith(prefix)
84
+ }
85
+
86
+ static endsWith(str, suffix) {
87
+ return String(str ?? '').endsWith(suffix)
88
+ }
89
+
90
+ static contains(str, needle) {
91
+ return String(str ?? '').includes(needle)
92
+ }
93
+
94
+ // Ensure a string begins with the given prefix exactly once.
95
+ static start(str, prefix) {
96
+ str = String(str ?? '')
97
+ return str.startsWith(prefix) ? str : prefix + str
98
+ }
99
+
100
+ // Ensure a string ends with the given suffix exactly once.
101
+ static finish(str, suffix) {
102
+ str = String(str ?? '')
103
+ return str.endsWith(suffix) ? str : str + suffix
104
+ }
105
+
106
+ static mask(str, char = '*', start = 0, length = null) {
107
+ str = String(str ?? '')
108
+ const end = length === null ? str.length : start + length
109
+ return str
110
+ .split('')
111
+ .map((c, i) => (i >= start && i < end ? char : c))
112
+ .join('')
113
+ }
114
+
115
+ // Cryptographically strong random hex string.
116
+ static random(length = 16) {
117
+ return crypto
118
+ .randomBytes(Math.ceil(length / 2))
119
+ .toString('hex')
120
+ .slice(0, length)
121
+ }
122
+
123
+ static uuid() {
124
+ return crypto.randomUUID()
125
+ }
126
+
127
+ static escapeHtml(str) {
128
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
129
+ return String(str ?? '').replace(/[&<>"']/g, (c) => map[c])
130
+ }
131
+
132
+ static isEmpty(str) {
133
+ return str === null || str === undefined || String(str).trim() === ''
134
+ }
135
+ }
136
+
137
+ module.exports = Str
@@ -0,0 +1,81 @@
1
+ 'use strict'
2
+
3
+ // URL helpers built on the WHATWG URL and URLSearchParams APIs.
4
+ class Url {
5
+ // Join base and path segments without doubling slashes.
6
+ static join(...parts) {
7
+ return parts
8
+ .filter((p) => p !== null && p !== undefined && p !== '')
9
+ .map((p, i) => {
10
+ let s = String(p)
11
+ if (i > 0) s = s.replace(/^\/+/, '')
12
+ if (i < parts.length - 1) s = s.replace(/\/+$/, '')
13
+ return s
14
+ })
15
+ .join('/')
16
+ }
17
+
18
+ // Serialize an object to a query string (without the leading '?').
19
+ static buildQuery(params = {}) {
20
+ const search = new URLSearchParams()
21
+ for (const [key, value] of Object.entries(params)) {
22
+ if (value === null || value === undefined) continue
23
+ if (Array.isArray(value)) {
24
+ value.forEach((v) => search.append(key, String(v)))
25
+ } else {
26
+ search.append(key, String(value))
27
+ }
28
+ }
29
+ return search.toString()
30
+ }
31
+
32
+ // Parse a query string into a plain object, grouping repeated keys.
33
+ static parseQuery(query = '') {
34
+ const search = new URLSearchParams(query.replace(/^\?/, ''))
35
+ const out = {}
36
+ for (const key of new Set(search.keys())) {
37
+ const all = search.getAll(key)
38
+ out[key] = all.length > 1 ? all : all[0]
39
+ }
40
+ return out
41
+ }
42
+
43
+ // Append/override query parameters on an existing URL.
44
+ static withQuery(url, params = {}) {
45
+ const u = new URL(url)
46
+ for (const [key, value] of Object.entries(params)) {
47
+ if (value === null || value === undefined) {
48
+ u.searchParams.delete(key)
49
+ } else {
50
+ u.searchParams.set(key, String(value))
51
+ }
52
+ }
53
+ return u.toString()
54
+ }
55
+
56
+ // Break a URL into its component parts.
57
+ static parse(url) {
58
+ const u = new URL(url)
59
+ return {
60
+ protocol: u.protocol.replace(':', ''),
61
+ host: u.host,
62
+ hostname: u.hostname,
63
+ port: u.port,
64
+ path: u.pathname,
65
+ query: Url.parseQuery(u.search),
66
+ hash: u.hash.replace('#', ''),
67
+ origin: u.origin,
68
+ }
69
+ }
70
+
71
+ static isValid(url) {
72
+ try {
73
+ new URL(url)
74
+ return true
75
+ } catch {
76
+ return false
77
+ }
78
+ }
79
+ }
80
+
81
+ module.exports = Url
@@ -0,0 +1,93 @@
1
+ 'use strict'
2
+
3
+ const Arr = require('@core/common/array')
4
+ const Str = require('@core/common/string')
5
+ const Obj = require('@core/common/object')
6
+ const Url = require('@core/common/url')
7
+ const Path = require('@core/common/path')
8
+ const Hash = require('@core/common/hash')
9
+ const Crypt = require('@core/common/crypt')
10
+ const Collection = require('@core/common/collection')
11
+ const DateTime = require('@core/common/date')
12
+ const Cache = require('@core/common/cache')
13
+ const Storage = require('@core/common/storage')
14
+
15
+ class Common {
16
+ static getEnv(key, defaultValue = null) {
17
+ const value = process.env[key]
18
+ return value !== undefined && value !== '' ? value : defaultValue
19
+ }
20
+
21
+ static getEnvInt(key, defaultValue = null) {
22
+ const value = process.env[key]
23
+ if (value === undefined || value === '') return defaultValue
24
+ const parsed = parseInt(value, 10)
25
+ return isNaN(parsed) ? defaultValue : parsed
26
+ }
27
+
28
+ static getEnvFloat(key, defaultValue = null) {
29
+ const value = process.env[key]
30
+ if (value === undefined || value === '') return defaultValue
31
+ const parsed = parseFloat(value)
32
+ return isNaN(parsed) ? defaultValue : parsed
33
+ }
34
+
35
+ static getEnvBool(key, defaultValue = null) {
36
+ const value = process.env[key]
37
+ if (value === undefined || value === '') return defaultValue
38
+ return ['true', '1', 'yes', 'on', 'y', 't', 'enable', 'enabled'].includes(value.toLowerCase())
39
+ }
40
+
41
+ static getEnvArray(key, defaultValue = [], separator = ',') {
42
+ const value = process.env[key]
43
+ if (value === undefined || value === '') return defaultValue
44
+ return value
45
+ .split(separator)
46
+ .map((v) => v.trim())
47
+ .filter(Boolean)
48
+ }
49
+
50
+ static getEnvJson(key, defaultValue = null) {
51
+ const value = process.env[key]
52
+ if (value === undefined || value === '') return defaultValue
53
+ try {
54
+ return JSON.parse(value)
55
+ } catch {
56
+ return defaultValue
57
+ }
58
+ }
59
+
60
+ static sleep(ms) {
61
+ return new Promise((resolve) => setTimeout(resolve, ms))
62
+ }
63
+
64
+ static isEmpty(value) {
65
+ if (value === null || value === undefined) return true
66
+ if (typeof value === 'string') return value.trim() === ''
67
+ if (Array.isArray(value)) return value.length === 0
68
+ if (typeof value === 'object') return Object.keys(value).length === 0
69
+ return false
70
+ }
71
+
72
+ static async attempt(fn) {
73
+ try {
74
+ return [null, await fn()]
75
+ } catch (error) {
76
+ return [error, null]
77
+ }
78
+ }
79
+ }
80
+
81
+ Common.Arr = Arr
82
+ Common.Str = Str
83
+ Common.Obj = Obj
84
+ Common.Url = Url
85
+ Common.Path = Path
86
+ Common.Hash = Hash
87
+ Common.Crypt = Crypt
88
+ Common.Collection = Collection
89
+ Common.Date = DateTime
90
+ Common.Cache = Cache
91
+ Common.Storage = Storage
92
+
93
+ module.exports = Common
@@ -0,0 +1,141 @@
1
+ 'use strict'
2
+
3
+ const crypto = require('crypto')
4
+ const cron = require('node-cron')
5
+
6
+ const config = require('@config/index')
7
+ const logger = require('@core/logger.core')
8
+ const Register = require('@core/register.core')
9
+
10
+ const REGISTER_FILE = Register.path('app', 'jobs', 'register.job.js')
11
+ const TABLE = 'cron_runs'
12
+
13
+ class Cron {
14
+ static jobs = new Map()
15
+ static _db = null
16
+
17
+ static define(name, expression, handler, options = {}) {
18
+ if (Cron.jobs.has(name)) {
19
+ logger.warn(`Cron job "${name}" is already defined; overwriting`)
20
+ }
21
+ if (!cron.validate(expression)) {
22
+ throw new Error(`Invalid cron expression for "${name}": ${expression}`)
23
+ }
24
+
25
+ Cron.jobs.set(name, {
26
+ name,
27
+ expression,
28
+ handler,
29
+ timezone: options.timezone || config.cron.timezone,
30
+ runOnInit: options.runOnInit || false,
31
+ task: null,
32
+ })
33
+
34
+ return Cron
35
+ }
36
+
37
+ static async start() {
38
+ if (!config.cron.enabled) {
39
+ logger.info('Cron is disabled (CRON_ENABLED=false)')
40
+ return
41
+ }
42
+
43
+ await Register.invoke(REGISTER_FILE, [Cron], { label: 'app/jobs/register.job.js' })
44
+
45
+ if (config.cron.history && config.db.enabled) {
46
+ try {
47
+ const Database = require('@core/database.core')
48
+ if (Database.sequelize) {
49
+ Cron._db = Database.sequelize
50
+ await Cron._ensureTable()
51
+ }
52
+ } catch (err) {
53
+ logger.warn(`Cron: DB history unavailable: ${err.message}`)
54
+ }
55
+ }
56
+
57
+ for (const job of Cron.jobs.values()) {
58
+ job.task = cron.schedule(job.expression, () => Cron._run(job), {
59
+ timezone: job.timezone,
60
+ })
61
+ if (job.runOnInit) Cron._run(job)
62
+ }
63
+
64
+ logger.info(`Cron started ${Cron.jobs.size} job(s)${Cron.jobs.size ? `: ${[...Cron.jobs.keys()].join(', ')}` : ''}`)
65
+ }
66
+
67
+ static async _run(job) {
68
+ const runId = crypto.randomUUID()
69
+ const startedAt = new Date()
70
+
71
+ if (Cron._db) {
72
+ await Cron._db
73
+ .query(`INSERT INTO \`${TABLE}\` (id, job_name, started_at, status, created_at) VALUES (:id, :name, :startedAt, 'running', :now)`, {
74
+ replacements: { id: runId, name: job.name, startedAt, now: startedAt },
75
+ })
76
+ .catch((err) => logger.debug(`Cron: failed to record run start for "${job.name}": ${err.message}`))
77
+ }
78
+
79
+ try {
80
+ logger.debug(`Cron running: ${job.name}`)
81
+ await job.handler()
82
+ const finishedAt = new Date()
83
+ if (Cron._db) {
84
+ await Cron._db.query(`UPDATE \`${TABLE}\` SET status = 'completed', finished_at = :finishedAt, duration_ms = :duration WHERE id = :id`, { replacements: { finishedAt, duration: finishedAt - startedAt, id: runId } }).catch(() => {})
85
+ }
86
+ } catch (err) {
87
+ logger.error(`Cron job "${job.name}" failed`, err)
88
+ const finishedAt = new Date()
89
+ if (Cron._db) {
90
+ await Cron._db.query(`UPDATE \`${TABLE}\` SET status = 'failed', finished_at = :finishedAt, duration_ms = :duration, error = :error WHERE id = :id`, { replacements: { finishedAt, duration: finishedAt - startedAt, error: err.message, id: runId } }).catch(() => {})
91
+ }
92
+ }
93
+ }
94
+
95
+ static async runNow(name) {
96
+ const job = Cron.jobs.get(name)
97
+ if (!job) throw new Error(`Cron job "${name}" is not defined`)
98
+ return Cron._run(job)
99
+ }
100
+
101
+ static remove(name) {
102
+ const job = Cron.jobs.get(name)
103
+ if (job?.task) job.task.stop()
104
+ return Cron.jobs.delete(name)
105
+ }
106
+
107
+ static stop() {
108
+ for (const job of Cron.jobs.values()) {
109
+ if (job.task) job.task.stop()
110
+ }
111
+ logger.info('Cron stopped')
112
+ }
113
+
114
+ static list() {
115
+ return [...Cron.jobs.values()].map(({ name, expression, timezone }) => ({
116
+ name,
117
+ expression,
118
+ timezone,
119
+ }))
120
+ }
121
+
122
+ static async _ensureTable() {
123
+ await Cron._db.query(`
124
+ CREATE TABLE IF NOT EXISTS \`${TABLE}\` (
125
+ \`id\` VARCHAR(36) NOT NULL,
126
+ \`job_name\` VARCHAR(100) NOT NULL,
127
+ \`started_at\` DATETIME NOT NULL,
128
+ \`finished_at\` DATETIME NULL,
129
+ \`status\` ENUM('running','completed','failed') NOT NULL DEFAULT 'running',
130
+ \`error\` TEXT NULL,
131
+ \`duration_ms\` INT NULL,
132
+ \`created_at\` DATETIME NOT NULL,
133
+ PRIMARY KEY (\`id\`),
134
+ INDEX idx_job_name (\`job_name\`),
135
+ INDEX idx_started_at (\`started_at\`)
136
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
137
+ `)
138
+ }
139
+ }
140
+
141
+ module.exports = Cron
@@ -0,0 +1,113 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { Sequelize, DataTypes, Model, Op } = require('sequelize')
6
+
7
+ const config = require('@config/index')
8
+ const logger = require('@core/logger.core')
9
+
10
+ const MODELS_DIR = path.resolve(process.cwd(), 'database', 'models')
11
+
12
+ class Database {
13
+ static sequelize = null
14
+ static models = new Map()
15
+
16
+ // ── Connection ───────────────────────────────────────────────────────────
17
+ static async connect() {
18
+ if (Database.sequelize) return Database.sequelize
19
+
20
+ const db = config.db
21
+
22
+ Database.sequelize = new Sequelize(db.database, db.username, db.password, {
23
+ host: db.host,
24
+ port: db.port,
25
+ dialect: db.dialect,
26
+ timezone: db.timezone,
27
+ logging: db.logging ? (msg) => logger.debug(msg) : false,
28
+ benchmark: db.benchmark,
29
+ logQueryParameters: db.logQueryParameters,
30
+ dialectOptions: db.dialectOptions,
31
+ pool: db.pool,
32
+ retry: db.retry,
33
+ define: db.define,
34
+ })
35
+
36
+ try {
37
+ await Database.sequelize.authenticate()
38
+ logger.info(`Database connected (${db.dialect}://${db.host}:${db.port}/${db.database})`)
39
+ Database.loadModels()
40
+ } catch (err) {
41
+ logger.error('Database connection failed', err)
42
+ throw err
43
+ }
44
+
45
+ return Database.sequelize
46
+ }
47
+
48
+ static async disconnect() {
49
+ if (Database.sequelize) {
50
+ await Database.sequelize.close()
51
+ Database.sequelize = null
52
+ Database.models.clear()
53
+ logger.info('Database connection closed')
54
+ }
55
+ }
56
+
57
+ // ── Models ───────────────────────────────────────────────────────────────
58
+ static loadModels() {
59
+ if (!fs.existsSync(MODELS_DIR)) {
60
+ logger.warn(`Models directory not found: ${MODELS_DIR}`)
61
+ return Database.models
62
+ }
63
+
64
+ const files = fs.readdirSync(MODELS_DIR).filter((file) => file.endsWith('.js') && !file.startsWith('_'))
65
+
66
+ for (const file of files) {
67
+ try {
68
+ const definition = require(path.join(MODELS_DIR, file))
69
+ const factory = definition.default || definition
70
+
71
+ if (typeof factory !== 'function') {
72
+ logger.warn(`Skipped model "${file}": expected a (sequelize, DataTypes) factory export`)
73
+ continue
74
+ }
75
+
76
+ const model = factory(Database.sequelize, DataTypes)
77
+ if (model?.name) {
78
+ Database.models.set(model.name, model)
79
+ }
80
+ } catch (err) {
81
+ logger.error(`Failed to load model "${file}"`, err)
82
+ }
83
+ }
84
+
85
+ // Second pass: wire up associations once all models are loaded.
86
+ const modelsAsObject = Object.fromEntries(Database.models)
87
+ for (const model of Database.models.values()) {
88
+ if (typeof model.associate === 'function') {
89
+ try {
90
+ model.associate(modelsAsObject)
91
+ } catch (err) {
92
+ logger.error(`Failed to associate model "${model.name}"`, err)
93
+ }
94
+ }
95
+ }
96
+
97
+ const names = [...Database.models.keys()]
98
+ logger.info(`Loaded ${names.length} model(s)${names.length ? `: ${names.join(', ')}` : ''}`)
99
+ return Database.models
100
+ }
101
+
102
+ static model(name) {
103
+ if (!Database.models.has(name)) throw new Error(`Model "${name}" is not registered`)
104
+ return Database.models.get(name)
105
+ }
106
+ }
107
+
108
+ Database.Sequelize = Sequelize
109
+ Database.DataTypes = DataTypes
110
+ Database.Model = Model
111
+ Database.Op = Op
112
+
113
+ module.exports = Database
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ const logger = require('@core/logger.core')
4
+ const config = require('@config/index')
5
+ const Common = require('@core/common.core')
6
+
7
+ module.exports = class ErrorHandler {
8
+ static isProd = config.app.env === 'production'
9
+
10
+ static setExceptionHandler() {
11
+ process.on('uncaughtException', (err) => {
12
+ logger.error('Uncaught exception', err)
13
+ process.exit(1)
14
+ })
15
+ }
16
+
17
+ static setRejectionHandler() {
18
+ process.on('unhandledRejection', (reason) => {
19
+ const err = reason instanceof Error ? reason : new Error(String(reason))
20
+ logger.error('Unhandled rejection', err)
21
+ })
22
+ }
23
+
24
+ static notFoundHandler(req, res, _next) {
25
+ const status = 404
26
+ const message = `Cannot ${req.method} ${req.originalUrl}`
27
+
28
+ if (ErrorHandler._wantsJson(req)) {
29
+ return res.status(status).json({
30
+ success: false,
31
+ status,
32
+ message,
33
+ })
34
+ }
35
+
36
+ return res.status(status).send(`
37
+ <!DOCTYPE html>
38
+ <html>
39
+ <head><title>404 Not Found</title></head>
40
+ <body>
41
+ <h1>404 Not Found</h1>
42
+ <p>${Common.Str.escapeHtml(message)}</p>
43
+ </body>
44
+ </html>
45
+ `)
46
+ }
47
+
48
+ static expressErrorHandler(err, req, res, _next) {
49
+ const status = err.status || err.statusCode || 500
50
+ const message = err.message || 'Internal server error'
51
+ const detail = ErrorHandler.isProd ? undefined : err.stack
52
+
53
+ logger.error(err)
54
+
55
+ if (ErrorHandler._wantsJson(req)) {
56
+ return res.status(status).json({
57
+ success: false,
58
+ status,
59
+ message,
60
+ ...(detail && { stack: detail }),
61
+ })
62
+ }
63
+
64
+ return res.status(status).send(`
65
+ <!DOCTYPE html>
66
+ <html>
67
+ <head><title>${status} Error</title></head>
68
+ <body>
69
+ <h1>${status} - ${Common.Str.escapeHtml(message)}</h1>
70
+ ${detail ? `<pre>${Common.Str.escapeHtml(detail)}</pre>` : ''}
71
+ </body>
72
+ </html>
73
+ `)
74
+ }
75
+
76
+ // Detect whether the client expects JSON:
77
+ // - Explicit Accept: application/json header
78
+ // - XHR request (axios, fetch with X-Requested-With)
79
+ // - API path convention (/api/*)
80
+ static _wantsJson(req) {
81
+ return req.xhr || req.path.startsWith('/api') || (req.headers.accept && req.headers.accept.includes('application/json'))
82
+ }
83
+ }