millas 0.2.12-beta-1 → 0.2.13

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 (120) hide show
  1. package/package.json +3 -2
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +516 -199
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +318 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +393 -97
  12. package/src/admin/static/admin.css +1422 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +87 -1046
  19. package/src/admin/views/pages/detail.njk +56 -21
  20. package/src/admin/views/pages/error.njk +65 -0
  21. package/src/admin/views/pages/form.njk +47 -599
  22. package/src/admin/views/pages/list.njk +270 -62
  23. package/src/admin/views/partials/form-field.njk +53 -0
  24. package/src/admin/views/partials/form-footer.njk +28 -0
  25. package/src/admin/views/partials/form-readonly.njk +114 -0
  26. package/src/admin/views/partials/form-scripts.njk +480 -0
  27. package/src/admin/views/partials/form-widget.njk +297 -0
  28. package/src/admin/views/partials/icons.njk +64 -0
  29. package/src/admin/views/partials/json-dialog.njk +80 -0
  30. package/src/admin/views/partials/json-editor.njk +37 -0
  31. package/src/ai/AIManager.js +954 -0
  32. package/src/ai/AITokenBudget.js +250 -0
  33. package/src/ai/PromptGuard.js +216 -0
  34. package/src/ai/agents.js +218 -0
  35. package/src/ai/conversation.js +213 -0
  36. package/src/ai/drivers.js +734 -0
  37. package/src/ai/files.js +249 -0
  38. package/src/ai/media.js +303 -0
  39. package/src/ai/pricing.js +152 -0
  40. package/src/ai/provider_tools.js +114 -0
  41. package/src/ai/types.js +356 -0
  42. package/src/auth/Auth.js +18 -2
  43. package/src/auth/AuthUser.js +65 -44
  44. package/src/cli.js +3 -1
  45. package/src/commands/createsuperuser.js +267 -0
  46. package/src/commands/lang.js +589 -0
  47. package/src/commands/migrate.js +154 -81
  48. package/src/commands/serve.js +3 -4
  49. package/src/container/AppInitializer.js +101 -20
  50. package/src/container/Application.js +31 -1
  51. package/src/container/MillasApp.js +10 -3
  52. package/src/container/MillasConfig.js +35 -6
  53. package/src/core/admin.js +5 -0
  54. package/src/core/db.js +2 -1
  55. package/src/core/foundation.js +2 -10
  56. package/src/core/lang.js +1 -0
  57. package/src/errors/HttpError.js +32 -16
  58. package/src/facades/AI.js +411 -0
  59. package/src/facades/Hash.js +67 -0
  60. package/src/facades/Process.js +144 -0
  61. package/src/hashing/Hash.js +262 -0
  62. package/src/http/HtmlEscape.js +162 -0
  63. package/src/http/MillasRequest.js +63 -7
  64. package/src/http/MillasResponse.js +70 -4
  65. package/src/http/ResponseDispatcher.js +21 -27
  66. package/src/http/SafeFilePath.js +195 -0
  67. package/src/http/SafeRedirect.js +62 -0
  68. package/src/http/SecurityBootstrap.js +70 -0
  69. package/src/http/helpers.js +40 -125
  70. package/src/http/index.js +10 -1
  71. package/src/http/middleware/CsrfMiddleware.js +258 -0
  72. package/src/http/middleware/RateLimiter.js +314 -0
  73. package/src/http/middleware/SecurityHeaders.js +281 -0
  74. package/src/i18n/I18nServiceProvider.js +91 -0
  75. package/src/i18n/Translator.js +643 -0
  76. package/src/i18n/defaults.js +122 -0
  77. package/src/i18n/index.js +164 -0
  78. package/src/i18n/locales/en.js +55 -0
  79. package/src/i18n/locales/sw.js +48 -0
  80. package/src/logger/LogRedactor.js +247 -0
  81. package/src/logger/Logger.js +1 -1
  82. package/src/logger/formatters/JsonFormatter.js +11 -4
  83. package/src/logger/formatters/PrettyFormatter.js +103 -65
  84. package/src/logger/formatters/SimpleFormatter.js +14 -3
  85. package/src/middleware/ThrottleMiddleware.js +27 -4
  86. package/src/migrations/system/0001_users.js +21 -0
  87. package/src/migrations/system/0002_admin_log.js +25 -0
  88. package/src/migrations/system/0003_sessions.js +23 -0
  89. package/src/orm/fields/index.js +210 -188
  90. package/src/orm/migration/DefaultValueParser.js +325 -0
  91. package/src/orm/migration/InteractiveResolver.js +191 -0
  92. package/src/orm/migration/Makemigrations.js +312 -0
  93. package/src/orm/migration/MigrationGraph.js +227 -0
  94. package/src/orm/migration/MigrationRunner.js +202 -108
  95. package/src/orm/migration/MigrationWriter.js +463 -0
  96. package/src/orm/migration/ModelInspector.js +143 -74
  97. package/src/orm/migration/ModelScanner.js +225 -0
  98. package/src/orm/migration/ProjectState.js +213 -0
  99. package/src/orm/migration/RenameDetector.js +175 -0
  100. package/src/orm/migration/SchemaBuilder.js +8 -81
  101. package/src/orm/migration/operations/base.js +57 -0
  102. package/src/orm/migration/operations/column.js +191 -0
  103. package/src/orm/migration/operations/fields.js +252 -0
  104. package/src/orm/migration/operations/index.js +55 -0
  105. package/src/orm/migration/operations/models.js +152 -0
  106. package/src/orm/migration/operations/registry.js +131 -0
  107. package/src/orm/migration/operations/special.js +51 -0
  108. package/src/orm/migration/utils.js +208 -0
  109. package/src/orm/model/Model.js +81 -13
  110. package/src/process/Process.js +333 -0
  111. package/src/providers/AdminServiceProvider.js +66 -9
  112. package/src/providers/AuthServiceProvider.js +40 -5
  113. package/src/providers/CacheStorageServiceProvider.js +2 -2
  114. package/src/providers/DatabaseServiceProvider.js +3 -2
  115. package/src/providers/LogServiceProvider.js +4 -1
  116. package/src/providers/MailServiceProvider.js +1 -1
  117. package/src/providers/QueueServiceProvider.js +1 -1
  118. package/src/router/MiddlewareRegistry.js +27 -2
  119. package/src/scaffold/templates.js +80 -21
  120. package/src/validation/Validator.js +348 -607
