millas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/bin/millas.js +6 -0
  4. package/package.json +56 -0
  5. package/src/admin/Admin.js +617 -0
  6. package/src/admin/index.js +13 -0
  7. package/src/admin/resources/AdminResource.js +317 -0
  8. package/src/auth/Auth.js +254 -0
  9. package/src/auth/AuthController.js +188 -0
  10. package/src/auth/AuthMiddleware.js +67 -0
  11. package/src/auth/Hasher.js +51 -0
  12. package/src/auth/JwtDriver.js +74 -0
  13. package/src/auth/RoleMiddleware.js +44 -0
  14. package/src/cache/Cache.js +231 -0
  15. package/src/cache/drivers/FileDriver.js +152 -0
  16. package/src/cache/drivers/MemoryDriver.js +158 -0
  17. package/src/cache/drivers/NullDriver.js +27 -0
  18. package/src/cache/index.js +8 -0
  19. package/src/cli.js +27 -0
  20. package/src/commands/make.js +61 -0
  21. package/src/commands/migrate.js +174 -0
  22. package/src/commands/new.js +50 -0
  23. package/src/commands/queue.js +92 -0
  24. package/src/commands/route.js +93 -0
  25. package/src/commands/serve.js +50 -0
  26. package/src/container/Application.js +177 -0
  27. package/src/container/Container.js +281 -0
  28. package/src/container/index.js +13 -0
  29. package/src/controller/Controller.js +367 -0
  30. package/src/errors/HttpError.js +29 -0
  31. package/src/events/Event.js +39 -0
  32. package/src/events/EventEmitter.js +151 -0
  33. package/src/events/Listener.js +46 -0
  34. package/src/events/index.js +15 -0
  35. package/src/index.js +93 -0
  36. package/src/mail/Mail.js +210 -0
  37. package/src/mail/MailMessage.js +196 -0
  38. package/src/mail/TemplateEngine.js +150 -0
  39. package/src/mail/drivers/LogDriver.js +36 -0
  40. package/src/mail/drivers/MailgunDriver.js +84 -0
  41. package/src/mail/drivers/SendGridDriver.js +97 -0
  42. package/src/mail/drivers/SmtpDriver.js +67 -0
  43. package/src/mail/index.js +19 -0
  44. package/src/middleware/AuthMiddleware.js +46 -0
  45. package/src/middleware/CorsMiddleware.js +59 -0
  46. package/src/middleware/LogMiddleware.js +61 -0
  47. package/src/middleware/Middleware.js +36 -0
  48. package/src/middleware/MiddlewarePipeline.js +94 -0
  49. package/src/middleware/ThrottleMiddleware.js +61 -0
  50. package/src/orm/drivers/DatabaseManager.js +135 -0
  51. package/src/orm/fields/index.js +132 -0
  52. package/src/orm/index.js +19 -0
  53. package/src/orm/migration/MigrationRunner.js +216 -0
  54. package/src/orm/migration/ModelInspector.js +338 -0
  55. package/src/orm/migration/SchemaBuilder.js +173 -0
  56. package/src/orm/model/Model.js +371 -0
  57. package/src/orm/query/QueryBuilder.js +197 -0
  58. package/src/providers/AdminServiceProvider.js +40 -0
  59. package/src/providers/AuthServiceProvider.js +53 -0
  60. package/src/providers/CacheStorageServiceProvider.js +71 -0
  61. package/src/providers/DatabaseServiceProvider.js +45 -0
  62. package/src/providers/EventServiceProvider.js +34 -0
  63. package/src/providers/MailServiceProvider.js +51 -0
  64. package/src/providers/ProviderRegistry.js +82 -0
  65. package/src/providers/QueueServiceProvider.js +52 -0
  66. package/src/providers/ServiceProvider.js +45 -0
  67. package/src/queue/Job.js +135 -0
  68. package/src/queue/Queue.js +147 -0
  69. package/src/queue/drivers/DatabaseDriver.js +194 -0
  70. package/src/queue/drivers/SyncDriver.js +72 -0
  71. package/src/queue/index.js +16 -0
  72. package/src/queue/workers/QueueWorker.js +140 -0
  73. package/src/router/MiddlewareRegistry.js +82 -0
  74. package/src/router/Route.js +255 -0
  75. package/src/router/RouteGroup.js +19 -0
  76. package/src/router/RouteRegistry.js +55 -0
  77. package/src/router/Router.js +138 -0
  78. package/src/router/index.js +15 -0
  79. package/src/scaffold/generator.js +34 -0
  80. package/src/scaffold/maker.js +272 -0
  81. package/src/scaffold/templates.js +350 -0
  82. package/src/storage/Storage.js +170 -0
  83. package/src/storage/drivers/LocalDriver.js +215 -0
  84. package/src/storage/index.js +6 -0
