millas 0.2.13 → 0.2.15

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 (88) hide show
  1. package/package.json +6 -3
  2. package/src/admin/Admin.js +107 -1027
  3. package/src/admin/AdminAuth.js +1 -1
  4. package/src/admin/ViewContext.js +1 -1
  5. package/src/admin/handlers/ActionHandler.js +103 -0
  6. package/src/admin/handlers/ApiHandler.js +113 -0
  7. package/src/admin/handlers/AuthHandler.js +76 -0
  8. package/src/admin/handlers/ExportHandler.js +70 -0
  9. package/src/admin/handlers/InlineHandler.js +71 -0
  10. package/src/admin/handlers/PageHandler.js +351 -0
  11. package/src/admin/resources/AdminResource.js +22 -1
  12. package/src/admin/static/SelectFilter2.js +34 -0
  13. package/src/admin/static/actions.js +201 -0
  14. package/src/admin/static/admin.css +7 -0
  15. package/src/admin/static/change_form.js +585 -0
  16. package/src/admin/static/core.js +128 -0
  17. package/src/admin/static/login.js +76 -0
  18. package/src/admin/static/vendor/bi/bootstrap-icons.min.css +5 -0
  19. package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff +0 -0
  20. package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff2 +0 -0
  21. package/src/admin/static/vendor/jquery.min.js +2 -0
  22. package/src/admin/views/layouts/base.njk +30 -113
  23. package/src/admin/views/pages/detail.njk +10 -9
  24. package/src/admin/views/pages/form.njk +4 -4
  25. package/src/admin/views/pages/list.njk +11 -193
  26. package/src/admin/views/pages/login.njk +19 -64
  27. package/src/admin/views/partials/form-field.njk +1 -1
  28. package/src/admin/views/partials/form-scripts.njk +4 -478
  29. package/src/admin/views/partials/form-widget.njk +10 -10
  30. package/src/ai/AITokenBudget.js +1 -1
  31. package/src/auth/Auth.js +112 -3
  32. package/src/auth/AuthMiddleware.js +18 -15
  33. package/src/auth/Hasher.js +15 -43
  34. package/src/cli.js +3 -0
  35. package/src/commands/call.js +190 -0
  36. package/src/commands/createsuperuser.js +3 -4
  37. package/src/commands/key.js +97 -0
  38. package/src/commands/make.js +16 -2
  39. package/src/commands/new.js +16 -1
  40. package/src/commands/serve.js +5 -5
  41. package/src/console/Command.js +337 -0
  42. package/src/console/CommandLoader.js +165 -0
  43. package/src/console/index.js +6 -0
  44. package/src/container/AppInitializer.js +48 -1
  45. package/src/container/Application.js +3 -1
  46. package/src/container/HttpServer.js +0 -1
  47. package/src/container/MillasConfig.js +48 -0
  48. package/src/controller/Controller.js +13 -11
  49. package/src/core/docs.js +6 -0
  50. package/src/core/foundation.js +8 -0
  51. package/src/core/http.js +20 -10
  52. package/src/core/validation.js +58 -27
  53. package/src/docs/Docs.js +268 -0
  54. package/src/docs/DocsServiceProvider.js +80 -0
  55. package/src/docs/SchemaInferrer.js +131 -0
  56. package/src/docs/handlers/ApiHandler.js +305 -0
  57. package/src/docs/handlers/PageHandler.js +47 -0
  58. package/src/docs/index.js +13 -0
  59. package/src/docs/resources/ApiResource.js +402 -0
  60. package/src/docs/static/docs.css +723 -0
  61. package/src/docs/static/docs.js +1181 -0
  62. package/src/encryption/Encrypter.js +381 -0
  63. package/src/facades/Auth.js +5 -2
  64. package/src/facades/Crypt.js +166 -0
  65. package/src/facades/Docs.js +43 -0
  66. package/src/facades/Mail.js +1 -1
  67. package/src/http/MillasRequest.js +7 -31
  68. package/src/http/RequestContext.js +11 -7
  69. package/src/http/SecurityBootstrap.js +24 -2
  70. package/src/http/Shape.js +168 -0
  71. package/src/http/adapters/ExpressAdapter.js +9 -5
  72. package/src/middleware/CorsMiddleware.js +3 -0
  73. package/src/middleware/ThrottleMiddleware.js +10 -7
  74. package/src/orm/model/Model.js +20 -2
  75. package/src/providers/EncryptionServiceProvider.js +66 -0
  76. package/src/router/MiddlewareRegistry.js +79 -54
  77. package/src/router/Route.js +9 -4
  78. package/src/router/RouteEntry.js +91 -0
  79. package/src/router/Router.js +71 -1
  80. package/src/scaffold/maker.js +138 -1
  81. package/src/scaffold/templates.js +12 -0
  82. package/src/serializer/Serializer.js +239 -0
  83. package/src/support/Str.js +1080 -0
  84. package/src/validation/BaseValidator.js +45 -5
  85. package/src/validation/Validator.js +67 -61
  86. package/src/validation/types.js +490 -0
  87. package/src/middleware/AuthMiddleware.js +0 -46
  88. package/src/middleware/MiddlewareRegistry.js +0 -106