@@ -1,12 +1,14 @@
1
1
  'use strict';
2
2
 
3
- const { LEVEL_NAMES } = require('../levels');
3
+ const { LEVEL_NAMES } = require('../levels');
4
+ const { LogRedactor } = require('../LogRedactor');
4
5
 
5
6
  /**
6
7
  * JsonFormatter
7
8
  *
8
9
  * Emits one JSON object per log entry — ideal for production environments
9
10
  * where logs are shipped to Datadog, Elasticsearch, CloudWatch, etc.
11
+ * Sensitive context fields are automatically redacted before serialisation.
10
12
  *
11
13
  * Output (one line per entry):
12
14
  * {"ts":"2026-03-15T12:00:00.000Z","level":"INFO","tag":"Auth","msg":"Login","ctx":{...}}
@@ -15,11 +17,13 @@ class JsonFormatter {
15
17
  /**
16
18
  * @param {object} options
17
19
  * @param {boolean} [options.pretty=false] — pretty-print JSON (for debugging)
18
- * @param {object} [options.extra] — static fields merged into every entry (e.g. service name)
20
+ * @param {object} [options.extra] — static fields merged into every entry
21
+ * @param {boolean} [options.redact=true] — redact sensitive context fields
19
22
  */
20
23
  constructor(options = {}) {
21
24
  this.pretty = options.pretty || false;
22
25
  this.extra = options.extra || {};
26
+ this.redact = options.redact !== false; // default: true
23
27
  }
24
28
 