@@ -0,0 +1,255 @@
1
+ 'use strict';
2
+
3
+ const RouteGroup = require('./RouteGroup');
4
+ const RouteRegistry = require('./RouteRegistry');
5
+
6
+ /**
7
+ * Route
8
+ *
9
+ * The primary developer-facing API for defining routes.
10
+ *
11
+ * Usage:
12
+ * Route.get('/users', UserController, 'index')
13
+ * Route.post('/users', UserController, 'store')
14
+ * Route.resource('/users', UserController)
15
+ * Route.group({ prefix: '/api', middleware: ['auth'] }, () => { ... })
16
+ * Route.prefix('/v1').group(() => { ... })
17
+ */
18
+ class Route {
19
+ constructor() {
20
+ this._registry = new RouteRegistry();
21
+ this._groupStack = []; // stack of active group contexts
22
+ }
23
+
24
+ // ─── HTTP Verbs ─────────────────────────────────────────────────────────────
25
+
26
+ get(path, handler, method) {
27
+ return this._add('GET', path, handler, method);
28
+ }
29
+
30
+ post(path, handler, method) {
31
+ return this._add('POST', path, handler, method);
32
+ }
33
+
34
+ put(path, handler, method) {
35
+ return this._add('PUT', path, handler, method);
36
+ }
37
+
38
+ patch(path, handler, method) {
39
+ return this._add('PATCH', path, handler, method);
40
+ }
41
+
42
+ delete(path, handler, method) {
43
+ return this._add('DELETE', path, handler, method);
44
+ }
45
+
46
+ options(path, handler, method) {
47
+ return this._add('OPTIONS', path, handler, method);
48
+ }
49
+
50
+ any(path, handler, method) {
51
+ const verbs = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
52
+ verbs.forEach(v => this._add(v, path, handler, method));
53
+ }
54
+
55
+ // ─── Resource Routes ─────────────────────────────────────────────────────────
56
+ // Generates 5 conventional RESTful routes for a controller.
57
+
58
+ resource(path, ControllerClass, options = {}) {
59
+ const only = options.only;
60
+ const except = options.except || [];
61
+
62
+ const map = [
63
+ { verb: 'GET', suffix: '', action: 'index', name: `${path}.index` },
64
+ { verb: 'GET', suffix: '/:id', action: 'show', name: `${path}.show` },
65
+ { verb: 'POST', suffix: '', action: 'store', name: `${path}.store` },
66
+ { verb: 'PUT', suffix: '/:id', action: 'update', name: `${path}.update` },
67
+ { verb: 'DELETE', suffix: '/:id', action: 'destroy', name: `${path}.destroy` },
68
+ ];
69
+
70
+ for (const route of map) {
71
+ if (only && !only.includes(route.action)) continue;
72
+ if (except.includes(route.action)) continue;
73
+ this._add(route.verb, path + route.suffix, ControllerClass, route.action, route.name);
74
+ }
75
+
76
+ return this;
77
+ }
78
+
79
+ // ─── Route Groups ────────────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Route.group({ prefix, middleware, name }, callback)
83
+ * Route.group(callback) — shorthand, no attributes
84
+ */
85
+ group(attributes, callback) {
86
+ if (typeof attributes === 'function') {
87
+ callback = attributes;
88
+ attributes = {};
89
+ }
90
+
91
+ const group = new RouteGroup(attributes, this._groupStack);
92
+ this._groupStack.push(group);
93
+ callback();
94
+ this._groupStack.pop();
95
+
96
+ return this;
97
+ }
98
+
99
+ /**
100
+ * Fluent prefix() — returns a builder that defers to group()
101
+ * Route.prefix('/api/v1').middleware(['auth']).group(() => { ... })
102
+ */
103
+ prefix(prefix) {
104
+ return new RouteGroupBuilder(this, { prefix });
105
+ }
106
+
107
+ /**
108
+ * Attach middleware to the next group or route
109
+ */
110
+ middleware(middleware) {
111
+ return new RouteGroupBuilder(this, { middleware });
112
+ }
113
+
114
+ /**
115
+ * Named route prefix
116
+ */
117
+ name(name) {
118
+ return new RouteGroupBuilder(this, { name });
119
+ }
120
+
121
+ // ─── Auth convenience ────────────────────────────────────────────────────────
122
+
123
+ /**
124
+ * Register all standard auth routes under a given prefix.
125
+ *
126
+ * Route.auth() // registers under /auth
127
+ * Route.auth('/api/auth') // custom prefix
128
+ *
129
+ * Registers:
130
+ * POST /auth/register
131
+ * POST /auth/login
132
+ * POST /auth/logout
133
+ * GET /auth/me
134
+ * POST /auth/refresh
135
+ * POST /auth/forgot-password
136
+ * POST /auth/reset-password
137
+ */
138
+ auth(prefix = '/auth') {
139
+ const AuthController = require('../auth/AuthController');
140
+ this.group({ prefix }, () => {
141
+ this.post('/register', AuthController, 'register');
142
+ this.post('/login', AuthController, 'login');
143
+ this.post('/logout', AuthController, 'logout');
144
+ this.get('/me', AuthController, 'me');
145
+ this.post('/refresh', AuthController, 'refresh');
146
+ this.post('/forgot-password', AuthController, 'forgotPassword');
147
+ this.post('/reset-password', AuthController, 'resetPassword');
148
+ });
149
+ return this;
150
+ }
151
+
152
+ // ─── Internal ────────────────────────────────────────────────────────────────
153
+
154
+ _add(verb, path, handler, method, routeName) {
155
+ // Merge active group context
156
+ const context = this._mergeGroupStack();
157
+
158
+ // Build full path
159
+ const fullPath = this._joinPaths(context.prefix || '', path);
160
+
161
+ // Resolve middleware
162
+ const middleware = [
163
+ ...(context.middleware || []),
164
+ ];
165
+
166
+ // Build route name
167
+ const name = routeName
168
+ ? (context.name ? context.name + '.' + routeName : routeName)
169
+ : null;
170
+
171
+ const entry = {
172
+ verb,
173
+ path: fullPath,
174
+ handler,
175
+ method, // string method name OR raw function
176
+ middleware,
177
+ name,
178
+ };
179
+
180
+ this._registry.register(entry);
181
+ return this;
182
+ }
183
+
184
+ _mergeGroupStack() {
185
+ return this._groupStack.reduce((merged, group) => {
186
+ // Prefix: concatenate
187
+ merged.prefix = this._joinPaths(merged.prefix || '', group.prefix || '');
188
+ // Middleware: accumulate
189
+ merged.middleware = [
190
+ ...(merged.middleware || []),
191
+ ...(group.middleware || []),
192
+ ];
193
+ // Name: concatenate
194
+ merged.name = [merged.name, group.name].filter(Boolean).join('');
195
+ return merged;
196
+ }, {});
197
+ }
198
+
199
+ _joinPaths(...parts) {
200
+ return '/' + parts
201
+ .map(p => p.replace(/^\/|\/$/g, ''))
202
+ .filter(Boolean)
203
+ .join('/');
204
+ }
205
+
206
+ // ─── Public Accessors ────────────────────────────────────────────────────────
207
+
208
+ getRegistry() {
209
+ return this._registry;
210
+ }
211
+
212
+ list() {
213
+ return this._registry.all();
214
+ }
215
+ }
216
+
217
+ // ─── RouteGroupBuilder (fluent chain) ────────────────────────────────────────
218
+
219
+ class RouteGroupBuilder {
220
+ constructor(router, attrs) {
221
+ this._router = router;
222
+ this._attrs = attrs;
223
+ }
224
+
225
+ prefix(prefix) {
226
+ this._attrs.prefix = this._joinPaths(this._attrs.prefix || '', prefix);
227
+ return this;
228
+ }
229
+
230
+ middleware(middleware) {
231
+ this._attrs.middleware = [
232
+ ...(this._attrs.middleware || []),
233
+ ...(Array.isArray(middleware) ? middleware : [middleware]),
234
+ ];
235
+ return this;
236
+ }
237
+
238
+ name(name) {
239
+ this._attrs.name = (this._attrs.name || '') + name;
240
+ return this;
241
+ }
242
+
243
+ group(callback) {
244
+ return this._router.group(this._attrs, callback);
245
+ }
246
+
247
+ _joinPaths(...parts) {
248
+ return '/' + parts
249
+ .map(p => p.replace(/^\/|\/$/g, ''))
250
+ .filter(Boolean)
251
+ .join('/');
252
+ }
253
+ }
254
+
255
+ module.exports = Route;
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * RouteGroup
5
+ *
6
+ * Represents a single group context pushed onto the group stack.
7
+ * Holds prefix, middleware array, and name prefix for the group.
8
+ */
9
+ class RouteGroup {
10
+ constructor(attributes = {}) {
11
+ this.prefix = attributes.prefix || '';
12
+ this.middleware = Array.isArray(attributes.middleware)
13
+ ? attributes.middleware
14
+ : (attributes.middleware ? [attributes.middleware] : []);
15
+ this.name = attributes.name || '';
16
+ }
17
+ }
18
+
19
+ module.exports = RouteGroup;
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * RouteRegistry
5
+ *
6
+ * Central store for all registered routes.
7
+ * Used by the Router to bind to Express, and by `millas route:list`.
8
+ */
9
+ class RouteRegistry {
10
+ constructor() {
11
+ this._routes = [];
12
+ this._namedRoutes = {};
13
+ }
14
+
15
+ register(entry) {
16
+ this._routes.push(entry);
17
+
18
+ if (entry.name) {
19
+ this._namedRoutes[entry.name] = entry;
20
+ }
21
+ }
22
+
23
+ all() {
24
+ return [...this._routes];
25
+ }
26
+
27
+ findByName(name) {
28
+ return this._namedRoutes[name] || null;
29
+ }
30
+
31
+ findByPath(verb, path) {
32
+ return this._routes.find(
33
+ r => r.verb === verb.toUpperCase() && r.path === path
34
+ ) || null;
35
+ }
36
+
37
+ /**
38
+ * Return a formatted table for `millas route:list`
39
+ */
40
+ toTable() {
41
+ return this._routes.map(r => ({
42
+ Method: r.verb.padEnd(7),
43
+ Path: r.path,
44
+ Handler: r.handler
45
+ ? (typeof r.handler === 'function'
46
+ ? r.handler.name || '<closure>'
47
+ : (r.handler.name || r.handler.toString()) + (r.method ? `@${r.method}` : ''))
48
+ : '<none>',
49
+ Middleware: (r.middleware || []).join(', ') || '—',
50
+ Name: r.name || '—',
51
+ }));
52
+ }
53
+ }
54
+
55
+ module.exports = RouteRegistry;
@@ -0,0 +1,138 @@
1
+ 'use strict';
2
+
3
+ const MiddlewareRegistry = require('./MiddlewareRegistry');
4
+
5
+ /**
6
+ * Router
7
+ *
8
+ * Takes a populated RouteRegistry and binds every route
9
+ * onto a live Express app instance.
10
+ *
11
+ * Also wraps async controller methods so unhandled promise
12
+ * rejections are forwarded to Express error handlers.
13
+ */
14
+ class Router {
15
+ /**
16
+ * @param {object} expressApp — the Express application
17
+ * @param {RouteRegistry} registry
18
+ * @param {MiddlewareRegistry} middlewareRegistry
19
+ */
20
+ constructor(expressApp, registry, middlewareRegistry) {
21
+ this._app = expressApp;
22
+ this._registry = registry;
23
+ this._mw = middlewareRegistry || new MiddlewareRegistry();
24
+ }
25
+
26
+ /**
27
+ * Bind all registered routes onto the Express app.
28
+ */
29
+ mount() {
30
+ const routes = this._registry.all();
31
+
32
+ for (const route of routes) {
33
+ this._bindRoute(route);
34
+ }
35
+
36
+ // Mount 404 handler after all routes
37
+ this._app.use((req, res) => {
38
+ res.status(404).json({
39
+ error: 'Not Found',
40
+ message: `Cannot ${req.method} ${req.path}`,
41
+ status: 404,
42
+ });
43
+ });
44
+
45
+ // Mount global error handler
46
+ this._app.use((err, req, res, _next) => {
47
+ const status = err.status || err.statusCode || 500;
48
+ const message = err.message || 'Internal Server Error';
49
+
50
+ if (status >= 500 && process.env.NODE_ENV !== 'production') {
51
+ console.error(err.stack);
52
+ }
53
+
54
+ res.status(status).json({
55
+ error: status >= 500 ? 'Internal Server Error' : message,
56
+ message,
57
+ status,
58
+ // Validation errors (HttpError with errors field)
59
+ ...(err.errors && { errors: err.errors }),
60
+ // Stack trace in development for 5xx only
61
+ ...(status >= 500 && process.env.NODE_ENV !== 'production' && { stack: err.stack }),
62
+ });
63
+ });
64
+ }
65
+
66
+ // ─── Private ──────────────────────────────────────────────────────────────
67
+
68
+ _bindRoute(route) {
69
+ const verb = route.verb.toLowerCase();
70
+ const path = route.path;
71
+
72
+ // Resolve middleware chain
73
+ const mwHandlers = this._resolveMiddleware(route.middleware || []);
74
+
75
+ // Resolve the terminal handler
76
+ const terminal = this._resolveHandler(route.handler, route.method);
77
+
78
+ // Register on Express: app.get(path, [...mw], handler)
79
+ this._app[verb](path, ...mwHandlers, terminal);
80
+ }
81
+
82
+ _resolveMiddleware(list) {
83
+ return list.map(alias => {
84
+ try {
85
+ return this._mw.resolve(alias);
86
+ } catch (err) {
87
+ console.warn(`[Millas] Warning: ${err.message} — skipping.`);
88
+ return (_req, _res, next) => next();
89
+ }
90
+ });
91
+ }
92
+
93
+ _resolveHandler(handler, method) {
94
+ // Case 1: raw async/sync function
95
+ if (typeof handler === 'function' && !method) {
96
+ return this._wrapAsync(handler);
97
+ }
98
+
99
+ // Case 2: controller class + method name string
100
+ if (typeof handler === 'function' && typeof method === 'string') {
101
+ const instance = new handler();
102
+ if (typeof instance[method] !== 'function') {
103
+ throw new Error(
104
+ `Method "${method}" not found on controller "${handler.name}".`
105
+ );
106
+ }
107
+ return this._wrapAsync(instance[method].bind(instance));
108
+ }
109
+
110
+ // Case 3: already-instantiated object + method name
111
+ if (typeof handler === 'object' && handler !== null && typeof method === 'string') {
112
+ if (typeof handler[method] !== 'function') {
113
+ throw new Error(`Method "${method}" not found on handler object.`);
114
+ }
115
+ return this._wrapAsync(handler[method].bind(handler));
116
+ }
117
+
118
+ // Case 4: plain object/function with no method (fallback)
119
+ if (typeof handler === 'function') {
120
+ return this._wrapAsync(handler);
121
+ }
122
+
123
+ throw new Error(`Invalid route handler: ${JSON.stringify(handler)}`);
124
+ }
125
+
126
+ /**
127
+ * Wrap an async function so rejections are forwarded to next(err).
128
+ * Sync functions pass through unchanged.
129
+ */
130
+ _wrapAsync(fn) {
131
+ if (fn.constructor.name === 'AsyncFunction') {
132
+ return (req, res, next) => fn(req, res, next).catch(next);
133
+ }
134
+ return fn;
135
+ }
136
+ }
137
+
138
+ module.exports = Router;
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ const Route = require('./Route');
4
+ const Router = require('./Router');
5
+ const RouteRegistry = require('./RouteRegistry');
6
+ const RouteGroup = require('./RouteGroup');
7
+ const MiddlewareRegistry = require('./MiddlewareRegistry');
8
+
9
+ module.exports = {
10
+ Route,
11
+ Router,
12
+ RouteRegistry,
13
+ RouteGroup,
14
+ MiddlewareRegistry,
15
+ };
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const { getProjectFiles } = require('./templates');
6
+
7
+ /**
8
+ * Generates a full Millas project at the given targetDir.
9
+ */
10
+ async function generateProject(projectName, targetDir) {
11
+ const files = getProjectFiles(projectName);
12
+
13
+ for (const [filePath, content] of Object.entries(files)) {
14
+ const fullPath = path.join(targetDir, filePath);
15
+ await fs.ensureDir(path.dirname(fullPath));
16
+ await fs.writeFile(fullPath, content, 'utf8');
17
+ }
18
+
19
+ // Create empty directories that need to exist but have no initial files
20
+ const emptyDirs = [
21
+ 'storage/logs',
22
+ 'storage/uploads',
23
+ 'database/migrations',
24
+ 'database/seeders',
25
+ 'tests',
26
+ ];
27
+
28
+ for (const dir of emptyDirs) {
29
+ await fs.ensureDir(path.join(targetDir, dir));
30
+ await fs.writeFile(path.join(targetDir, dir, '.gitkeep'), '', 'utf8');
31
+ }
32
+ }
33
+
34
+ module.exports = { generateProject };