@@ -52,6 +52,13 @@ class MillasConfig {
52
52
  // Admin panel — null means disabled, {} or options object means enabled
53
53
  admin: null,
54
54
 
55
+ // Docs panel — null means disabled, {} or options object means enabled
56
+ docs: null,
57
+
58
+ // CORS — null means disabled, true means enabled.
59
+ // All configuration comes from config/app.js cors: { ... }.
60
+ cors: null,
61
+
55
62
  // Raw adapter-level middleware (e.g. helmet, compression)
56
63
  adapterMiddleware: [],
57
64
 
@@ -130,6 +137,47 @@ class MillasConfig {
130
137
  return this;
131
138
  }
132
139
 
140
+ /**
141
+ * Enable the API documentation panel.
142
+ *
143
+ * DocsServiceProvider is registered automatically — no need to add it
144
+ * to .providers([]). The panel mounts at /docs by default.
145
+ *
146
+ * .withDocs()
147
+ * .withDocs({ prefix: '/api-docs', title: 'My API', auth: true })
148
+ *
149
+ * To register ApiResources, call Docs.register() / Docs.registerMany()
150
+ * inside AppServiceProvider.boot() or a dedicated bootstrap/docs.js.
151
+ */
152
+ withDocs(options = {}) {
153
+ this._config.docs = options;
154
+ return this;
155
+ }
156
+
157
+ /**
158
+ * Enable global CORS headers.
159
+ *
160
+ * Takes no arguments — all CORS settings are configured in config/app.js:
161
+ *
162
+ * // config/app.js
163
+ * cors: {
164
+ * origins: ['https://app.example.com'],
165
+ * credentials: true,
166
+ * methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
167
+ * headers: ['Content-Type', 'Authorization', 'X-Requested-With'],
168
+ * maxAge: 86400,
169
+ * }
170
+ *
171
+ * Omitting the cors key in config/app.js falls back to CorsMiddleware defaults
172
+ * (allow all origins, standard methods, no credentials).
173
+ *
174
+ * .withCors()
175
+ */
176
+ withCors() {
177
+ this._config.cors = true;
178
+ return this;
179
+ }
180
+
133
181
  /**
134
182
  * Disable individual core services.
135
183
  *
@@ -8,24 +8,26 @@ const { jsonify, redirect, view, text, empty } = require('../http/helpers');
8
8
  *
9
9
  * Base class for all Millas controllers.
10
10
  *
11
- * Controller methods receive a MillasRequest and return a MillasResponse
12
- * (or a plain value that the kernel auto-wraps). Express is never exposed.
11
+ * Controller methods receive a RequestContext destructured as named keys.
12
+ * Express req/res are never exposed. Return a response helper or plain value.
13
13
  *
14
14
  * Usage:
15
15
  * class UserController extends Controller {
16
- * async index(req) {
17
- * const users = await User.all();
16
+ * async index({ query }) {
17
+ * const users = await User.paginate(query.page, query.per_page);
18
18
  * return this.ok(users);
19
19
  * }
20
20
  *
21
- * async store(req) {
22
- * const data = await req.validate({
23
- * name: 'required|string',
24
- * email: 'required|email',
25
- * });
26
- * const user = await User.create(data);
21
+ * async store({ body }) {
22
+ * // body is already validated when .shape() is used on the route
23
+ * const user = await User.create(body);
27
24
  * return this.created(user);
28
25
  * }
26
+ *
27
+ * async show({ params }) {
28
+ * const user = await User.findOrFail(params.id);
29
+ * return this.ok(user);
30
+ * }
29
31
  * }
30
32
  */
31
33
  class Controller {
@@ -143,4 +145,4 @@ class Controller {
143
145
  }
144
146
  }
145
147
 
146
- module.exports = Controller;
148
+ module.exports = Controller;
@@ -0,0 +1,6 @@
1
+ // ── Docs ──────────────────────────────────────────────────────────────────────
2
+ const { Docs, ApiResource, ApiEndpoint, ApiField } = require('../docs');
3
+ module.exports.Docs = Docs;
4
+ module.exports.ApiResource = ApiResource;
5
+ module.exports.ApiEndpoint = ApiEndpoint;
6
+ module.exports.ApiField = ApiField;
@@ -41,6 +41,10 @@ const { CacheServiceProvider, StorageServiceProvider } = require('../providers/C
41
41
  // ── Storage ───────────────────────────────────────────────────────
42
42
  const Storage = require('../storage/Storage');
43
43
  const LocalDriver = require('../storage/drivers/LocalDriver');
44
+ // ── Serializer ────────────────────────────────────────────────────
45
+ const { Serializer } = require('../serializer/Serializer');
46
+ const {Str, FluentString} = require("../support/Str");
47
+
44
48
  module.exports = {
45
49
  // ── Millas HTTP layer ──────────────────────────────────────────
46
50
  MillasRequest, MillasResponse, ResponseDispatcher, RequestContext,
@@ -55,5 +59,9 @@ module.exports = {
55
59
  Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider,
56
60
  // Storage
57
61
  Storage, LocalDriver, StorageServiceProvider,
62
+ // Serializer
63
+ Serializer,
64
+ // Support
65
+ Str, FluentString,
58
66
  Log: require('../logger').Log,
59
67
  };
package/src/core/http.js CHANGED
@@ -1,11 +1,21 @@
1
- const Controller = require("../controller/Controller");
2
- const Middleware = require("../middleware/Middleware");
3
- const MiddlewarePipeline = require("../middleware/MiddlewarePipeline");
4
- const CorsMiddleware = require("../middleware/CorsMiddleware");
5
- const ThrottleMiddleware = require("../middleware/ThrottleMiddleware");
6
- const LogMiddleware = require("../middleware/LogMiddleware");
7
- const HttpError = require("../errors/HttpError");
1
+ const Controller = require('../controller/Controller');
2
+ const Middleware = require('../middleware/Middleware');
3
+ const MiddlewarePipeline = require('../middleware/MiddlewarePipeline');
4
+ const CorsMiddleware = require('../middleware/CorsMiddleware');
5
+ const ThrottleMiddleware = require('../middleware/ThrottleMiddleware');
6
+ const LogMiddleware = require('../middleware/LogMiddleware');
7
+ const HttpError = require('../errors/HttpError');
8
+ const { shape, isShape } = require('../http/Shape');
9
+
8
10
  module.exports = {
9
- Controller, Middleware, MiddlewarePipeline,
10
- CorsMiddleware, ThrottleMiddleware, LogMiddleware, HttpError,
11
- }
11
+ Controller,
12
+ Middleware,
13
+ MiddlewarePipeline,
14
+ CorsMiddleware,
15
+ ThrottleMiddleware,
16
+ LogMiddleware,
17
+ HttpError,
18
+ // Shape factory — define route input/output contracts
19
+ shape,
20
+ isShape,
21
+ };
@@ -1,29 +1,60 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * millas/core/validation
5
+ *
6
+ * Re-exports everything from the validation layer.
7
+ * Import from here in your app code — never from the internal paths.
8
+ *
9
+ * const { string, email, number, boolean, array, object, date, file } =
10
+ * require('millas/core/validation');
11
+ */
12
+
13
+ const { Validator, ValidationError } = require('../validation/Validator');
14
+ const { BaseValidator } = require('../validation/BaseValidator');
1
15
  const {
2
- Validator,
3
- BaseValidator,
4
- StringValidator,
5
- EmailValidator,
6
- NumberValidator,
7
- BooleanValidator,
8
- DateValidator,
9
- ArrayValidator,
10
- ObjectValidator,
11
- FileValidator,
12
- string,
13
- email,
14
- number,
15
- boolean,
16
- date,
17
- array,
18
- objectField,
19
- fileField
20
- } = require("../validation/Validator");
16
+ StringValidator,
17
+ EmailValidator,
18
+ NumberValidator,
19
+ BooleanValidator,
20
+ ArrayValidator,
21
+ DateValidator,
22
+ ObjectValidator,
23
+ FileValidator,
24
+ string,
25
+ email,
26
+ number,
27
+ boolean,
28
+ array,
29
+ date,
30
+ object,
31
+ file,
32
+ } = require('../validation/types');
33
+
21
34
  module.exports = {
22
- Validator,
23
- BaseValidator,
24
- StringValidator, EmailValidator, NumberValidator, BooleanValidator,
25
- DateValidator, ArrayValidator, ObjectValidator, FileValidator,
26
- string, email, number, boolean, date, array,
27
- object: objectField,
28
- file: fileField,
29
- }
35
+ // Core classes
36
+ Validator,
37
+ ValidationError,
38
+ BaseValidator,
39
+ // Typed validator classes (for instanceof checks)
40
+ StringValidator,
41
+ EmailValidator,
42
+ NumberValidator,
43
+ BooleanValidator,
44
+ ArrayValidator,
45
+ DateValidator,
46
+ ObjectValidator,
47
+ FileValidator,
48
+ // Factory functions — what developers use
49
+ string,
50
+ email,
51
+ number,
52
+ boolean,
53
+ array,
54
+ date,
55
+ object,
56
+ file,
57
+ // Aliases matching the original zip's core/validation.js exports
58
+ objectField: object,
59
+ fileField: file,
60
+ };
@@ -0,0 +1,268 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const express = require('express');
5
+ const { ApiResource } = require('./resources/ApiResource');
6
+
7
+ /**
8
+ * Docs
9
+ *
10
+ * The singleton that owns all ApiResources, configuration,
11
+ * and the Express mount logic.
12
+ *
13
+ * Lifecycle (mirrors Admin):
14
+ * 1. DocsServiceProvider.boot() calls Docs.configure({ prefix, title, ... })
15
+ * 2. Developer calls Docs.register() / Docs.registerMany() in
16
+ * AppServiceProvider.boot() or bootstrap/docs.js
17
+ * 3. AppInitializer._serve() calls Docs.mount(expressApp) after routes are mounted
18
+ *
19
+ * Usage in AppServiceProvider.boot():
20
+ *
21
+ * const { Docs } = require('millas/src/docs');
22
+ * Docs.registerMany([ UserApiResource, PropertyApiResource ]);
23
+ */
24
+ class Docs {
25
+ constructor() {
26
+ this._resources = new Map(); // slug → ApiResource class
27
+ this._config = {
28
+ prefix: '/docs',
29
+ title: 'API Docs',
30
+ enabled: true,
31
+ auth: false, // require admin session to view docs
32
+ };
33
+ this._routeRegistry = null; // injected by DocsServiceProvider
34
+ }
35
+
36
+ // ── Configuration ──────────────────────────────────────────────────────────
37
+
38
+ configure(config = {}) {
39
+ Object.assign(this._config, config);
40
+ return this;
41
+ }
42
+
43
+ setRouteRegistry(registry) {
44
+ this._routeRegistry = registry;
45
+ return this;
46
+ }
47
+
48
+ // ── Resource registration ──────────────────────────────────────────────────
49
+
50
+ register(ResourceClass) {
51
+ // Accept raw ApiResource subclass or auto-wrap anything with a controller property
52
+ if (!(ResourceClass.prototype instanceof ApiResource) &&
53
+ ResourceClass !== ApiResource) {
54
+ // Developer passed something that isn't an ApiResource — ignore
55
+ process.stderr.write(`[Docs] Skipping non-ApiResource: ${ResourceClass?.name}\n`);
56
+ return this;
57
+ }
58
+ this._resources.set(ResourceClass.slug, ResourceClass);
59
+ return this;
60
+ }
61
+
62
+ registerMany(list = []) {
63
+ list.forEach(r => this.register(r));
64
+ return this;
65
+ }
66
+
67
+ resources() {
68
+ return [...this._resources.values()];
69
+ }
70
+
71
+ // ── Mount ──────────────────────────────────────────────────────────────────
72
+
73
+ /**
74
+ * Mount the docs panel onto an Express app.
75
+ * Called automatically by AppInitializer when .withDocs() was used.
76
+ */
77
+ mount(expressApp) {
78
+ if (!this._config.enabled) return this;
79
+
80
+ const prefix = this._config.prefix;
81
+
82
+ // Static assets
83
+ const staticPath = path.join(__dirname, 'static');
84
+ expressApp.use(
85
+ prefix + '/static',
86
+ express.static(staticPath, { maxAge: '1h' })
87
+ );
88
+
89
+ const PageHandler = require('./handlers/PageHandler');
90
+ const ApiHandler = require('./handlers/ApiHandler');
91
+
92
+ const page = new PageHandler(this);
93
+ const api = new ApiHandler(this);
94
+
95
+ // ── UI routes ─────────────────────────────────────────────────────────
96
+ expressApp.get(prefix, (q, s) => page.index(q, s));
97
+ expressApp.get(prefix + '/', (q, s) => page.index(q, s));
98
+
99
+ // ── Internal API routes (used by the "Try it" panel) ──────────────────
100
+ // Returns the full docs manifest as JSON — all groups, endpoints, schemas
101
+ expressApp.get(`${prefix}/_api/manifest`, (q, s) => api.manifest(q, s));
102
+ // Proxy a real request so the browser never has CORS issues
103
+ expressApp.post(`${prefix}/_api/try`, (q, s) => api.tryRequest(q, s));
104
+ // Export: Postman collection
105
+ expressApp.get(`${prefix}/_api/export/postman`, (q, s) => api.exportPostman(q, s));
106
+ // Export: OpenAPI 3.0
107
+ expressApp.get(`${prefix}/_api/export/openapi`, (q, s) => api.exportOpenApi(q, s));
108
+
109
+ return this;
110
+ }
111
+
112
+ // ── Internal helpers ───────────────────────────────────────────────────────
113
+
114
+ /**
115
+ * Build the full manifest: all groups with their endpoints.
116
+ * Merges declared ApiResources with auto-discovered routes.
117
+ */
118
+ buildManifest() {
119
+ const registry = this._routeRegistry;
120
+ const { inferFields } = require('./SchemaInferrer');
121
+ const { ApiEndpoint } = require('./resources/ApiResource');
122
+
123
+ // ── Phase 1: Build groups from route-level shapes ─────────────────────
124
+ // Routes with .shape() / .fromShape() are the primary source of truth.
125
+ // Grouped by shape.group, then shape.label within each group.
126
+ const shapeGroups = new Map(); // groupName → [endpoint, ...]
127
+ const claimedPaths = new Set();
128
+
129
+ if (registry) {
130
+ for (const route of registry.all()) {
131
+ if (_isFrameworkRoute(route.path)) continue;
132
+ if (!route.shape) continue;
133
+
134
+ const shape = route.shape;
135
+ const verb = route.verb.toLowerCase();
136
+ const group = shape.group || 'General';
137
+ const isAuth = (route.middleware || []).includes('auth');
138
+
139
+ // Infer body + query fields from the shape validators
140
+ const bodyFields = inferFields(shape.in || {});
141
+ const queryFields = inferFields(shape.query || {});
142
+
143
+ // Build out responses
144
+ const responses = Object.entries(shape.out || {}).map(([status, example]) => ({
145
+ status: Number(status),
146
+ example: example || null,
147
+ description: null,
148
+ }));
149
+
150
+ const endpointJson = {
151
+ verb: verb,
152
+ path: route.path,
153
+ shortPath: route.path,
154
+ label: shape.label || _autoLabel(route.path, verb),
155
+ description: shape.description || null,
156
+ auth: isAuth,
157
+ body: bodyFields,
158
+ query: queryFields,
159
+ params: {},
160
+ headers: {},
161
+ responses,
162
+ tags: [],
163
+ deprecated: false,
164
+ bodyEncoding: shape.encoding || 'json',
165
+ autoDiscovered: false,
166
+ unmatched: false,
167
+ routeName: route.name || null,
168
+ pathParams: _extractPathParams(route.path),
169
+ };
170
+
171
+ if (!shapeGroups.has(group)) shapeGroups.set(group, []);
172
+ shapeGroups.get(group).push(endpointJson);
173
+ claimedPaths.add(`${verb}:${route.path}`);
174
+ }
175
+ }
176
+
177
+ const groups = [];
178
+
179
+ // Convert shapeGroups map → groups array (sorted alphabetically)
180
+ for (const [groupName, endpoints] of [...shapeGroups.entries()].sort()) {
181
+ groups.push({
182
+ slug: groupName.toLowerCase().replace(/\s+/g, '-'),
183
+ label: groupName,
184
+ group: groupName,
185
+ icon: 'code-slash',
186
+ description: null,
187
+ endpoints: endpoints.sort((a, b) => a.path.localeCompare(b.path)),
188
+ });
189
+ }
190
+
191
+ // ── Phase 2: ApiResource declarations (override / enrich) ────────────
192
+ for (const R of this._resources.values()) {
193
+ const endpoints = R._build(registry).map(ep => {
194
+ const j = ep.toJSON();
195
+ claimedPaths.add(`${j.verb}:${j.path}`);
196
+ return j;
197
+ });
198
+
199
+ groups.push({
200
+ slug: R.slug,
201
+ label: R.label || R.controller?.name || R.slug,
202
+ group: R.group,
203
+ icon: R.icon,
204
+ description: R.description,
205
+ endpoints,
206
+ });
207
+ }
208
+
209
+ // ── Phase 3: Auto-discovered routes (no shape, no ApiResource) ────────
210
+ const undocumented = [];
211
+ if (registry) {
212
+ for (const route of registry.all()) {
213
+ if (_isFrameworkRoute(route.path)) continue;
214
+ const key = `${route.verb.toLowerCase()}:${route.path}`;
215
+ if (claimedPaths.has(key)) continue;
216
+
217
+ const ep = new ApiEndpoint(route.verb.toLowerCase(), route.path);
218
+ ep._auth = (route.middleware || []).includes('auth');
219
+ ep._autoDiscovered = true;
220
+ ep._routeName = route.name || null;
221
+ undocumented.push(ep.toJSON());
222
+ }
223
+ }
224
+
225
+ if (undocumented.length) {
226
+ groups.push({
227
+ slug: '_undocumented',
228
+ label: 'Undocumented',
229
+ group: null,
230
+ icon: 'question-circle',
231
+ description: 'Routes with no .shape() declaration. Add .shape() to document them.',
232
+ endpoints: undocumented,
233
+ });
234
+ }
235
+
236
+ return {
237
+ title: this._config.title,
238
+ prefix: this._config.prefix,
239
+ groups,
240
+ };
241
+ }
242
+ }
243
+
244
+ function _isFrameworkRoute(p) {
245
+ return /^\/(admin|docs)(\/|$)/.test(p || '');
246
+ }
247
+
248
+ function _autoLabel(path, verb) {
249
+ const parts = (path || '')
250
+ .split('/').filter(p => p && !p.startsWith(':') && !/^v\d+$/.test(p) && p !== 'api');
251
+ const base = (parts[parts.length - 1] || 'Endpoint')
252
+ .replace(/-/g, ' ').replace(/\w/g, c => c.toUpperCase());
253
+ const hasId = (path || '').includes(':');
254
+ const v = (verb || '').toUpperCase();
255
+ if (v === 'GET' && hasId) return 'Get ' + base;
256
+ if (v === 'GET') return 'List ' + base;
257
+ if (v === 'POST') return 'Create ' + base;
258
+ if (v === 'PUT' || v === 'PATCH') return 'Update ' + base;
259
+ if (v === 'DELETE') return 'Delete ' + base;
260
+ return base;
261
+ }
262
+
263
+ function _extractPathParams(path) {
264
+ return ((path || '').match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g) || []).map(function(m) { return m.slice(1); });
265
+ }
266
+
267
+ // Singleton export — mirrors Admin
268
+ module.exports = new Docs();
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const ServiceProvider = require('../providers/ServiceProvider');
4
+ const Docs = require('./Docs');
5
+ const { ApiResource, ApiEndpoint, ApiField } = require('./resources/ApiResource');
6
+
7
+ /**
8
+ * DocsServiceProvider
9
+ *
10
+ * Boots the docs panel and wires it to the live RouteRegistry.
11
+ *
12
+ * ── Usage (bootstrap/app.js) ─────────────────────────────────────────────────
13
+ *
14
+ * module.exports = Millas.config()
15
+ * .providers([AppServiceProvider])
16
+ * .withDocs()
17
+ * .create();
18
+ *
19
+ * ── Optional config/docs.js ──────────────────────────────────────────────────
20
+ *
21
+ * module.exports = {
22
+ * prefix: '/docs',
23
+ * title: 'My App API',
24
+ * enabled: process.env.NODE_ENV !== 'production',
25
+ * auth: false, // set true to require admin login
26
+ * };
27
+ *
28
+ * ── Registering ApiResources ─────────────────────────────────────────────────
29
+ *
30
+ * // In AppServiceProvider.boot():
31
+ * const { Docs } = require('millas/src/docs');
32
+ * Docs.registerMany([ UserApiResource, PropertyApiResource ]);
33
+ *
34
+ * // Or in a dedicated bootstrap/docs.js:
35
+ * const { Docs } = require('millas/src/docs');
36
+ * require('../app/docs')(Docs); // passes Docs to a registration file
37
+ */
38
+ class DocsServiceProvider extends ServiceProvider {
39
+ register(container) {
40
+ container.instance('Docs', Docs);
41
+ container.instance('ApiResource', ApiResource);
42
+ container.instance('ApiEndpoint', ApiEndpoint);
43
+ container.instance('ApiField', ApiField);
44
+ }
45
+
46
+ async boot(container) {
47
+ const basePath = container.make('basePath') || process.cwd();
48
+
49
+ // Load optional config/docs.js
50
+ let docsConfig = {};
51
+ try { docsConfig = require(basePath + '/config/docs'); } catch { /* optional */ }
52
+
53
+ // Resolve the admin prefix so PageHandler can point BI CSS at the
54
+ // admin's local vendor directory instead of an external CDN.
55
+ let adminPrefix = '/admin';
56
+ try {
57
+ const adminConfig = require(basePath + '/config/admin');
58
+ if (adminConfig.prefix) adminPrefix = adminConfig.prefix;
59
+ } catch { /* no config/admin.js — use default */ }
60
+
61
+ Docs.configure({
62
+ prefix: docsConfig.prefix || '/docs',
63
+ title: docsConfig.title || process.env.APP_NAME || 'API Docs',
64
+ enabled: docsConfig.enabled !== undefined ? docsConfig.enabled : (process.env.NODE_ENV !== 'production'),
65
+ auth: docsConfig.auth || false,
66
+ adminPrefix,
67
+ ...docsConfig,
68
+ });
69
+
70
+ // Wire the live RouteRegistry so Docs can auto-discover routes at mount time
71
+ try {
72
+ const app = container.make('app');
73
+ if (app && app.route && typeof app.route.getRegistry === 'function') {
74
+ Docs.setRouteRegistry(app.route.getRegistry());
75
+ }
76
+ } catch { /* app not yet fully wired — will be set during mount */ }
77
+ }
78
+ }
79
+
80
+ module.exports = DocsServiceProvider;