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,19 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ app: require('@config/app.config'),
5
+ cors: require('@config/cors.config'),
6
+ socket: require('@config/socket.config'),
7
+ db: require('@config/database.config'),
8
+ jwt: require('@config/jwt.config'),
9
+ bcrypt: require('@config/bcrypt.config'),
10
+ storage: require('@config/storage.config'),
11
+ logger: require('@config/logger.config'),
12
+ runtime: require('@config/runtime.config'),
13
+ cache: require('@config/cache.config'),
14
+ redis: require('@config/redis.config'),
15
+ cron: require('@config/cron.config'),
16
+ queue: require('@config/queue.config'),
17
+ mail: require('@config/mail.config'),
18
+ express: require('@config/express.config'),
19
+ }
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ const Common = require('@core/common.core')
4
+
5
+ module.exports = {
6
+ secret: Common.getEnv('JWT_SECRET', null),
7
+ expiresIn: Common.getEnv('JWT_EXPIRES_IN', '1d'),
8
+ refreshSecret: Common.getEnv('JWT_REFRESH_SECRET', null),
9
+ refreshExpiresIn: Common.getEnv('JWT_REFRESH_EXPIRES_IN', '7d'),
10
+ }
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ const Common = require('@core/common.core')
4
+
5
+ module.exports = {
6
+ level: Common.getEnv('LOG_LEVEL', 'debug'),
7
+ file: Common.getEnv('LOG_FILE', 'logs'),
8
+ maxSize: Common.getEnv('LOG_MAX_SIZE', '20m'),
9
+ maxFiles: Common.getEnv('LOG_MAX_FILES', '14d'),
10
+ }
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ const Common = require('@core/common.core')
4
+
5
+ module.exports = {
6
+ enabled: Common.getEnvBool('MAIL_ENABLED', false),
7
+ host: Common.getEnv('MAIL_HOST', null),
8
+ port: Common.getEnvInt('MAIL_PORT', 587),
9
+ secure: Common.getEnvBool('MAIL_SECURE', false),
10
+ username: Common.getEnv('MAIL_USERNAME', null),
11
+ password: Common.getEnv('MAIL_PASSWORD', null),
12
+ fromAddress: Common.getEnv('MAIL_FROM_ADDRESS', null),
13
+ fromName: Common.getEnv('MAIL_FROM_NAME', null),
14
+ }
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ const Common = require('@core/common.core')
4
+
5
+ module.exports = {
6
+ enabled: Common.getEnvBool('QUEUE_ENABLED', true),
7
+ concurrency: Common.getEnvInt('QUEUE_CONCURRENCY', 5),
8
+ maxRetries: Common.getEnvInt('QUEUE_MAX_RETRIES', 3),
9
+ retryDelay: Common.getEnvInt('QUEUE_RETRY_DELAY', 1000),
10
+ persist: Common.getEnvBool('QUEUE_PERSIST', true),
11
+ }
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+ const Common = require('@core/common.core')
4
+
5
+ // Konfigurasi Redis (standalone). `redis.core` membuat koneksi hanya bila
6
+ // `enabled=true`. Pemakaian Redis oleh fitur lain bersifat OPSIONAL dan diatur
7
+ // lewat flag `use.*` — masing-masing core (cache/queue/socket/session) memeriksa
8
+ // `config.redis.enabled && config.redis.use.<fitur>` sebelum memakai Redis,
9
+ // dan jatuh ke implementasi in-memory/default bila false.
10
+ module.exports = {
11
+ enabled: Common.getEnvBool('REDIS_ENABLED', false),
12
+
13
+ host: Common.getEnv('REDIS_HOST', '127.0.0.1'),
14
+ port: Common.getEnvInt('REDIS_PORT', 6379),
15
+ username: Common.getEnv('REDIS_USERNAME', null),
16
+ password: Common.getEnv('REDIS_PASSWORD', null),
17
+ db: Common.getEnvInt('REDIS_DB', 0),
18
+ keyPrefix: Common.getEnv('REDIS_KEY_PREFIX', ''),
19
+ tls: Common.getEnvBool('REDIS_TLS', false),
20
+
21
+ // Reconnect/retry (dipakai ioredis)
22
+ maxRetriesPerRequest: Common.getEnvInt('REDIS_MAX_RETRIES', 3),
23
+ connectTimeout: Common.getEnvInt('REDIS_CONNECT_TIMEOUT', 10000),
24
+
25
+ // Toggle pemakaian Redis per fitur (hanya berlaku bila enabled=true)
26
+ use: {
27
+ cache: Common.getEnvBool('REDIS_USE_CACHE', false),
28
+ queue: Common.getEnvBool('REDIS_USE_QUEUE', false),
29
+ socket: Common.getEnvBool('REDIS_USE_SOCKET', false),
30
+ session: Common.getEnvBool('REDIS_USE_SESSION', false),
31
+ },
32
+ }
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ const Common = require('@core/common.core')
4
+
5
+ module.exports = {
6
+ maxListeners: Common.getEnvInt('RUNTIME_MAX_LISTENERS', 50),
7
+ stackTraceLimit: Common.getEnvInt('RUNTIME_STACK_TRACE_LIMIT', 50),
8
+ uvThreadpoolSize: Common.getEnvInt('RUNTIME_UV_THREADPOOL_SIZE', 4),
9
+ bigintJson: Common.getEnvBool('RUNTIME_BIGINT_JSON', true),
10
+ deprecationWarnings: Common.getEnvBool('RUNTIME_DEPRECATION_WARNINGS', true),
11
+ }
@@ -0,0 +1,15 @@
1
+ 'use strict'
2
+
3
+ const Common = require('@core/common.core')
4
+
5
+ module.exports = {
6
+ enabled: Common.getEnvBool('SOCKET_ENABLED', true),
7
+ path: Common.getEnv('SOCKET_PATH', '/socket.io/'),
8
+ serveClient: Common.getEnvBool('SOCKET_SERVE_CLIENT', false),
9
+ connectTimeout: Common.getEnvInt('SOCKET_CONNECT_TIMEOUT', 45000),
10
+ pingInterval: Common.getEnvInt('SOCKET_PING_INTERVAL', 25000),
11
+ pingTimeout: Common.getEnvInt('SOCKET_PING_TIMEOUT', 20000),
12
+ transports: Common.getEnvArray('SOCKET_TRANSPORTS', ['polling', 'websocket']),
13
+ allowUpgrades: Common.getEnvBool('SOCKET_ALLOW_UPGRADES', true),
14
+ maxHttpBufferSize: Common.getEnvInt('SOCKET_MAX_HTTP_BUFFER_SIZE', 1e6),
15
+ }
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ const Common = require('@core/common.core')
4
+
5
+ module.exports = {
6
+ root: Common.getEnv('STORAGE_ROOT', 'storage'),
7
+ uploadPath: Common.getEnv('STORAGE_UPLOAD_PATH', 'storage/uploads'),
8
+ maxSizeMb: Common.getEnvInt('STORAGE_UPLOAD_MAX_SIZE_MB', 50),
9
+ allowedTypes: Common.getEnvArray('STORAGE_UPLOAD_ALLOWED_TYPES', ['image/jpeg', 'image/png', 'image/webp', 'application/pdf']),
10
+ }
@@ -0,0 +1,92 @@
1
+ 'use strict'
2
+
3
+ const config = require('@config/index')
4
+ const logger = require('@core/logger.core')
5
+ const ErrorHandler = require('@core/error.core')
6
+ const Hooks = require('@core/hooks.core')
7
+ const Express = require('@core/express.core')
8
+ const Socket = require('@core/socket.core')
9
+ const Cron = require('@core/cron.core')
10
+ const Queue = require('@core/queue.core')
11
+
12
+ // Orchestrates the boot sequence in a deterministic order and wires graceful
13
+ // shutdown. Optional subsystems (Redis, Database) are required lazily so a project
14
+ // that never enables them does not pay their load cost.
15
+ class Bootstrap {
16
+ static _shuttingDown = false
17
+
18
+ static async run() {
19
+ ErrorHandler.setExceptionHandler()
20
+ ErrorHandler.setRejectionHandler()
21
+
22
+ Bootstrap._banner()
23
+
24
+ try {
25
+ await Hooks.before({ config })
26
+
27
+ if (config.redis.enabled) {
28
+ await require('@core/redis.core').connect()
29
+ }
30
+
31
+ if (config.db.enabled) {
32
+ await require('@core/database.core').connect()
33
+ }
34
+
35
+ const { server } = await Express.create()
36
+ await Socket.attach(server)
37
+ await Cron.start()
38
+ await Queue.start()
39
+ await Express.listen()
40
+
41
+ await Hooks.after({ config })
42
+
43
+ logger.info(`${config.app.name} ready`)
44
+ Bootstrap._registerShutdown()
45
+ } catch (err) {
46
+ logger.error('Bootstrap failed', err)
47
+ process.exit(1)
48
+ }
49
+ }
50
+
51
+ static _banner() {
52
+ logger.info(`Starting ${config.app.name}`)
53
+ logger.debug(`env=${config.app.env} tz=${config.app.timezone} port=${config.app.port}`)
54
+ }
55
+
56
+ static _registerShutdown() {
57
+ const shutdown = async (signal) => {
58
+ if (Bootstrap._shuttingDown) return
59
+ Bootstrap._shuttingDown = true
60
+ logger.info(`Received ${signal}, shutting down`)
61
+
62
+ await Bootstrap._safe('hooks.shutdown', () => Hooks.shutdown({ config }))
63
+ await Bootstrap._safe('cron.stop', () => Cron.stop())
64
+ await Bootstrap._safe('queue.stop', () => Queue.stop())
65
+ await Bootstrap._safe('socket.close', () => Socket.close())
66
+ await Bootstrap._safe('express.close', () => Express.close())
67
+
68
+ if (config.db.enabled) {
69
+ await Bootstrap._safe('database.disconnect', () => require('@core/database.core').disconnect())
70
+ }
71
+ if (config.redis.enabled) {
72
+ await Bootstrap._safe('redis.disconnect', () => require('@core/redis.core').disconnect())
73
+ }
74
+
75
+ logger.info('Shutdown complete')
76
+ process.exit(0)
77
+ }
78
+
79
+ process.on('SIGINT', () => shutdown('SIGINT'))
80
+ process.on('SIGTERM', () => shutdown('SIGTERM'))
81
+ }
82
+
83
+ static async _safe(label, fn) {
84
+ try {
85
+ await fn()
86
+ } catch (err) {
87
+ logger.error(`Shutdown step "${label}" failed`, err)
88
+ }
89
+ }
90
+ }
91
+
92
+ module.exports = Bootstrap
@@ -0,0 +1,144 @@
1
+ 'use strict'
2
+
3
+ // Array helpers — small, predictable utilities for everyday list work.
4
+ class Arr {
5
+ // Wrap any value into an array. null/undefined become an empty array.
6
+ static wrap(value) {
7
+ if (value === null || value === undefined) return []
8
+ return Array.isArray(value) ? value : [value]
9
+ }
10
+
11
+ // Return the first element, or the value matching a predicate.
12
+ static first(arr, predicate = null, fallback = null) {
13
+ if (!Array.isArray(arr)) return fallback
14
+ if (typeof predicate === 'function') {
15
+ const found = arr.find(predicate)
16
+ return found === undefined ? fallback : found
17
+ }
18
+ return arr.length ? arr[0] : fallback
19
+ }
20
+
21
+ // Return the last element, or the value matching a predicate.
22
+ static last(arr, predicate = null, fallback = null) {
23
+ if (!Array.isArray(arr)) return fallback
24
+ if (typeof predicate === 'function') {
25
+ for (let i = arr.length - 1; i >= 0; i--) {
26
+ if (predicate(arr[i], i)) return arr[i]
27
+ }
28
+ return fallback
29
+ }
30
+ return arr.length ? arr[arr.length - 1] : fallback
31
+ }
32
+
33
+ // Remove duplicate values. With a key/selector, dedupe objects by that key.
34
+ static unique(arr, key = null) {
35
+ if (!Array.isArray(arr)) return []
36
+ if (!key) return [...new Set(arr)]
37
+ const selector = typeof key === 'function' ? key : (item) => item[key]
38
+ const seen = new Set()
39
+ return arr.filter((item) => {
40
+ const id = selector(item)
41
+ if (seen.has(id)) return false
42
+ seen.add(id)
43
+ return true
44
+ })
45
+ }
46
+
47
+ // Flatten nested arrays up to the given depth.
48
+ static flatten(arr, depth = Infinity) {
49
+ return Array.isArray(arr) ? arr.flat(depth) : []
50
+ }
51
+
52
+ // Split an array into chunks of the given size.
53
+ static chunk(arr, size = 1) {
54
+ if (!Array.isArray(arr) || size < 1) return []
55
+ const out = []
56
+ for (let i = 0; i < arr.length; i += size) {
57
+ out.push(arr.slice(i, i + size))
58
+ }
59
+ return out
60
+ }
61
+
62
+ // Group items into an object keyed by the result of a selector.
63
+ static groupBy(arr, key) {
64
+ const selector = typeof key === 'function' ? key : (item) => item[key]
65
+ return (arr || []).reduce((acc, item) => {
66
+ const group = selector(item)
67
+ ;(acc[group] = acc[group] || []).push(item)
68
+ return acc
69
+ }, {})
70
+ }
71
+
72
+ // Turn an array of objects into a lookup keyed by a property.
73
+ static keyBy(arr, key) {
74
+ const selector = typeof key === 'function' ? key : (item) => item[key]
75
+ return (arr || []).reduce((acc, item) => {
76
+ acc[selector(item)] = item
77
+ return acc
78
+ }, {})
79
+ }
80
+
81
+ // Pluck a single property out of each object.
82
+ static pluck(arr, key) {
83
+ return (arr || []).map((item) => item?.[key])
84
+ }
85
+
86
+ // Sum a numeric array, optionally via a selector.
87
+ static sum(arr, key = null) {
88
+ const selector = key ? (typeof key === 'function' ? key : (i) => i[key]) : (i) => i
89
+ return (arr || []).reduce((total, item) => total + (Number(selector(item)) || 0), 0)
90
+ }
91
+
92
+ // Average of a numeric array, optionally via a selector.
93
+ static avg(arr, key = null) {
94
+ if (!arr || !arr.length) return 0
95
+ return Arr.sum(arr, key) / arr.length
96
+ }
97
+
98
+ // Shuffle a copy of the array (Fisher–Yates).
99
+ static shuffle(arr) {
100
+ const copy = [...(arr || [])]
101
+ for (let i = copy.length - 1; i > 0; i--) {
102
+ const j = Math.floor(Math.random() * (i + 1))
103
+ ;[copy[i], copy[j]] = [copy[j], copy[i]]
104
+ }
105
+ return copy
106
+ }
107
+
108
+ // Pick one or more random elements.
109
+ static random(arr, count = 1) {
110
+ const shuffled = Arr.shuffle(arr)
111
+ return count === 1 ? shuffled[0] : shuffled.slice(0, count)
112
+ }
113
+
114
+ // Everything in `a` that is not in `b`.
115
+ static difference(a, b) {
116
+ const set = new Set(b || [])
117
+ return (a || []).filter((item) => !set.has(item))
118
+ }
119
+
120
+ // Values present in both arrays.
121
+ static intersect(a, b) {
122
+ const set = new Set(b || [])
123
+ return (a || []).filter((item) => set.has(item))
124
+ }
125
+
126
+ // Strip out null/undefined/false/empty-string entries.
127
+ static compact(arr) {
128
+ return (arr || []).filter((item) => item !== null && item !== undefined && item !== false && item !== '')
129
+ }
130
+
131
+ // A range of numbers, inclusive of both ends.
132
+ static range(start, end, step = 1) {
133
+ const out = []
134
+ if (step === 0) return out
135
+ if (start <= end) {
136
+ for (let i = start; i <= end; i += step) out.push(i)
137
+ } else {
138
+ for (let i = start; i >= end; i -= Math.abs(step)) out.push(i)
139
+ }
140
+ return out
141
+ }
142
+ }
143
+
144
+ module.exports = Arr
@@ -0,0 +1,100 @@
1
+ 'use strict'
2
+
3
+ // Process-local key/value cache with per-entry TTL. Suitable for hot data that
4
+ // can be safely recomputed; swap in a Redis driver later if you outgrow it.
5
+ const store = new Map()
6
+
7
+ // Lazily-started sweeper that drops expired keys so the Map does not grow.
8
+ let sweeper = null
9
+
10
+ function startSweeper() {
11
+ if (sweeper) return
12
+ sweeper = setInterval(() => {
13
+ const now = Date.now()
14
+ for (const [key, entry] of store) {
15
+ if (entry.expires !== 0 && entry.expires <= now) store.delete(key)
16
+ }
17
+ }, 60 * 1000)
18
+ // Do not keep the event loop alive solely for cache cleanup.
19
+ if (sweeper.unref) sweeper.unref()
20
+ }
21
+
22
+ function resolveTtl(ttlSeconds) {
23
+ if (ttlSeconds === null || ttlSeconds === undefined) {
24
+ const config = require('@config/index')
25
+ ttlSeconds = config.cache.ttl
26
+ }
27
+ return ttlSeconds === 0 ? 0 : Date.now() + ttlSeconds * 1000
28
+ }
29
+
30
+ class Cache {
31
+ // Store a value for ttlSeconds (0 = never expires; null = config default).
32
+ static set(key, value, ttlSeconds = null) {
33
+ startSweeper()
34
+ store.set(key, { value, expires: resolveTtl(ttlSeconds) })
35
+ return value
36
+ }
37
+
38
+ // Read a value, or fallback when missing/expired.
39
+ static get(key, fallback = null) {
40
+ const entry = store.get(key)
41
+ if (!entry) return fallback
42
+ if (entry.expires !== 0 && entry.expires <= Date.now()) {
43
+ store.delete(key)
44
+ return fallback
45
+ }
46
+ return entry.value
47
+ }
48
+
49
+ static has(key) {
50
+ return Cache.get(key, Symbol.for('miss')) !== Symbol.for('miss')
51
+ }
52
+
53
+ static forget(key) {
54
+ return store.delete(key)
55
+ }
56
+
57
+ static flush() {
58
+ store.clear()
59
+ }
60
+
61
+ // Return the cached value, or compute it via the resolver and cache it.
62
+ static async remember(key, ttlSeconds, resolver) {
63
+ const cached = Cache.get(key, Symbol.for('miss'))
64
+ if (cached !== Symbol.for('miss')) return cached
65
+ const value = await resolver()
66
+ Cache.set(key, value, ttlSeconds)
67
+ return value
68
+ }
69
+
70
+ // remember() with no expiry.
71
+ static async rememberForever(key, resolver) {
72
+ return Cache.remember(key, 0, resolver)
73
+ }
74
+
75
+ // Get and delete in one step.
76
+ static pull(key, fallback = null) {
77
+ const value = Cache.get(key, fallback)
78
+ store.delete(key)
79
+ return value
80
+ }
81
+
82
+ // Atomically increment/decrement a numeric entry.
83
+ static increment(key, amount = 1) {
84
+ const current = Number(Cache.get(key, 0)) || 0
85
+ const next = current + amount
86
+ const entry = store.get(key)
87
+ Cache.set(key, next, entry ? null : 0)
88
+ return next
89
+ }
90
+
91
+ static decrement(key, amount = 1) {
92
+ return Cache.increment(key, -amount)
93
+ }
94
+
95
+ static keys() {
96
+ return [...store.keys()]
97
+ }
98
+ }
99
+
100
+ module.exports = Cache
@@ -0,0 +1,173 @@
1
+ 'use strict'
2
+
3
+ // A fluent, chainable wrapper over an array — inspired by Laravel collections.
4
+ // Every transforming method returns a new Collection so chains stay immutable.
5
+ class Collection {
6
+ constructor(items = []) {
7
+ this.items = Array.isArray(items) ? [...items] : Object.values(items || {})
8
+ }
9
+
10
+ static make(items = []) {
11
+ return new Collection(items)
12
+ }
13
+
14
+ all() {
15
+ return this.items
16
+ }
17
+
18
+ count() {
19
+ return this.items.length
20
+ }
21
+
22
+ isEmpty() {
23
+ return this.items.length === 0
24
+ }
25
+
26
+ isNotEmpty() {
27
+ return this.items.length > 0
28
+ }
29
+
30
+ map(fn) {
31
+ return new Collection(this.items.map(fn))
32
+ }
33
+
34
+ filter(fn = (v) => Boolean(v)) {
35
+ return new Collection(this.items.filter(fn))
36
+ }
37
+
38
+ reject(fn) {
39
+ return new Collection(this.items.filter((v, i) => !fn(v, i)))
40
+ }
41
+
42
+ each(fn) {
43
+ this.items.forEach(fn)
44
+ return this
45
+ }
46
+
47
+ reduce(fn, initial) {
48
+ return this.items.reduce(fn, initial)
49
+ }
50
+
51
+ // Filter by a field/value pair: where('status', 'active').
52
+ where(key, value) {
53
+ return new Collection(this.items.filter((item) => item?.[key] === value))
54
+ }
55
+
56
+ whereIn(key, values) {
57
+ const set = new Set(values)
58
+ return new Collection(this.items.filter((item) => set.has(item?.[key])))
59
+ }
60
+
61
+ first(fn = null, fallback = null) {
62
+ if (fn) {
63
+ const found = this.items.find(fn)
64
+ return found === undefined ? fallback : found
65
+ }
66
+ return this.items.length ? this.items[0] : fallback
67
+ }
68
+
69
+ last(fallback = null) {
70
+ return this.items.length ? this.items[this.items.length - 1] : fallback
71
+ }
72
+
73
+ pluck(key) {
74
+ return new Collection(this.items.map((item) => item?.[key]))
75
+ }
76
+
77
+ unique(key = null) {
78
+ if (!key) return new Collection([...new Set(this.items)])
79
+ const seen = new Set()
80
+ return new Collection(
81
+ this.items.filter((item) => {
82
+ const id = item?.[key]
83
+ if (seen.has(id)) return false
84
+ seen.add(id)
85
+ return true
86
+ }),
87
+ )
88
+ }
89
+
90
+ sortBy(key, direction = 'asc') {
91
+ const selector = typeof key === 'function' ? key : (item) => item?.[key]
92
+ const sorted = [...this.items].sort((a, b) => {
93
+ const va = selector(a)
94
+ const vb = selector(b)
95
+ if (va < vb) return direction === 'asc' ? -1 : 1
96
+ if (va > vb) return direction === 'asc' ? 1 : -1
97
+ return 0
98
+ })
99
+ return new Collection(sorted)
100
+ }
101
+
102
+ groupBy(key) {
103
+ const selector = typeof key === 'function' ? key : (item) => item?.[key]
104
+ const groups = {}
105
+ for (const item of this.items) {
106
+ const g = selector(item)
107
+ ;(groups[g] = groups[g] || []).push(item)
108
+ }
109
+ return groups
110
+ }
111
+
112
+ take(count) {
113
+ return new Collection(count < 0 ? this.items.slice(count) : this.items.slice(0, count))
114
+ }
115
+
116
+ skip(count) {
117
+ return new Collection(this.items.slice(count))
118
+ }
119
+
120
+ chunk(size) {
121
+ const out = []
122
+ for (let i = 0; i < this.items.length; i += size) {
123
+ out.push(new Collection(this.items.slice(i, i + size)))
124
+ }
125
+ return new Collection(out)
126
+ }
127
+
128
+ sum(key = null) {
129
+ const selector = key ? (item) => item?.[key] : (item) => item
130
+ return this.items.reduce((total, item) => total + (Number(selector(item)) || 0), 0)
131
+ }
132
+
133
+ avg(key = null) {
134
+ return this.items.length ? this.sum(key) / this.items.length : 0
135
+ }
136
+
137
+ min(key = null) {
138
+ const selector = key ? (item) => item?.[key] : (item) => item
139
+ return this.items.length ? Math.min(...this.items.map(selector)) : null
140
+ }
141
+
142
+ max(key = null) {
143
+ const selector = key ? (item) => item?.[key] : (item) => item
144
+ return this.items.length ? Math.max(...this.items.map(selector)) : null
145
+ }
146
+
147
+ contains(value) {
148
+ return this.items.includes(value)
149
+ }
150
+
151
+ reverse() {
152
+ return new Collection([...this.items].reverse())
153
+ }
154
+
155
+ values() {
156
+ return new Collection([...this.items])
157
+ }
158
+
159
+ toArray() {
160
+ return this.items
161
+ }
162
+
163
+ toJson() {
164
+ return JSON.stringify(this.items)
165
+ }
166
+
167
+ // Make the collection itself iterable (for...of, spread).
168
+ [Symbol.iterator]() {
169
+ return this.items[Symbol.iterator]()
170
+ }
171
+ }
172
+
173
+ module.exports = Collection