25
29
  format(entry) {
@@ -33,7 +37,10 @@ class JsonFormatter {
33
37
 
34
38
  if (tag) record.tag = tag;
35
39
  record.msg = message;
36
- if (context !== undefined && context !== null) record.ctx = context;
40
+
41
+ if (context !== undefined && context !== null) {
42
+ record.ctx = this.redact ? LogRedactor.redact(context) : context;
43
+ }
37
44
 
38
45
  if (error instanceof Error) {
39
46
  record.error = {
@@ -49,4 +56,4 @@ class JsonFormatter {
49
56
  }
50
57
  }
51
58
 
52
- module.exports = JsonFormatter;
59
+ module.exports = JsonFormatter;
@@ -1,98 +1,136 @@
1
1
  'use strict';
2
2
 
3
- const { LEVEL_NAMES, LEVEL_TAGS, LEVEL_COLOURS, RESET, BOLD, DIM } = require('../levels');
4
-
5
- /**
6
- * PrettyFormatter
7
- *
8
- * Colourful, human-readable output. Designed for development.
9
- * Inspired by Timber (Android) and Laravel's log formatting.
10
- *
11
- * Output:
12
- * [2026-03-15 12:00:00] I UserController User #5 logged in
13
- * [2026-03-15 12:00:01] E Database Connection refused { host: 'localhost' }
14
- * [2026-03-15 12:00:02] W Auth Token expiring soon
15
- *
16
- * WTF level also prints the full stack trace.
17
- */
3
+ const { LEVEL_TAGS, LEVEL_COLOURS, RESET, BOLD } = require('../levels');
4
+ const { LogRedactor } = require('../LogRedactor');
5
+
6
+ const SEP = ' ';
7
+ const TAG_WIDTH = 18;
8
+
9
+ // Matches /absolute/path/to/file.js or /absolute/path/to/file.js:12:34
10
+ const FILE_PATH_RE = /(\/[^\s)'"]+\.(js|ts|json|mjs|cjs)(?::\d+(?::\d+)?)?)/g;
11
+ // Matches http(s):// URLs
12
+ const URL_RE = /(https?:\/\/[^\s)'"]+)/g;
13
+
14
+ function linkify(text, dim, useColour) {
15
+ if (!useColour) return text;
16
+
17
+ text = text.replace(FILE_PATH_RE, (match) => {
18
+ const filePath = match.replace(/(:\d+)+$/, '');
19
+ const uri = `file://${filePath}`;
20
+ return `${RESET}\x1b]8;;${uri}\x1b\\${match}\x1b]8;;\x1b\\`;
21
+ });
22
+
23
+ text = text.replace(URL_RE, (match) => {
24
+ return `${RESET}\x1b]8;;${match}\x1b\\${match}\x1b]8;;\x1b\\`;
25
+ });
26
+
27
+ return text;
28
+ }
29
+
18
30
  class PrettyFormatter {
19
- /**
20
- * @param {object} options
21
- * @param {boolean} [options.timestamp=true] — show timestamp
22
- * @param {boolean} [options.tag=true] — show tag/component name
23
- * @param {boolean} [options.colour=true] — ANSI colour (disable for pipes/files)
24
- * @param {string} [options.timestampFormat] — 'iso' | 'short' (default: 'short')
25
- */
26
31
  constructor(options = {}) {
27
32
  this.showTimestamp = options.timestamp !== false;
28
33
  this.showTag = options.tag !== false;
29
34
  this.colour = options.colour !== false;
30
35
  this.tsFormat = options.timestampFormat || 'short';
36
+ this.tagWidth = options.tagWidth || TAG_WIDTH;
31
37
  }
32
38
 
33
39
  format(entry) {
34
40
  const { level, tag, message, context, error } = entry;
35
41
 
36
- const c = this.colour ? LEVEL_COLOURS[level] : '';
37
- const r = this.colour ? RESET : '';
38
- const b = this.colour ? BOLD : '';
39
- const d = this.colour ? '\x1b[2m' : '';
40
- const lvl = LEVEL_TAGS[level] || '?';
42
+ const c = this.colour ? (LEVEL_COLOURS[level] || '') : '';
43
+ const r = this.colour ? RESET : '';
44
+ const b = this.colour ? BOLD : '';
45
+ const d = this.colour ? '\x1b[2m' : '';
46
+ const lvl = LEVEL_TAGS[level] || '?';
41
47
 
42
- const parts = [];
48
+ // ── 1. Measure plain prefix once ─────────────────────────────────────────
49
+ const ts = this._timestamp();
50
+ const plainCols = [];
51
+ if (this.showTimestamp) plainCols.push(`[${ts}]`);
52
+ plainCols.push(lvl);
53
+ if (this.showTag && tag) plainCols.push(tag.padEnd(this.tagWidth));
43
54
 
44
- // Timestamp
45
- if (this.showTimestamp) {
46
- const ts = this._timestamp();
47
- parts.push(`${d}[${ts}]${r}`);
48
- }
55
+ const indentWidth = plainCols.join(SEP).length + SEP.length;
56
+ const indent = ' '.repeat(indentWidth);
49
57
 
50
- // Level tag (single letter, coloured)
51
- parts.push(`${c}${b}${lvl}${r}`);
58
+ // ── 2. Coloured prefix ───────────────────────────────────────────────────
59
+ const colCols = [];
60
+ if (this.showTimestamp) colCols.push(`${d}[${ts}]${r}`);
61
+ colCols.push(`${c}${b}${lvl}${r}`);
62
+ if (this.showTag && tag) colCols.push(`${b}${tag.padEnd(this.tagWidth)}${r}`);
52
63
 
53
- // Component/tag
54
- if (this.showTag && tag) {
55
- const tagStr = tag.padEnd(18);
56
- parts.push(`${b}${tagStr}${r}`);
57
- }
64
+ const prefix = colCols.join(SEP) + SEP;
58
65
 
59
- // Message (handle multi-line)
60
- const lines = message.split('\n');
61
- parts.push(`${c}${lines[0]}${r}`);
66
+ // ── 3. Terminal width ────────────────────────────────────────────────────
67
+ const termWidth = (process.stdout.columns || 120);
68
+ const msgWidth = termWidth - indentWidth;
62
69
 
63
- let output = parts.join(' ');
70
+ // ── 4. Collect all logical lines ─────────────────────────────────────────
71
+ const logicalLines = [];
64
72
 
65
- // Continuation lines (aligned with first line)
66
- if (lines.length > 1) {
67
- const prefix = parts.slice(0, -1).map(p => p.replace(/\x1b\[[0-9;]*m/g, '')).join(' ');
68
- const indent = ' '.repeat(prefix.length + 2);
69
- for (let i = 1; i < lines.length; i++) {
70
- output += `\n${indent}${c}${lines[i]}${r}`;
71
- }
72
- }
73
+ for (const l of String(message).split('\n')) logicalLines.push({ text: l, dim: false });
73
74
 
74
- // Context object
75
- if (context !== undefined && context !== null) {
76
- const ctx = typeof context === 'object'
77
- ? JSON.stringify(context, null, 0)
78
- : String(context);
79
- output += ` ${d}${ctx}${r}`;
75
+ if (context != null) {
76
+ const safe = this.redact !== false ? LogRedactor.redact(context) : context;
77
+ const ctx = typeof safe === 'object' ? JSON.stringify(safe) : String(safe);
78
+ logicalLines.push({ text: ctx, dim: true });
80
79
  }
81
80
 
82
- // Error stack
83
81
  if (error instanceof Error) {
84
- output += `\n${d}${error.stack || error.message}${r}`;
82
+ for (const l of (error.stack || error.message).split('\n'))
83
+ logicalLines.push({ text: l, dim: true });
84
+ }
85
+
86
+ // ── 5. Hard-wrap (skip stack frames so paths stay intact) ────────────────
87
+ const wrappedLines = [];
88
+
89
+ for (const { text, dim } of logicalLines) {
90
+ const isStackFrame = dim && /^\s*at /.test(text);
91
+
92
+ if (isStackFrame || text.length <= msgWidth) {
93
+ wrappedLines.push({ text, dim });
94
+ continue;
95
+ }
96
+
97
+ const words = text.split(' ');
98
+ let chunk = '';
99
+ for (const word of words) {
100
+ if (chunk.length === 0) {
101
+ if (word.length > msgWidth) {
102
+ for (let i = 0; i < word.length; i += msgWidth)
103
+ wrappedLines.push({ text: word.slice(i, i + msgWidth), dim });
104
+ } else {
105
+ chunk = word;
106
+ }
107
+ } else if (chunk.length + 1 + word.length <= msgWidth) {
108
+ chunk += ' ' + word;
109
+ } else {
110
+ wrappedLines.push({ text: chunk, dim });
111
+ chunk = word;
112
+ }
113
+ }
114
+ if (chunk.length) wrappedLines.push({ text: chunk, dim });
85
115
  }
86
116
 
87
- // WTF: print big warning banner
117
+ // ── 6. Render + linkify ───────────────────────────────────────────────────
118
+ const rendered = wrappedLines.map((line, i) => {
119
+ const col = line.dim ? d : c;
120
+ const text = linkify(line.text, line.dim, this.colour);
121
+ if (i === 0) return `${prefix}${col}${text}${r}`;
122
+ return `${indent}${col}${text}${r}`;
123
+ }).join('\n');
124
+
125
+ // ── 7. WTF banner ─────────────────────────────────────────────────────────
88
126
  if (level === 5) {
89
- const banner = this.colour
127
+ const bar = this.colour
90
128
  ? `\x1b[35m\x1b[1m${'━'.repeat(60)}\x1b[0m`
91
129
  : '━'.repeat(60);
92
- output = `${banner}\n${output}\n${banner}`;
130
+ return `${bar}\n${rendered}\n${bar}`;
93
131
  }
94
132
 
95
- return output;
133
+ return rendered;
96
134
  }
97
135
 
98
136
  _timestamp() {
@@ -102,4 +140,4 @@ class PrettyFormatter {
102
140
  }
103
141
  }
104
142
 
105
- module.exports = PrettyFormatter;
143
+ module.exports = PrettyFormatter;
@@ -1,18 +1,28 @@
1
1
  'use strict';
2
2
 
3
3
  const { LEVEL_NAMES } = require('../levels');
4
+ const { LogRedactor } = require('../LogRedactor');
4
5
 
5
6
  /**
6
7
  * SimpleFormatter
7
8
  *
8
9
  * Plain, no-colour text. Suitable for file output or any sink
9
- * where ANSI codes would be noise.
10
+ * where ANSI codes would be noise. Sensitive context fields are
11
+ * automatically redacted before serialisation.
10
12
  *
11
13
  * Output:
12
14
  * [2026-03-15 12:00:00] [INFO] Auth: User logged in
13
15
  * [2026-03-15 12:00:01] [ERROR] DB: Query failed {"table":"users"}
14
16
  */
15
17
  class SimpleFormatter {
18
+ /**
19
+ * @param {object} [options]
20
+ * @param {boolean} [options.redact=true] — redact sensitive context fields
21
+ */
22
+ constructor(options = {}) {
23
+ this.redact = options.redact !== false;
24
+ }
25
+
16
26
  format(entry) {
17
27
  const { level, tag, message, context, error, timestamp } = entry;
18
28
 
@@ -23,7 +33,8 @@ class SimpleFormatter {
23
33
  let line = `[${ts}] [${lvlName}] ${tagPart}${message}`;
24
34
 
25
35
  if (context !== undefined && context !== null) {
26
- line += ' ' + (typeof context === 'object' ? JSON.stringify(context) : String(context));
36
+ const safe = this.redact ? LogRedactor.redact(context) : context;
37
+ line += ' ' + (typeof safe === 'object' ? JSON.stringify(safe) : String(safe));
27
38
  }
28
39
 
29
40
  if (error instanceof Error) {
@@ -34,4 +45,4 @@ class SimpleFormatter {
34
45
  }
35
46
  }
36
47
 
37
- module.exports = SimpleFormatter;
48
+ module.exports = SimpleFormatter;
@@ -7,18 +7,41 @@ const { jsonify } = require('../http/helpers');
7
7
  /**
8
8
  * ThrottleMiddleware
9
9
  *
10
- * Simple in-memory rate limiter.
11
- * Uses the Millas middleware signature: handle(req, next).
10
+ * Per-IP (or per-user) rate limiter registered as the 'throttle' middleware alias.
11
+ * Used via the route middleware system developers never import this directly.
12
+ *
13
+ * Usage in routes:
14
+ * Route.middleware('throttle:5,10').group(() => { // 5 req per 10 min
15
+ * Route.post('/login', AuthController, 'login');
16
+ * });
17
+ *
18
+ * Route.post('/login', AuthController, 'login') // same, single route
19
+ * — add 'throttle:5,10' to route middleware array
20
+ *
21
+ * Format: 'throttle:<max>,<minutes>'
22
+ * throttle:60,1 — 60 requests per minute
23
+ * throttle:5,10 — 5 requests per 10 minutes
24
+ * throttle:100,15 — 100 requests per 15 minutes
12
25
  */
13
26
  class ThrottleMiddleware extends Middleware {
14
27
  constructor(options = {}) {
15
28
  super();
16
29
  this.max = options.max || 60;
17
- this.window = options.window || 60;
30
+ this.window = options.window || 60; // seconds
18
31
  this.keyBy = options.keyBy || ((req) => req.ip || 'anonymous');
19
32
  this._store = new Map();
20
33
  }
21
34
 
35
+ /**
36
+ * Factory used by MiddlewareRegistry when parsing 'throttle:max,minutes'.
37
+ * @param {string[]} params — ['5', '10'] from 'throttle:5,10'
38
+ */
39
+ static fromParams(params) {
40
+ const max = parseInt(params[0], 10) || 60;
41
+ const minutes = parseInt(params[1], 10) || 1;
42
+ return new ThrottleMiddleware({ max, window: minutes * 60 });
43
+ }
44
+
22
45
  async handle(req, next) {
23
46
  const key = this.keyBy(req);
24
47
  const now = Date.now();
@@ -54,4 +77,4 @@ class ThrottleMiddleware extends Middleware {
54
77
  }
55
78
  }
56
79
 
57
- module.exports = ThrottleMiddleware;
80
+ module.exports = ThrottleMiddleware;
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * System migration: 0001_users
5
+ *
6
+ * Previously created the users table directly.
7
+ *
8
+ * As of Millas 0.3+, the users table is owned by the APP — not the framework.
9
+ * AuthUser is now fully abstract (no table). The app defines its own User model
10
+ * that extends AuthUser and owns whatever table it wants (typically 'users').
11
+ *
12
+ * This migration is kept as a no-op so existing projects that depend on
13
+ * ['system', '0001_users'] don't break. It creates nothing.
14
+ *
15
+ * Equivalent to Django's pattern: AbstractUser has no table; your custom
16
+ * User model creates its own table via app migrations.
17
+ */
18
+ module.exports = {
19
+ dependencies: [],
20
+ operations: [],
21
+ };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const { CreateModel } = require('../../orm/migration/operations');
4
+
5
+ /**
6
+ * System migration: millas_admin_log
7
+ * Equivalent to Django's django_admin_log.
8
+ */
9
+ module.exports = {
10
+ dependencies: [['system', '0001_users']],
11
+
12
+ operations: [
13
+ new CreateModel('millas_admin_log', {
14
+ id: { type: 'id', unsigned: true, nullable: false, unique: false, default: null, max: null, enumValues: null, references: null, precision: null, scale: null },
15
+ user_id: { type: 'integer', unsigned: true, nullable: true, unique: false, default: null, max: null, enumValues: null, references: { table: 'users', column: 'id', onDelete: 'SET NULL' }, precision: null, scale: null },
16
+ user_email: { type: 'string', max: 255, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
17
+ resource: { type: 'string', max: 100, nullable: false, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
18
+ record_id: { type: 'string', max: 100, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
19
+ action: { type: 'enum', enumValues: ['create','update','delete'], nullable: false, unique: false, default: null, max: null, unsigned: false, references: null, precision: null, scale: null },
20
+ label: { type: 'string', max: 255, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
21
+ change_msg: { type: 'text', nullable: true, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
22
+ created_at: { type: 'timestamp', nullable: true, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
23
+ }),
24
+ ],
25
+ };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const { CreateModel } = require('../../orm/migration/operations');
4
+
5
+ /**
6
+ * System migration: millas_sessions
7
+ * Equivalent to Django's django_session.
8
+ */
9
+ module.exports = {
10
+ dependencies: [['system', '0001_users']],
11
+
12
+ operations: [
13
+ new CreateModel('millas_sessions', {
14
+ session_key: { type: 'string', max: 64, nullable: false, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
15
+ user_id: { type: 'integer', unsigned: true, nullable: false, unique: false, default: null, max: null, enumValues: null, references: { table: 'users', column: 'id', onDelete: 'CASCADE' }, precision: null, scale: null },
16
+ payload: { type: 'text', nullable: true, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
17
+ ip_address: { type: 'string', max: 45, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
18
+ user_agent: { type: 'string', max: 512, nullable: true, unique: false, default: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
19
+ expires_at: { type: 'timestamp', nullable: false, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
20
+ created_at: { type: 'timestamp', nullable: true, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null },
21
+ }),
22
+ ],
23
+ };