millas 0.2.13 → 0.2.14
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 +14 -1
- 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
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ApiResource / ApiEndpoint / ApiField
|
|
5
|
+
*
|
|
6
|
+
* The three classes developers use to document their API.
|
|
7
|
+
* Mirrors the Admin panel's AdminResource/AdminField pattern.
|
|
8
|
+
*
|
|
9
|
+
* ── Minimal (grouping only, zero boilerplate) ────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* class UserApiResource extends ApiResource {
|
|
12
|
+
* static controller = UserController;
|
|
13
|
+
* static label = 'Users';
|
|
14
|
+
* static group = 'Auth & Users';
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* ── Full ─────────────────────────────────────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* class UserApiResource extends ApiResource {
|
|
20
|
+
* static controller = UserController;
|
|
21
|
+
* static label = 'Users';
|
|
22
|
+
* static group = 'Auth & Users';
|
|
23
|
+
* static prefix = '/api/v1';
|
|
24
|
+
* static description = 'Manage users and sessions.';
|
|
25
|
+
*
|
|
26
|
+
* static endpoints() {
|
|
27
|
+
* return [
|
|
28
|
+
* ApiEndpoint.post('/auth/register')
|
|
29
|
+
* .label('Register')
|
|
30
|
+
* .body({
|
|
31
|
+
* name: ApiField.text().required().example('Jane Doe'),
|
|
32
|
+
* email: ApiField.email().required().example('jane@example.com'),
|
|
33
|
+
* password: ApiField.password().required(),
|
|
34
|
+
* })
|
|
35
|
+
* .response(201, { id: 1, name: 'Jane Doe', token: 'eyJ...' }),
|
|
36
|
+
*
|
|
37
|
+
* ApiEndpoint.get('/users/me')
|
|
38
|
+
* .label('Get current user')
|
|
39
|
+
* .auth()
|
|
40
|
+
* .response(200, { id: 1, name: 'Jane Doe' }),
|
|
41
|
+
*
|
|
42
|
+
* ApiEndpoint.patch('/users/:id')
|
|
43
|
+
* .label('Update user')
|
|
44
|
+
* .auth()
|
|
45
|
+
* .param('id', ApiField.number().example(1).description('User ID'))
|
|
46
|
+
* .body({ name: ApiField.text().example('Jane') }),
|
|
47
|
+
*
|
|
48
|
+
* ApiEndpoint.get('/users')
|
|
49
|
+
* .label('List users')
|
|
50
|
+
* .auth()
|
|
51
|
+
* .query({
|
|
52
|
+
* page: ApiField.number().example(1),
|
|
53
|
+
* search: ApiField.text().example('alice'),
|
|
54
|
+
* }),
|
|
55
|
+
* ];
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
59
|
+
* Docs.register(UserApiResource);
|
|
60
|
+
*
|
|
61
|
+
* ── Auto-discovery ────────────────────────────────────────────────────────────
|
|
62
|
+
*
|
|
63
|
+
* Routes are always auto-discovered from RouteRegistry.
|
|
64
|
+
* ApiResource enriches them — you never re-declare paths manually.
|
|
65
|
+
* If no ApiResource matches a route, it still appears in the docs panel
|
|
66
|
+
* under an "Undocumented" group with its method, path, and auth badge.
|
|
67
|
+
*/
|
|
68
|
+
class ApiResource {
|
|
69
|
+
/** Controller class this resource documents. Drives auto-matching. */
|
|
70
|
+
static controller = null;
|
|
71
|
+
|
|
72
|
+
/** Sidebar label */
|
|
73
|
+
static label = null;
|
|
74
|
+
|
|
75
|
+
/** Sidebar group — resources sharing the same group collapse together */
|
|
76
|
+
static group = null;
|
|
77
|
+
|
|
78
|
+
/** Bootstrap Icons name */
|
|
79
|
+
static icon = 'code-slash';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Path prefix applied to all endpoint paths in this resource.
|
|
83
|
+
* Allows short paths in endpoints():
|
|
84
|
+
* prefix = '/api/v1' → .post('/auth/login') resolves to /api/v1/auth/login
|
|
85
|
+
*/
|
|
86
|
+
static prefix = '';
|
|
87
|
+
|
|
88
|
+
/** Short description shown at the top of the resource section */
|
|
89
|
+
static description = null;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Base URL override for "Try it" requests in this resource.
|
|
93
|
+
* Falls back to the global env base_url.
|
|
94
|
+
*/
|
|
95
|
+
static baseUrl = null;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Define enriched endpoint declarations.
|
|
99
|
+
* Return an array of ApiEndpoint instances.
|
|
100
|
+
*/
|
|
101
|
+
static endpoints() { return []; }
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the merged endpoint list by combining declared endpoints
|
|
105
|
+
* with auto-discovered routes from the live RouteRegistry.
|
|
106
|
+
* @internal — called by Docs
|
|
107
|
+
*/
|
|
108
|
+
static _build(routeRegistry) {
|
|
109
|
+
const declared = this.endpoints();
|
|
110
|
+
const prefix = this.prefix || '';
|
|
111
|
+
|
|
112
|
+
// Index declared endpoints by (verb, normalised path)
|
|
113
|
+
const byKey = new Map();
|
|
114
|
+
for (const ep of declared) {
|
|
115
|
+
byKey.set(`${ep._verb}:${_norm(prefix + ep._path)}`, ep);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const routes = routeRegistry ? routeRegistry.all() : [];
|
|
119
|
+
const merged = [];
|
|
120
|
+
|
|
121
|
+
for (const route of routes) {
|
|
122
|
+
if (_isFrameworkRoute(route.path)) continue;
|
|
123
|
+
// If controller is set, only pick up routes handled by that controller
|
|
124
|
+
if (this.controller && route.handler !== this.controller) continue;
|
|
125
|
+
|
|
126
|
+
const key = `${route.verb.toLowerCase()}:${_norm(route.path)}`;
|
|
127
|
+
const decl = byKey.get(key);
|
|
128
|
+
|
|
129
|
+
// Declared entry wins; otherwise create a bare auto-discovered one
|
|
130
|
+
const ep = decl || new ApiEndpoint(route.verb.toLowerCase(), route.path);
|
|
131
|
+
|
|
132
|
+
if (!ep._label) ep._label = _autoLabel(route.path, route.verb);
|
|
133
|
+
|
|
134
|
+
// Auto-detect auth from registered middleware
|
|
135
|
+
if (ep._auth === null) {
|
|
136
|
+
ep._auth = (route.middleware || []).includes('auth');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ep._autoDiscovered = !decl;
|
|
140
|
+
ep._routeName = route.name || null;
|
|
141
|
+
ep._resource = this;
|
|
142
|
+
merged.push(ep);
|
|
143
|
+
byKey.delete(key);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Any declared endpoint not matched to a live route → show as "pending"
|
|
147
|
+
for (const ep of byKey.values()) {
|
|
148
|
+
ep._unmatched = true;
|
|
149
|
+
ep._resource = this;
|
|
150
|
+
merged.push(ep);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Sort: alphabetical path, then verb order within the same path
|
|
154
|
+
merged.sort((a, b) => {
|
|
155
|
+
const pa = _norm(a._path), pb = _norm(b._path);
|
|
156
|
+
if (pa !== pb) return pa.localeCompare(pb);
|
|
157
|
+
return _verbOrder(a._verb) - _verbOrder(b._verb);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return merged;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static get slug() {
|
|
164
|
+
return (this.label || this.controller?.name || 'api')
|
|
165
|
+
.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── ApiEndpoint ────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
class ApiEndpoint {
|
|
172
|
+
constructor(verb, path) {
|
|
173
|
+
this._verb = verb.toLowerCase();
|
|
174
|
+
this._path = path;
|
|
175
|
+
this._label = null;
|
|
176
|
+
this._description = null;
|
|
177
|
+
this._auth = null; // null=auto-detect true=required false=public
|
|
178
|
+
this._body = {}; // { name: ApiField }
|
|
179
|
+
this._params = {}; // path params
|
|
180
|
+
this._query = {}; // query string params
|
|
181
|
+
this._headers = {}; // custom headers
|
|
182
|
+
this._responses = []; // [{ status, description, example }]
|
|
183
|
+
this._tags = [];
|
|
184
|
+
this._deprecated = false;
|
|
185
|
+
this._bodyEncoding = 'json'; // 'json' | 'form' | 'multipart'
|
|
186
|
+
this._autoDiscovered = false;
|
|
187
|
+
this._unmatched = false;
|
|
188
|
+
this._routeName = null;
|
|
189
|
+
this._resource = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── HTTP verb factories ────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
static get(p) { return new ApiEndpoint('get', p); }
|
|
195
|
+
static post(p) { return new ApiEndpoint('post', p); }
|
|
196
|
+
static put(p) { return new ApiEndpoint('put', p); }
|
|
197
|
+
static patch(p) { return new ApiEndpoint('patch', p); }
|
|
198
|
+
static delete(p) { return new ApiEndpoint('delete', p); }
|
|
199
|
+
|
|
200
|
+
// ── Fluent modifiers ───────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/** Human-readable label in sidebar and panel header */
|
|
203
|
+
label(l) { this._label = l; return this; }
|
|
204
|
+
|
|
205
|
+
/** Paragraph description shown in endpoint detail */
|
|
206
|
+
description(d) { this._description = d; return this; }
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Mark as requiring authentication.
|
|
210
|
+
* The "Try it" panel injects the env Bearer token automatically.
|
|
211
|
+
* Pass 'bearer' | 'api-key' | 'basic' to override the type.
|
|
212
|
+
*/
|
|
213
|
+
auth(type = true) { this._auth = type; return this; }
|
|
214
|
+
|
|
215
|
+
/** Explicitly mark as public — overrides auto-detection */
|
|
216
|
+
public() { this._auth = false; return this; }
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Declare request body fields.
|
|
220
|
+
*
|
|
221
|
+
* .body({
|
|
222
|
+
* email: ApiField.email().required().example('jane@example.com'),
|
|
223
|
+
* password: ApiField.password().required(),
|
|
224
|
+
* })
|
|
225
|
+
*/
|
|
226
|
+
body(fields) { this._body = fields; return this; }
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Declare a path parameter with description / example.
|
|
230
|
+
*
|
|
231
|
+
* .param('id', ApiField.number().example(1).description('User ID'))
|
|
232
|
+
*/
|
|
233
|
+
param(name, field) { this._params[name] = field; return this; }
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Declare query string parameters.
|
|
237
|
+
*
|
|
238
|
+
* .query({
|
|
239
|
+
* page: ApiField.number().example(1),
|
|
240
|
+
* search: ApiField.text().example('alice'),
|
|
241
|
+
* })
|
|
242
|
+
*/
|
|
243
|
+
query(fields) { this._query = fields; return this; }
|
|
244
|
+
|
|
245
|
+
/** Declare custom request headers */
|
|
246
|
+
headers(fields) { this._headers = fields; return this; }
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Document a response.
|
|
250
|
+
* Call multiple times for different status codes.
|
|
251
|
+
*
|
|
252
|
+
* .response(200, { id: 1, name: 'Jane' })
|
|
253
|
+
* .response(422, { message: 'Validation failed', errors: {} })
|
|
254
|
+
*/
|
|
255
|
+
response(status, example, description = null) {
|
|
256
|
+
this._responses.push({ status, example, description });
|
|
257
|
+
return this;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Tag for future filtering */
|
|
261
|
+
tag(...tags) { this._tags.push(...tags); return this; }
|
|
262
|
+
|
|
263
|
+
/** Show a deprecated badge */
|
|
264
|
+
deprecated() { this._deprecated = true; return this; }
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Set request body encoding.
|
|
268
|
+
* 'json' (default) | 'form' | 'multipart' (file uploads)
|
|
269
|
+
*/
|
|
270
|
+
encoding(type) { this._bodyEncoding = type; return this; }
|
|
271
|
+
|
|
272
|
+
toJSON() {
|
|
273
|
+
// _resource is set by ApiResource._build() — use its prefix to build the
|
|
274
|
+
// full path the client uses for "Try it" URLs and code snippets.
|
|
275
|
+
const resourcePrefix = this._resource?.prefix || '';
|
|
276
|
+
const fullPath = resourcePrefix + this._path;
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
verb: this._verb,
|
|
280
|
+
path: fullPath, // full path incl. prefix — used for Try it & URL bar
|
|
281
|
+
shortPath: this._path, // short path without prefix — for matching & display
|
|
282
|
+
label: this._label || _autoLabel(this._path, this._verb),
|
|
283
|
+
description: this._description,
|
|
284
|
+
auth: this._auth,
|
|
285
|
+
body: _serFields(this._body),
|
|
286
|
+
params: _serFields(this._params),
|
|
287
|
+
query: _serFields(this._query),
|
|
288
|
+
headers: _serFields(this._headers),
|
|
289
|
+
responses: this._responses,
|
|
290
|
+
tags: this._tags,
|
|
291
|
+
deprecated: this._deprecated,
|
|
292
|
+
bodyEncoding: this._bodyEncoding,
|
|
293
|
+
autoDiscovered: this._autoDiscovered,
|
|
294
|
+
unmatched: this._unmatched,
|
|
295
|
+
routeName: this._routeName,
|
|
296
|
+
pathParams: _extractPathParams(fullPath),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── ApiField ───────────────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
class ApiField {
|
|
304
|
+
constructor(type) {
|
|
305
|
+
this._type = type;
|
|
306
|
+
this._required = false;
|
|
307
|
+
this._nullable = true;
|
|
308
|
+
this._example = undefined;
|
|
309
|
+
this._description = null;
|
|
310
|
+
this._default = undefined;
|
|
311
|
+
this._enum = null;
|
|
312
|
+
this._min = null;
|
|
313
|
+
this._max = null;
|
|
314
|
+
this._format = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static text() { return new ApiField('string'); }
|
|
318
|
+
static email() { return new ApiField('email'); }
|
|
319
|
+
static password() { return new ApiField('password'); }
|
|
320
|
+
static number() { return new ApiField('number'); }
|
|
321
|
+
static integer() { return new ApiField('integer'); }
|
|
322
|
+
static boolean() { return new ApiField('boolean'); }
|
|
323
|
+
static date() { return new ApiField('date'); }
|
|
324
|
+
static datetime() { return new ApiField('datetime'); }
|
|
325
|
+
static url() { return new ApiField('url'); }
|
|
326
|
+
static phone() { return new ApiField('phone'); }
|
|
327
|
+
static uuid() { return new ApiField('uuid'); }
|
|
328
|
+
static json() { return new ApiField('json'); }
|
|
329
|
+
static file() { return new ApiField('file'); }
|
|
330
|
+
static array() { return new ApiField('array'); }
|
|
331
|
+
|
|
332
|
+
static select(opts) {
|
|
333
|
+
const f = new ApiField('select');
|
|
334
|
+
f._enum = (opts || []).map(o => typeof o === 'string' ? { value: o, label: o } : o);
|
|
335
|
+
return f;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
required() { this._required = true; this._nullable = false; return this; }
|
|
339
|
+
nullable() { this._nullable = true; this._required = false; return this; }
|
|
340
|
+
example(v) { this._example = v; return this; }
|
|
341
|
+
description(d) { this._description = d; return this; }
|
|
342
|
+
default(v) { this._default = v; return this; }
|
|
343
|
+
min(n) { this._min = n; return this; }
|
|
344
|
+
max(n) { this._max = n; return this; }
|
|
345
|
+
format(f) { this._format = f; return this; }
|
|
346
|
+
enum(vals) { this._enum = vals; return this; }
|
|
347
|
+
|
|
348
|
+
toJSON() {
|
|
349
|
+
return {
|
|
350
|
+
type: this._type, required: this._required, nullable: this._nullable,
|
|
351
|
+
example: this._example, description: this._description,
|
|
352
|
+
default: this._default, enum: this._enum,
|
|
353
|
+
min: this._min, max: this._max, format: this._format,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ── Private helpers ───────────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
function _norm(p) {
|
|
361
|
+
return (p || '').replace(/\/+$/, '').toLowerCase();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function _isFrameworkRoute(p) {
|
|
365
|
+
return /^\/(admin|docs)(\/|$)/.test(p || '');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function _verbOrder(v) {
|
|
369
|
+
return { get: 0, post: 1, put: 2, patch: 3, delete: 4 }[v] ?? 5;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function _autoLabel(path, verb) {
|
|
373
|
+
const parts = (path || '')
|
|
374
|
+
.split('/')
|
|
375
|
+
.filter(p => p && !p.startsWith(':') && !/^v\d+$/.test(p) && p !== 'api');
|
|
376
|
+
const base = (parts[parts.length - 1] || 'Endpoint')
|
|
377
|
+
.replace(/-/g, ' ')
|
|
378
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
379
|
+
const hasId = (path || '').includes(':');
|
|
380
|
+
const v = (verb || '').toUpperCase();
|
|
381
|
+
if (v === 'GET' && hasId) return `Get ${base}`;
|
|
382
|
+
if (v === 'GET') return `List ${base}`;
|
|
383
|
+
if (v === 'POST') return `Create ${base}`;
|
|
384
|
+
if (v === 'PUT' || v === 'PATCH') return `Update ${base}`;
|
|
385
|
+
if (v === 'DELETE') return `Delete ${base}`;
|
|
386
|
+
return base;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function _extractPathParams(path) {
|
|
390
|
+
return ((path || '').match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g) || []).map(m => m.slice(1));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function _serFields(fields) {
|
|
394
|
+
if (!fields || typeof fields !== 'object') return {};
|
|
395
|
+
const out = {};
|
|
396
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
397
|
+
out[k] = (v && typeof v.toJSON === 'function') ? v.toJSON() : v;
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
module.exports = { ApiResource, ApiEndpoint, ApiField };
|