millas 0.2.11 → 0.2.12-beta-1
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 -5
- package/src/auth/Auth.js +13 -8
- package/src/auth/AuthController.js +45 -134
- package/src/auth/AuthMiddleware.js +12 -23
- package/src/auth/AuthUser.js +98 -0
- package/src/auth/RoleMiddleware.js +7 -17
- package/src/cli.js +1 -1
- package/src/commands/migrate.js +46 -31
- package/src/commands/serve.js +238 -38
- package/src/container/AppInitializer.js +158 -0
- package/src/container/Application.js +288 -183
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +23 -280
- package/src/container/MillasConfig.js +163 -0
- package/src/controller/Controller.js +79 -300
- package/src/core/auth.js +9 -0
- package/src/core/db.js +8 -0
- package/src/core/foundation.js +67 -0
- package/src/core/http.js +11 -0
- package/src/core/mail.js +6 -0
- package/src/core/queue.js +7 -0
- package/src/core/validation.js +29 -0
- package/src/errors/ErrorRenderer.js +640 -0
- package/src/facades/Admin.js +49 -0
- package/src/facades/Auth.js +29 -0
- package/src/facades/Cache.js +28 -0
- package/src/facades/Database.js +43 -0
- package/src/facades/Events.js +25 -0
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +51 -0
- package/src/facades/Log.js +32 -0
- package/src/facades/Mail.js +35 -0
- package/src/facades/Queue.js +30 -0
- package/src/facades/Storage.js +25 -0
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -0
- package/src/http/MillasRequest.js +253 -0
- package/src/http/MillasResponse.js +196 -0
- package/src/http/RequestContext.js +176 -0
- package/src/http/ResponseDispatcher.js +51 -0
- package/src/http/UrlGenerator.js +375 -0
- package/src/http/WelcomePage.js +273 -0
- package/src/http/adapters/ExpressAdapter.js +315 -0
- package/src/http/adapters/HttpAdapter.js +168 -0
- package/src/http/adapters/index.js +9 -0
- package/src/http/helpers.js +164 -0
- package/src/http/index.js +13 -0
- package/src/index.js +5 -91
- package/src/logger/formatters/PrettyFormatter.js +15 -5
- package/src/logger/internal.js +76 -0
- package/src/logger/patchConsole.js +145 -0
- package/src/middleware/CorsMiddleware.js +22 -30
- package/src/middleware/LogMiddleware.js +27 -59
- package/src/middleware/Middleware.js +24 -15
- package/src/middleware/MiddlewarePipeline.js +30 -67
- package/src/middleware/MiddlewareRegistry.js +106 -0
- package/src/middleware/ThrottleMiddleware.js +22 -26
- package/src/orm/fields/index.js +124 -56
- package/src/orm/migration/ModelInspector.js +339 -336
- package/src/orm/model/Model.js +96 -6
- package/src/orm/query/QueryBuilder.js +141 -3
- package/src/providers/AuthServiceProvider.js +9 -5
- package/src/providers/CacheStorageServiceProvider.js +3 -1
- package/src/providers/EventServiceProvider.js +2 -1
- package/src/providers/LogServiceProvider.js +88 -17
- package/src/providers/MailServiceProvider.js +3 -2
- package/src/providers/ProviderRegistry.js +14 -1
- package/src/providers/QueueServiceProvider.js +3 -2
- package/src/providers/ServiceProvider.js +40 -8
- package/src/router/Router.js +121 -222
- package/src/scaffold/maker.js +24 -59
- package/src/scaffold/templates.js +21 -19
- package/src/validation/BaseValidator.js +193 -0
- package/src/validation/Validator.js +680 -0
|
@@ -1,93 +1,56 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const MillasRequest = require('../http/MillasRequest');
|
|
4
|
+
const RequestContext = require('../http/RequestContext');
|
|
5
|
+
const MillasResponse = require('../http/MillasResponse');
|
|
6
|
+
const ResponseDispatcher = require('../http/ResponseDispatcher');
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* MiddlewarePipeline
|
|
5
10
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Each handler can be:
|
|
10
|
-
* - A Middleware subclass (instantiated automatically)
|
|
11
|
-
* - An already-instantiated Middleware object
|
|
12
|
-
* - A raw Express function (req, res, next) => {}
|
|
11
|
+
* Runs an ordered list of middleware instances against a request.
|
|
12
|
+
* Used for programmatic pipelines outside of the router (e.g. queue webhooks).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* const pipeline = new MiddlewarePipeline([AuthMiddleware, LogMiddleware]);
|
|
16
|
-
* app.use(pipeline.compose());
|
|
14
|
+
* Each middleware receives a RequestContext and a next() function.
|
|
17
15
|
*/
|
|
18
16
|
class MiddlewarePipeline {
|
|
19
|
-
constructor(
|
|
20
|
-
this.
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Add a handler to the end of the pipeline.
|
|
25
|
-
*/
|
|
26
|
-
pipe(handler) {
|
|
27
|
-
this._handlers.push(handler);
|
|
28
|
-
return this;
|
|
17
|
+
constructor(middlewares = []) {
|
|
18
|
+
this._middlewares = middlewares;
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*/
|
|
34
|
-
prepend(handler) {
|
|
35
|
-
this._handlers.unshift(handler);
|
|
21
|
+
add(middleware) {
|
|
22
|
+
this._middlewares.push(middleware);
|
|
36
23
|
return this;
|
|
37
24
|
}
|
|
38
25
|
|
|
39
26
|
/**
|
|
40
|
-
*
|
|
27
|
+
* Run the pipeline against an Express req/res.
|
|
28
|
+
* @param {object} expressReq
|
|
29
|
+
* @param {object} expressRes
|
|
30
|
+
* @param {object|null} container
|
|
41
31
|
*/
|
|
42
|
-
|
|
43
|
-
const
|
|
32
|
+
async run(expressReq, expressRes, container = null) {
|
|
33
|
+
const millaReq = new MillasRequest(expressReq);
|
|
34
|
+
const ctx = new RequestContext(millaReq, container);
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
const dispatch = async (index) => {
|
|
37
|
+
if (index >= this._middlewares.length) return null;
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const fn = fns[i];
|
|
39
|
+
const mw = this._middlewares[index];
|
|
40
|
+
const next = () => dispatch(index + 1);
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
dispatch(i + 1);
|
|
56
|
-
});
|
|
57
|
-
// Handle async middleware that returns a Promise
|
|
58
|
-
if (result && typeof result.catch === 'function') {
|
|
59
|
-
result.catch(next);
|
|
60
|
-
}
|
|
61
|
-
} catch (err) {
|
|
62
|
-
next(err);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
42
|
+
const instance = typeof mw === 'function' && mw.prototype?.handle
|
|
43
|
+
? new mw()
|
|
44
|
+
: mw;
|
|
65
45
|
|
|
66
|
-
|
|
46
|
+
return instance.handle(ctx, next);
|
|
67
47
|
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Resolve a handler to a plain (req, res, next) => {} function.
|
|
72
|
-
*/
|
|
73
|
-
_resolve(handler) {
|
|
74
|
-
// Raw Express function
|
|
75
|
-
if (typeof handler === 'function' && !(handler.prototype instanceof require('./Middleware'))) {
|
|
76
|
-
return handler;
|
|
77
|
-
}
|
|
78
48
|
|
|
79
|
-
|
|
80
|
-
if (typeof handler === 'function' && handler.prototype instanceof require('./Middleware')) {
|
|
81
|
-
const instance = new handler();
|
|
82
|
-
return (req, res, next) => instance.handle(req, res, next);
|
|
83
|
-
}
|
|
49
|
+
const response = await dispatch(0);
|
|
84
50
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return (req, res, next) => handler.handle(req, res, next);
|
|
51
|
+
if (response && MillasResponse.isResponse(response) && !expressRes.headersSent) {
|
|
52
|
+
ResponseDispatcher.dispatch(response, expressRes);
|
|
88
53
|
}
|
|
89
|
-
|
|
90
|
-
throw new Error(`Invalid middleware: ${handler}. Must be a Middleware class, instance, or function.`);
|
|
91
54
|
}
|
|
92
55
|
}
|
|
93
56
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MiddlewareRegistry
|
|
5
|
+
*
|
|
6
|
+
* Maps string aliases → Millas middleware classes or instances.
|
|
7
|
+
* Resolution produces adapter-native handler functions via the adapter,
|
|
8
|
+
* so this class has zero knowledge of Express (or any HTTP engine).
|
|
9
|
+
*
|
|
10
|
+
* The adapter is injected at resolution time (not construction time)
|
|
11
|
+
* so the registry can be built before the adapter exists.
|
|
12
|
+
*/
|
|
13
|
+
class MiddlewareRegistry {
|
|
14
|
+
constructor() {
|
|
15
|
+
this._map = {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a middleware alias.
|
|
20
|
+
*
|
|
21
|
+
* registry.register('auth', AuthMiddleware)
|
|
22
|
+
* registry.register('throttle', new ThrottleMiddleware({ max: 60 }))
|
|
23
|
+
*/
|
|
24
|
+
register(alias, handler) {
|
|
25
|
+
this._map[alias] = handler;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a middleware alias or class/instance into an adapter-native handler.
|
|
31
|
+
*
|
|
32
|
+
* @param {string|Function|object} aliasOrFn
|
|
33
|
+
* @param {import('../http/adapters/HttpAdapter')} adapter
|
|
34
|
+
* @param {object|null} container
|
|
35
|
+
* @returns {Function} adapter-native handler
|
|
36
|
+
*/
|
|
37
|
+
resolve(aliasOrFn, adapter, container = null) {
|
|
38
|
+
const Handler = typeof aliasOrFn === 'string'
|
|
39
|
+
? this._map[aliasOrFn]
|
|
40
|
+
: aliasOrFn;
|
|
41
|
+
|
|
42
|
+
if (!Handler) {
|
|
43
|
+
throw new Error(`Middleware "${aliasOrFn}" is not registered.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return this._wrap(Handler, adapter, container);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve all aliases in a list.
|
|
51
|
+
*/
|
|
52
|
+
resolveAll(list = [], adapter, container = null) {
|
|
53
|
+
return list.map(m => this.resolve(m, adapter, container));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Return a no-op passthrough handler for the given adapter.
|
|
58
|
+
* Used when a middleware alias is missing but should not crash the app.
|
|
59
|
+
*/
|
|
60
|
+
resolvePassthrough(adapter) {
|
|
61
|
+
// Adapter-agnostic: return a function matching the native signature
|
|
62
|
+
// by asking the adapter to wrap a no-op middleware instance.
|
|
63
|
+
return adapter.wrapMiddleware({
|
|
64
|
+
handle: (_ctx, next) => next(),
|
|
65
|
+
}, null);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
has(alias) {
|
|
69
|
+
return Object.prototype.hasOwnProperty.call(this._map, alias);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
all() {
|
|
73
|
+
return { ...this._map };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Internal ────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
_wrap(Handler, adapter, container) {
|
|
79
|
+
// Pre-instantiated Millas middleware object with handle()
|
|
80
|
+
if (
|
|
81
|
+
typeof Handler === 'object' &&
|
|
82
|
+
Handler !== null &&
|
|
83
|
+
typeof Handler.handle === 'function'
|
|
84
|
+
) {
|
|
85
|
+
return adapter.wrapMiddleware(Handler, container);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Millas middleware class (handle on prototype)
|
|
89
|
+
if (
|
|
90
|
+
typeof Handler === 'function' &&
|
|
91
|
+
Handler.prototype &&
|
|
92
|
+
typeof Handler.prototype.handle === 'function'
|
|
93
|
+
) {
|
|
94
|
+
return adapter.wrapMiddleware(new Handler(), container);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Raw adapter-native function — pass through as-is (escape hatch)
|
|
98
|
+
if (typeof Handler === 'function') {
|
|
99
|
+
return Handler;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error('Middleware must be a function or a class with handle().');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = MiddlewareRegistry;
|
|
@@ -1,35 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const Middleware
|
|
3
|
+
const Middleware = require('./Middleware');
|
|
4
|
+
const MillasResponse = require('../http/MillasResponse');
|
|
5
|
+
const { jsonify } = require('../http/helpers');
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* ThrottleMiddleware
|
|
7
9
|
*
|
|
8
10
|
* Simple in-memory rate limiter.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Register:
|
|
12
|
-
* middlewareRegistry.register('throttle', new ThrottleMiddleware({ max: 60, window: 60 }));
|
|
13
|
-
*
|
|
14
|
-
* Options:
|
|
15
|
-
* max — max requests per window (default: 60)
|
|
16
|
-
* window — window in seconds (default: 60)
|
|
17
|
-
* keyBy — function(req) => string, defaults to IP
|
|
11
|
+
* Uses the Millas middleware signature: handle(req, next).
|
|
18
12
|
*/
|
|
19
13
|
class ThrottleMiddleware extends Middleware {
|
|
20
14
|
constructor(options = {}) {
|
|
21
15
|
super();
|
|
22
16
|
this.max = options.max || 60;
|
|
23
|
-
this.window = options.window || 60;
|
|
17
|
+
this.window = options.window || 60;
|
|
24
18
|
this.keyBy = options.keyBy || ((req) => req.ip || 'anonymous');
|
|
25
|
-
this._store = new Map();
|
|
19
|
+
this._store = new Map();
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
async handle(req,
|
|
29
|
-
const key
|
|
30
|
-
const now
|
|
31
|
-
|
|
32
|
-
let record = this._store.get(key);
|
|
22
|
+
async handle(req, next) {
|
|
23
|
+
const key = this.keyBy(req);
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
let record = this._store.get(key);
|
|
33
26
|
|
|
34
27
|
if (!record || now > record.resetAt) {
|
|
35
28
|
record = { count: 0, resetAt: now + this.window * 1000 };
|
|
@@ -41,20 +34,23 @@ class ThrottleMiddleware extends Middleware {
|
|
|
41
34
|
const remaining = Math.max(0, this.max - record.count);
|
|
42
35
|
const resetIn = Math.ceil((record.resetAt - now) / 1000);
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
res
|
|
46
|
-
|
|
37
|
+
// Rate limit headers — added to whatever response comes back
|
|
38
|
+
// We set them on the raw Express res since we don't have the final response yet.
|
|
39
|
+
// These headers will be present on all responses from throttled routes.
|
|
40
|
+
req.raw.res.setHeader('X-RateLimit-Limit', String(this.max));
|
|
41
|
+
req.raw.res.setHeader('X-RateLimit-Remaining', String(remaining));
|
|
42
|
+
req.raw.res.setHeader('X-RateLimit-Reset', String(Math.ceil(record.resetAt / 1000)));
|
|
47
43
|
|
|
48
44
|
if (record.count > this.max) {
|
|
49
|
-
return
|
|
50
|
-
error:
|
|
51
|
-
message:
|
|
52
|
-
status:
|
|
45
|
+
return jsonify({
|
|
46
|
+
error: 'Too Many Requests',
|
|
47
|
+
message: `Rate limit exceeded. Try again in ${resetIn}s.`,
|
|
48
|
+
status: 429,
|
|
53
49
|
retryAfter: resetIn,
|
|
54
|
-
});
|
|
50
|
+
}, { status: 429 });
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
next();
|
|
53
|
+
return next();
|
|
58
54
|
}
|
|
59
55
|
}
|
|
60
56
|
|
package/src/orm/fields/index.js
CHANGED
|
@@ -1,128 +1,196 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* fields
|
|
5
|
-
*
|
|
6
|
-
* Schema field definitions used inside Model.fields = { ... }
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* class User extends Model {
|
|
10
|
-
* static fields = {
|
|
11
|
-
* id: fields.id(),
|
|
12
|
-
* name: fields.string({ max: 100 }),
|
|
13
|
-
* email: fields.string({ unique: true }),
|
|
14
|
-
* age: fields.integer({ nullable: true }),
|
|
15
|
-
* score: fields.float({ default: 0.0 }),
|
|
16
|
-
* active: fields.boolean({ default: true }),
|
|
17
|
-
* bio: fields.text({ nullable: true }),
|
|
18
|
-
* data: fields.json({ nullable: true }),
|
|
19
|
-
* role: fields.enum(['admin', 'user', 'guest'], { default: 'user' }),
|
|
20
|
-
* created_at: fields.timestamp(),
|
|
21
|
-
* updated_at: fields.timestamp(),
|
|
22
|
-
* };
|
|
23
|
-
* }
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
3
|
class FieldDefinition {
|
|
27
4
|
constructor(type, options = {}) {
|
|
28
|
-
this.type
|
|
29
|
-
this.options
|
|
30
|
-
this.nullable
|
|
31
|
-
this.unique
|
|
32
|
-
this.default
|
|
33
|
-
this.primary
|
|
34
|
-
this.unsigned
|
|
35
|
-
this.max
|
|
36
|
-
this.enumValues
|
|
37
|
-
this.references
|
|
5
|
+
this.type = type;
|
|
6
|
+
this.options = options;
|
|
7
|
+
this.nullable = options.nullable ?? false;
|
|
8
|
+
this.unique = options.unique ?? false;
|
|
9
|
+
this.default = options.default !== undefined ? options.default : undefined;
|
|
10
|
+
this.primary = options.primary ?? false;
|
|
11
|
+
this.unsigned = options.unsigned ?? false;
|
|
12
|
+
this.max = options.max ?? null;
|
|
13
|
+
this.enumValues = options.enumValues ?? null;
|
|
14
|
+
this.references = options.references ?? null;
|
|
15
|
+
this._isForeignKey = options._isForeignKey ?? false;
|
|
16
|
+
this._isOneToOne = options._isOneToOne ?? false;
|
|
17
|
+
this._isManyToMany = options._isManyToMany ?? false;
|
|
18
|
+
this._fkModel = options._fkModel ?? null;
|
|
19
|
+
this._fkModelRef = options._fkModelRef ?? null;
|
|
20
|
+
this._fkToField = options._fkToField ?? 'id';
|
|
21
|
+
this._fkOnDelete = options._fkOnDelete ?? 'CASCADE';
|
|
22
|
+
this._fkRelatedName = options._fkRelatedName ?? null;
|
|
23
|
+
this._m2mThrough = options._m2mThrough ?? null;
|
|
38
24
|
}
|
|
39
25
|
|
|
40
|
-
|
|
26
|
+
nullable_(val = true) { this.nullable = val; return this; }
|
|
27
|
+
unique_(val = true) { this.unique = val; return this; }
|
|
28
|
+
default_(val) { this.default = val; return this; }
|
|
29
|
+
unsigned_(val = true) { this.unsigned = val; return this; }
|
|
30
|
+
references_(table, col) { this.references = { table, column: col }; return this; }
|
|
31
|
+
}
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
function _makeModelRef(model) {
|
|
34
|
+
if (typeof model === 'function') return model;
|
|
35
|
+
if (model === 'self') return null;
|
|
36
|
+
return () => {
|
|
37
|
+
const path = require('path');
|
|
38
|
+
const modelsDir = path.join(process.cwd(), 'app', 'models');
|
|
39
|
+
try {
|
|
40
|
+
return require(path.join(modelsDir, model));
|
|
41
|
+
} catch {
|
|
42
|
+
try {
|
|
43
|
+
const fs = require('fs');
|
|
44
|
+
const files = fs.readdirSync(modelsDir);
|
|
45
|
+
const match = files.find(f =>
|
|
46
|
+
f.replace(/\.js$/, '').toLowerCase() === model.toLowerCase()
|
|
47
|
+
);
|
|
48
|
+
if (match) return require(path.join(modelsDir, match));
|
|
49
|
+
} catch {}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
const fields = {
|
|
50
|
-
|
|
56
|
+
|
|
51
57
|
id(options = {}) {
|
|
52
58
|
return new FieldDefinition('id', { primary: true, unsigned: true, ...options });
|
|
53
59
|
},
|
|
54
60
|
|
|
55
|
-
/** VARCHAR / TEXT string */
|
|
56
61
|
string(options = {}) {
|
|
57
62
|
return new FieldDefinition('string', { max: 255, ...options });
|
|
58
63
|
},
|
|
59
64
|
|
|
60
|
-
/** LONGTEXT */
|
|
61
65
|
text(options = {}) {
|
|
62
66
|
return new FieldDefinition('text', options);
|
|
63
67
|
},
|
|
64
68
|
|
|
65
|
-
/** INT */
|
|
66
69
|
integer(options = {}) {
|
|
67
70
|
return new FieldDefinition('integer', options);
|
|
68
71
|
},
|
|
69
72
|
|
|
70
|
-
/** BIGINT */
|
|
71
73
|
bigInteger(options = {}) {
|
|
72
74
|
return new FieldDefinition('bigInteger', options);
|
|
73
75
|
},
|
|
74
76
|
|
|
75
|
-
/** FLOAT / DOUBLE */
|
|
76
77
|
float(options = {}) {
|
|
77
78
|
return new FieldDefinition('float', options);
|
|
78
79
|
},
|
|
79
80
|
|
|
80
|
-
/** DECIMAL(precision, scale) */
|
|
81
81
|
decimal(precision = 8, scale = 2, options = {}) {
|
|
82
82
|
return new FieldDefinition('decimal', { precision, scale, ...options });
|
|
83
83
|
},
|
|
84
84
|
|
|
85
|
-
/** TINYINT(1) boolean */
|
|
86
85
|
boolean(options = {}) {
|
|
87
86
|
return new FieldDefinition('boolean', options);
|
|
88
87
|
},
|
|
89
88
|
|
|
90
|
-
/** JSON blob */
|
|
91
89
|
json(options = {}) {
|
|
92
90
|
return new FieldDefinition('json', options);
|
|
93
91
|
},
|
|
94
92
|
|
|
95
|
-
/** DATE */
|
|
96
93
|
date(options = {}) {
|
|
97
94
|
return new FieldDefinition('date', options);
|
|
98
95
|
},
|
|
99
96
|
|
|
100
|
-
/** DATETIME / TIMESTAMP */
|
|
101
97
|
timestamp(options = {}) {
|
|
102
98
|
return new FieldDefinition('timestamp', { nullable: true, ...options });
|
|
103
99
|
},
|
|
104
100
|
|
|
105
|
-
/** ENUM */
|
|
106
101
|
enum(values, options = {}) {
|
|
107
102
|
return new FieldDefinition('enum', { enumValues: values, ...options });
|
|
108
103
|
},
|
|
109
104
|
|
|
110
|
-
/** UUID */
|
|
111
105
|
uuid(options = {}) {
|
|
112
106
|
return new FieldDefinition('uuid', options);
|
|
113
107
|
},
|
|
114
108
|
|
|
115
109
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
110
|
+
* ForeignKey — Django-style.
|
|
111
|
+
*
|
|
112
|
+
* Declares the integer column AND wires the BelongsTo relation automatically.
|
|
113
|
+
* No `static relations` block needed.
|
|
114
|
+
*
|
|
115
|
+
* Field name convention:
|
|
116
|
+
* author → accessor: book.author() column: author_id
|
|
117
|
+
* author_id → accessor: book.author() column: author_id
|
|
118
|
+
*
|
|
119
|
+
* @param {string|Function} model 'Author' | () => Author | 'self'
|
|
120
|
+
* @param {object} [opts]
|
|
121
|
+
* @param {boolean} [opts.nullable] allow NULL (default: false)
|
|
122
|
+
* @param {string} [opts.onDelete] CASCADE|SET NULL|RESTRICT|PROTECT|DO_NOTHING (default: CASCADE)
|
|
123
|
+
* @param {string} [opts.relatedName] reverse accessor on target, e.g. 'books' → author.books()
|
|
124
|
+
* pass '+' to suppress the reverse relation
|
|
125
|
+
* @param {string} [opts.toField] target column (default: 'id')
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* author: fields.ForeignKey('Author', { onDelete: 'CASCADE', relatedName: 'books' })
|
|
129
|
+
* editor: fields.ForeignKey('User', { nullable: true, onDelete: 'SET NULL' })
|
|
130
|
+
* parent: fields.ForeignKey('self', { nullable: true, relatedName: 'children' })
|
|
131
|
+
*/
|
|
132
|
+
ForeignKey(model, opts = {}) {
|
|
133
|
+
return new FieldDefinition('integer', {
|
|
134
|
+
unsigned: true,
|
|
135
|
+
nullable: opts.nullable ?? false,
|
|
136
|
+
_isForeignKey: true,
|
|
137
|
+
_fkModel: model,
|
|
138
|
+
_fkModelRef: _makeModelRef(model),
|
|
139
|
+
_fkToField: opts.toField ?? 'id',
|
|
140
|
+
_fkOnDelete: opts.onDelete ?? 'CASCADE',
|
|
141
|
+
_fkRelatedName: opts.relatedName ?? null,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* OneToOne — unique ForeignKey. Both directions wired automatically.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* user: fields.OneToOne('User', { relatedName: 'profile' })
|
|
150
|
+
* // profile.user() and user.profile() both work
|
|
118
151
|
*/
|
|
152
|
+
OneToOne(model, opts = {}) {
|
|
153
|
+
return new FieldDefinition('integer', {
|
|
154
|
+
unsigned: true,
|
|
155
|
+
unique: true,
|
|
156
|
+
nullable: opts.nullable ?? false,
|
|
157
|
+
_isForeignKey: true,
|
|
158
|
+
_isOneToOne: true,
|
|
159
|
+
_fkModel: model,
|
|
160
|
+
_fkModelRef: _makeModelRef(model),
|
|
161
|
+
_fkToField: opts.toField ?? 'id',
|
|
162
|
+
_fkOnDelete: opts.onDelete ?? 'CASCADE',
|
|
163
|
+
_fkRelatedName: opts.relatedName ?? null,
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* ManyToMany — no DB column. Generates pivot table migration.
|
|
169
|
+
* Pivot table auto-named: sorted model names joined with underscore.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* tags: fields.ManyToMany('Tag', { relatedName: 'courses' })
|
|
173
|
+
* tags: fields.ManyToMany('Tag', { through: 'course_tags', relatedName: 'courses' })
|
|
174
|
+
*/
|
|
175
|
+
ManyToMany(model, opts = {}) {
|
|
176
|
+
return new FieldDefinition('m2m', {
|
|
177
|
+
nullable: true,
|
|
178
|
+
_isManyToMany: true,
|
|
179
|
+
_fkModel: model,
|
|
180
|
+
_fkModelRef: _makeModelRef(model),
|
|
181
|
+
_fkRelatedName: opts.relatedName ?? null,
|
|
182
|
+
_m2mThrough: opts.through ?? null,
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/** Legacy — kept for backward compatibility. Prefer ForeignKey(). */
|
|
119
187
|
foreignId(column, options = {}) {
|
|
120
188
|
const [table, col] = column.endsWith('_id')
|
|
121
189
|
? [column.slice(0, -3) + 's', 'id']
|
|
122
190
|
: [null, null];
|
|
123
191
|
return new FieldDefinition('integer', {
|
|
124
|
-
unsigned:
|
|
125
|
-
nullable:
|
|
192
|
+
unsigned: true,
|
|
193
|
+
nullable: options.nullable ?? false,
|
|
126
194
|
references: table ? { table, column: col } : null,
|
|
127
195
|
...options,
|
|
128
196
|
});
|