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.
- package/package.json +6 -3
- package/src/admin/Admin.js +107 -1027
- package/src/admin/AdminAuth.js +1 -1
- package/src/admin/ViewContext.js +1 -1
- package/src/admin/handlers/ActionHandler.js +103 -0
- package/src/admin/handlers/ApiHandler.js +113 -0
- package/src/admin/handlers/AuthHandler.js +76 -0
- package/src/admin/handlers/ExportHandler.js +70 -0
- package/src/admin/handlers/InlineHandler.js +71 -0
- package/src/admin/handlers/PageHandler.js +351 -0
- package/src/admin/resources/AdminResource.js +22 -1
- package/src/admin/static/SelectFilter2.js +34 -0
- package/src/admin/static/actions.js +201 -0
- package/src/admin/static/admin.css +7 -0
- package/src/admin/static/change_form.js +585 -0
- package/src/admin/static/core.js +128 -0
- package/src/admin/static/login.js +76 -0
- package/src/admin/static/vendor/bi/bootstrap-icons.min.css +5 -0
- package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff +0 -0
- package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff2 +0 -0
- package/src/admin/static/vendor/jquery.min.js +2 -0
- package/src/admin/views/layouts/base.njk +30 -113
- package/src/admin/views/pages/detail.njk +10 -9
- package/src/admin/views/pages/form.njk +4 -4
- package/src/admin/views/pages/list.njk +11 -193
- package/src/admin/views/pages/login.njk +19 -64
- package/src/admin/views/partials/form-field.njk +1 -1
- package/src/admin/views/partials/form-scripts.njk +4 -478
- package/src/admin/views/partials/form-widget.njk +10 -10
- package/src/ai/AITokenBudget.js +1 -1
- package/src/auth/Auth.js +112 -3
- package/src/auth/AuthMiddleware.js +18 -15
- package/src/auth/Hasher.js +15 -43
- package/src/cli.js +3 -0
- package/src/commands/call.js +190 -0
- package/src/commands/createsuperuser.js +3 -4
- package/src/commands/key.js +97 -0
- package/src/commands/make.js +16 -2
- package/src/commands/new.js +16 -1
- package/src/commands/serve.js +5 -5
- package/src/console/Command.js +337 -0
- package/src/console/CommandLoader.js +165 -0
- package/src/console/index.js +6 -0
- package/src/container/AppInitializer.js +48 -1
- package/src/container/Application.js +3 -1
- package/src/container/HttpServer.js +0 -1
- package/src/container/MillasConfig.js +48 -0
- package/src/controller/Controller.js +13 -11
- package/src/core/docs.js +6 -0
- package/src/core/foundation.js +8 -0
- package/src/core/http.js +20 -10
- package/src/core/validation.js +58 -27
- package/src/docs/Docs.js +268 -0
- package/src/docs/DocsServiceProvider.js +80 -0
- package/src/docs/SchemaInferrer.js +131 -0
- package/src/docs/handlers/ApiHandler.js +305 -0
- package/src/docs/handlers/PageHandler.js +47 -0
- package/src/docs/index.js +13 -0
- package/src/docs/resources/ApiResource.js +402 -0
- package/src/docs/static/docs.css +723 -0
- package/src/docs/static/docs.js +1181 -0
- package/src/encryption/Encrypter.js +381 -0
- package/src/facades/Auth.js +5 -2
- package/src/facades/Crypt.js +166 -0
- package/src/facades/Docs.js +43 -0
- package/src/facades/Mail.js +1 -1
- package/src/http/MillasRequest.js +7 -31
- package/src/http/RequestContext.js +11 -7
- package/src/http/SecurityBootstrap.js +24 -2
- package/src/http/Shape.js +168 -0
- package/src/http/adapters/ExpressAdapter.js +9 -5
- package/src/middleware/CorsMiddleware.js +3 -0
- package/src/middleware/ThrottleMiddleware.js +10 -7
- package/src/orm/model/Model.js +20 -2
- package/src/providers/EncryptionServiceProvider.js +66 -0
- package/src/router/MiddlewareRegistry.js +79 -54
- package/src/router/Route.js +9 -4
- package/src/router/RouteEntry.js +91 -0
- package/src/router/Router.js +71 -1
- package/src/scaffold/maker.js +138 -1
- package/src/scaffold/templates.js +12 -0
- package/src/serializer/Serializer.js +239 -0
- package/src/support/Str.js +1080 -0
- package/src/validation/BaseValidator.js +45 -5
- package/src/validation/Validator.js +67 -61
- package/src/validation/types.js +490 -0
- package/src/middleware/AuthMiddleware.js +0 -46
- 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
|
|
12
|
-
*
|
|
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(
|
|
17
|
-
* const users = await User.
|
|
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(
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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;
|
package/src/core/docs.js
ADDED
|
@@ -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;
|
package/src/core/foundation.js
CHANGED
|
@@ -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
|
|
2
|
-
const Middleware
|
|
3
|
-
const MiddlewarePipeline = require(
|
|
4
|
-
const CorsMiddleware
|
|
5
|
-
const ThrottleMiddleware = require(
|
|
6
|
-
const LogMiddleware
|
|
7
|
-
const 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
|
-
|
|
10
|
-
|
|
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
|
+
};
|
package/src/core/validation.js
CHANGED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
};
|
package/src/docs/Docs.js
ADDED
|
@@ -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;
|