millas 0.2.12-beta → 0.2.12-beta-2

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 (116) hide show
  1. package/package.json +3 -16
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +400 -167
  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 +309 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +383 -97
  12. package/src/admin/static/admin.css +1341 -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 +65 -1013
  19. package/src/admin/views/pages/detail.njk +40 -16
  20. package/src/admin/views/pages/form.njk +47 -599
  21. package/src/admin/views/pages/list.njk +145 -62
  22. package/src/admin/views/partials/form-field.njk +53 -0
  23. package/src/admin/views/partials/form-footer.njk +28 -0
  24. package/src/admin/views/partials/form-readonly.njk +114 -0
  25. package/src/admin/views/partials/form-scripts.njk +476 -0
  26. package/src/admin/views/partials/form-widget.njk +296 -0
  27. package/src/admin/views/partials/json-dialog.njk +80 -0
  28. package/src/admin/views/partials/json-editor.njk +37 -0
  29. package/src/admin.zip +0 -0
  30. package/src/auth/Auth.js +31 -10
  31. package/src/auth/AuthController.js +3 -1
  32. package/src/auth/AuthUser.js +119 -0
  33. package/src/cli.js +4 -2
  34. package/src/commands/createsuperuser.js +254 -0
  35. package/src/commands/lang.js +589 -0
  36. package/src/commands/migrate.js +154 -81
  37. package/src/commands/serve.js +82 -110
  38. package/src/container/AppInitializer.js +215 -0
  39. package/src/container/Application.js +278 -253
  40. package/src/container/HttpServer.js +156 -0
  41. package/src/container/MillasApp.js +29 -279
  42. package/src/container/MillasConfig.js +192 -0
  43. package/src/core/admin.js +5 -0
  44. package/src/core/auth.js +9 -0
  45. package/src/core/db.js +9 -0
  46. package/src/core/foundation.js +59 -0
  47. package/src/core/http.js +11 -0
  48. package/src/core/lang.js +1 -0
  49. package/src/core/mail.js +6 -0
  50. package/src/core/queue.js +7 -0
  51. package/src/core/validation.js +29 -0
  52. package/src/facades/Admin.js +1 -1
  53. package/src/facades/Auth.js +22 -39
  54. package/src/facades/Cache.js +21 -10
  55. package/src/facades/Database.js +1 -1
  56. package/src/facades/Events.js +18 -17
  57. package/src/facades/Facade.js +197 -0
  58. package/src/facades/Http.js +42 -45
  59. package/src/facades/Log.js +25 -49
  60. package/src/facades/Mail.js +27 -32
  61. package/src/facades/Queue.js +22 -15
  62. package/src/facades/Storage.js +18 -10
  63. package/src/facades/Url.js +53 -0
  64. package/src/http/HttpClient.js +673 -0
  65. package/src/http/ResponseDispatcher.js +18 -111
  66. package/src/http/UrlGenerator.js +375 -0
  67. package/src/http/WelcomePage.js +273 -0
  68. package/src/http/adapters/ExpressAdapter.js +315 -0
  69. package/src/http/adapters/HttpAdapter.js +168 -0
  70. package/src/http/adapters/index.js +9 -0
  71. package/src/i18n/I18nServiceProvider.js +91 -0
  72. package/src/i18n/Translator.js +635 -0
  73. package/src/i18n/defaults.js +122 -0
  74. package/src/i18n/index.js +164 -0
  75. package/src/i18n/locales/en.js +55 -0
  76. package/src/i18n/locales/sw.js +48 -0
  77. package/src/index.js +5 -144
  78. package/src/logger/formatters/PrettyFormatter.js +103 -57
  79. package/src/logger/internal.js +2 -2
  80. package/src/logger/patchConsole.js +91 -81
  81. package/src/middleware/MiddlewareRegistry.js +62 -82
  82. package/src/migrations/system/0001_users.js +21 -0
  83. package/src/migrations/system/0002_admin_log.js +25 -0
  84. package/src/migrations/system/0003_sessions.js +23 -0
  85. package/src/orm/fields/index.js +210 -188
  86. package/src/orm/migration/DefaultValueParser.js +325 -0
  87. package/src/orm/migration/InteractiveResolver.js +191 -0
  88. package/src/orm/migration/Makemigrations.js +312 -0
  89. package/src/orm/migration/MigrationGraph.js +227 -0
  90. package/src/orm/migration/MigrationRunner.js +202 -108
  91. package/src/orm/migration/MigrationWriter.js +463 -0
  92. package/src/orm/migration/ModelInspector.js +412 -344
  93. package/src/orm/migration/ModelScanner.js +225 -0
  94. package/src/orm/migration/ProjectState.js +213 -0
  95. package/src/orm/migration/RenameDetector.js +175 -0
  96. package/src/orm/migration/SchemaBuilder.js +8 -81
  97. package/src/orm/migration/operations/base.js +57 -0
  98. package/src/orm/migration/operations/column.js +191 -0
  99. package/src/orm/migration/operations/fields.js +252 -0
  100. package/src/orm/migration/operations/index.js +55 -0
  101. package/src/orm/migration/operations/models.js +152 -0
  102. package/src/orm/migration/operations/registry.js +131 -0
  103. package/src/orm/migration/operations/special.js +51 -0
  104. package/src/orm/migration/utils.js +208 -0
  105. package/src/orm/model/Model.js +81 -13
  106. package/src/providers/AdminServiceProvider.js +66 -9
  107. package/src/providers/AuthServiceProvider.js +46 -7
  108. package/src/providers/CacheStorageServiceProvider.js +5 -3
  109. package/src/providers/DatabaseServiceProvider.js +3 -2
  110. package/src/providers/EventServiceProvider.js +2 -1
  111. package/src/providers/LogServiceProvider.js +7 -3
  112. package/src/providers/MailServiceProvider.js +4 -3
  113. package/src/providers/QueueServiceProvider.js +4 -3
  114. package/src/router/Router.js +119 -152
  115. package/src/scaffold/templates.js +83 -26
  116. package/src/facades/Validation.js +0 -69
@@ -0,0 +1,325 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * DefaultValueParser
5
+ *
6
+ * Evaluates user-provided default value expressions in a safe, restricted
7
+ * context during makemigrations.
8
+ *
9
+ * ── What it does ─────────────────────────────────────────────────────────────
10
+ *
11
+ * 1. Receives raw string input from the developer (e.g. "42", "'hello'",
12
+ * "Date.now", "crypto.randomUUID", "() => new Date().toISOString()")
13
+ *
14
+ * 2. Classifies it as:
15
+ * literal — a plain value that can be serialised to JSON
16
+ * (number, string, boolean, null, array, object)
17
+ * callable — a reference or arrow function that must be called
18
+ * per-row at migration time (Date.now, uuid, etc.)
19
+ *
20
+ * 3. For literals: evaluates the expression and returns the typed value
21
+ * 4. For callables: stores the expression string as-is (never evaluated here)
22
+ *
23
+ * ── Safe context ─────────────────────────────────────────────────────────────
24
+ *
25
+ * The evaluation sandbox pre-imports a whitelist of safe helpers:
26
+ * Date — current time
27
+ * crypto — randomUUID, randomBytes
28
+ * Math — floor, random, etc.
29
+ *
30
+ * Blocked:
31
+ * require, process, __dirname, __filename, Buffer (file/OS/network access)
32
+ * eval, Function constructor (code injection)
33
+ *
34
+ * ── Result shape ─────────────────────────────────────────────────────────────
35
+ *
36
+ * Literal:
37
+ * { kind: 'literal', value: 42, expression: '42' }
38
+ *
39
+ * Callable:
40
+ * { kind: 'callable', expression: 'Date.now' }
41
+ * { kind: 'callable', expression: '() => crypto.randomUUID()' }
42
+ *
43
+ * ── How it's stored in migration files ───────────────────────────────────────
44
+ *
45
+ * Literal: oneOffDefault: { kind: 'literal', value: 42 }
46
+ * Rendered as: { kind: 'literal', value: 42 }
47
+ *
48
+ * Callable: oneOffDefault: { kind: 'callable', expression: 'Date.now' }
49
+ * Rendered as: { kind: 'callable', expression: 'Date.now' }
50
+ *
51
+ * ── How it's applied at migrate time ─────────────────────────────────────────
52
+ *
53
+ * Literal: db(table).whereNull(col).update({ col: value })
54
+ * — single UPDATE for all rows
55
+ *
56
+ * Callable: called once per row, each row gets its own value
57
+ * — used for uuid, timestamps, random values
58
+ *
59
+ * ── Determinism ──────────────────────────────────────────────────────────────
60
+ *
61
+ * The migration file always stores the EXPRESSION, never the evaluated
62
+ * result. This means:
63
+ * - `Date.now` in the file → each deployment gets the current timestamp
64
+ * - `42` in the file → always 42, deterministic across deployments
65
+ * The developer chooses which behaviour they want by what they type.
66
+ */
67
+ class DefaultValueParser {
68
+
69
+ /**
70
+ * Parse a raw string input from the developer.
71
+ *
72
+ * @param {string} raw — what the developer typed
73
+ * @param {string} fieldType — 'string' | 'integer' | 'boolean' | etc.
74
+ * @returns {{ kind: 'literal'|'callable', value?: *, expression: string }}
75
+ * @throws {Error} if input is unsafe or unparseable
76
+ */
77
+ parse(raw, fieldType) {
78
+ const trimmed = raw.trim();
79
+
80
+ if (!trimmed) {
81
+ throw new Error('No value provided. Enter a value or expression.');
82
+ }
83
+
84
+ // ── Detect callable ───────────────────────────────────────────────────────
85
+ if (this._isCallable(trimmed)) {
86
+ this._assertSafe(trimmed);
87
+ return { kind: 'callable', expression: trimmed };
88
+ }
89
+
90
+ // ── Evaluate as literal ───────────────────────────────────────────────────
91
+ this._assertSafe(trimmed);
92
+ const value = this._evalLiteral(trimmed, fieldType);
93
+ return { kind: 'literal', value, expression: this._serialiseExpression(value) };
94
+ }
95
+
96
+ // ─── Detection ────────────────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Detect if the input looks like a callable reference or function.
100
+ *
101
+ * Callables:
102
+ * Date.now — property reference (no call)
103
+ * crypto.randomUUID — property reference
104
+ * () => new Date().toISOString() — arrow function
105
+ * function() { return 1; } — function expression
106
+ * Date.now() — already called → callable (per-row)
107
+ */
108
+ _isCallable(expr) {
109
+ // Arrow function
110
+ if (/^\(?\s*\w*\s*\)?\s*=>/.test(expr)) return true;
111
+ // function keyword
112
+ if (/^function\s*\(/.test(expr)) return true;
113
+ // Property reference that maps to a known callable
114
+ if (CALLABLE_REFS.has(expr)) return true;
115
+ // Dot-notation that ends in a name (not a string/number/bool)
116
+ // e.g. Date.now, crypto.randomUUID, Math.random
117
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*(\.[A-Za-z_$][A-Za-z0-9_$]*)+$/.test(expr)) {
118
+ // Only treat as callable if first segment is a known safe object
119
+ const root = expr.split('.')[0];
120
+ return SAFE_GLOBALS.has(root);
121
+ }
122
+ // Function call expression e.g. Date.now() or crypto.randomUUID()
123
+ if (/^[A-Za-z_$][A-Za-z0-9_$.]+\(\)$/.test(expr)) return true;
124
+ return false;
125
+ }
126
+
127
+ // ─── Safety ───────────────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * Throw if the expression contains forbidden patterns.
131
+ * This is a defense-in-depth check — the sandbox also blocks these,
132
+ * but we want a clear error message before even attempting evaluation.
133
+ */
134
+ _assertSafe(expr) {
135
+ const forbidden = [
136
+ /\brequire\s*\(/, // require()
137
+ /\bimport\s*\(/, // dynamic import
138
+ /\bprocess\b/, // process.env, process.exit
139
+ /\b__dirname\b/, // filesystem
140
+ /\b__filename\b/, // filesystem
141
+ /\bfs\b/, // fs module
142
+ /\bchild_process\b/, // shell
143
+ /\bexec\b\s*\(/, // exec()
144
+ /\bspawn\b\s*\(/, // spawn()
145
+ /\bfetch\b\s*\(/, // network
146
+ /\bXMLHttpRequest\b/, // network
147
+ /\bnew\s+Function\b/, // Function constructor
148
+ /\beval\s*\(/, // eval
149
+ /\bsetTimeout\b/, // async timing
150
+ /\bsetInterval\b/, // async timing
151
+ /\bglobalThis\b/, // global escape
152
+ /\bself\b/, // global escape
153
+ /\bwindow\b/, // DOM escape
154
+ ];
155
+
156
+ for (const pattern of forbidden) {
157
+ if (pattern.test(expr)) {
158
+ throw new Error(
159
+ `Unsafe expression: "${expr}" contains a forbidden pattern.\n` +
160
+ `Only safe expressions are allowed (literals, Date, Math, crypto).`
161
+ );
162
+ }
163
+ }
164
+ }
165
+
166
+ // ─── Literal evaluation ───────────────────────────────────────────────────
167
+
168
+ _evalLiteral(expr, fieldType) {
169
+ // ── null ──────────────────────────────────────────────────────────────────
170
+ if (expr === 'null' || expr === 'NULL') return null;
171
+
172
+ // ── boolean ───────────────────────────────────────────────────────────────
173
+ if (expr === 'true' || expr === 'True') return true;
174
+ if (expr === 'false' || expr === 'False') return false;
175
+
176
+ // ── quoted string ─────────────────────────────────────────────────────────
177
+ if ((expr.startsWith('"') && expr.endsWith('"')) ||
178
+ (expr.startsWith("'") && expr.endsWith("'"))) {
179
+ return expr.slice(1, -1);
180
+ }
181
+
182
+ // ── number ────────────────────────────────────────────────────────────────
183
+ if (/^-?\d+(\.\d+)?$/.test(expr)) {
184
+ const n = Number(expr);
185
+ if (!isNaN(n)) return n;
186
+ }
187
+
188
+ // ── JSON array / object ───────────────────────────────────────────────────
189
+ if ((expr.startsWith('[') && expr.endsWith(']')) ||
190
+ (expr.startsWith('{') && expr.endsWith('}'))) {
191
+ try { return JSON.parse(expr); } catch {}
192
+ }
193
+
194
+ // ── Field-type coercion for bare unquoted strings ─────────────────────────
195
+ if (fieldType === 'integer' || fieldType === 'bigInteger') {
196
+ const n = parseInt(expr, 10);
197
+ if (!isNaN(n)) return n;
198
+ throw new Error(`"${expr}" is not a valid integer.`);
199
+ }
200
+ if (fieldType === 'float' || fieldType === 'decimal') {
201
+ const f = parseFloat(expr);
202
+ if (!isNaN(f)) return f;
203
+ throw new Error(`"${expr}" is not a valid number.`);
204
+ }
205
+ if (fieldType === 'boolean') {
206
+ if (['1', 'yes', 'y'].includes(expr.toLowerCase())) return true;
207
+ if (['0', 'no', 'n'].includes(expr.toLowerCase())) return false;
208
+ throw new Error(`"${expr}" is not a valid boolean.`);
209
+ }
210
+
211
+ // ── Safe Date/Math expressions ────────────────────────────────────────────
212
+ // e.g. "new Date().toISOString()" evaluated once as a literal snapshot
213
+ if (/^new\s+Date\s*\(/.test(expr) || /^Math\./.test(expr)) {
214
+ try {
215
+ const result = this._sandboxEval(expr);
216
+ return result;
217
+ } catch (e) {
218
+ throw new Error(`Could not evaluate "${expr}": ${e.message}`);
219
+ }
220
+ }
221
+
222
+ // ── Bare unquoted string (last resort for string/text/enum fields) ────────
223
+ if (['string', 'text', 'enum', 'uuid', 'date', 'timestamp'].includes(fieldType)) {
224
+ return expr;
225
+ }
226
+
227
+ throw new Error(
228
+ `Cannot interpret "${expr}" as a ${fieldType}.\n` +
229
+ `For strings, wrap in quotes: '${expr}'\n` +
230
+ `For callables (uuid, timestamp), they will be called per-row at migrate time.`
231
+ );
232
+ }
233
+
234
+ /**
235
+ * Minimal sandbox for safe Date/Math expressions only.
236
+ * Uses Function constructor with a clean scope — no globals exposed.
237
+ */
238
+ _sandboxEval(expr) {
239
+ // eslint-disable-next-line no-new-func
240
+ const fn = new Function('Date', 'Math', `'use strict'; return (${expr});`);
241
+ return fn(Date, Math);
242
+ }
243
+
244
+ // ─── Serialisation ────────────────────────────────────────────────────────
245
+
246
+ /**
247
+ * Convert a literal JS value back to a clean expression string.
248
+ * Used to display confirmation back to the developer.
249
+ */
250
+ _serialiseExpression(value) {
251
+ if (value === null) return 'null';
252
+ if (typeof value === 'string') return JSON.stringify(value);
253
+ if (typeof value === 'number') return String(value);
254
+ if (typeof value === 'boolean') return String(value);
255
+ return JSON.stringify(value);
256
+ }
257
+ }
258
+
259
+ // ─── Safe callable registry ───────────────────────────────────────────────────
260
+
261
+ /**
262
+ * Exact callable references the developer can type.
263
+ * These are resolved to actual functions at migration time.
264
+ */
265
+ const CALLABLE_REFS = new Map([
266
+ ['Date.now', () => Date.now()],
267
+ ['Date.now()', () => Date.now()],
268
+ ['new Date()', () => new Date().toISOString()],
269
+ ['crypto.randomUUID', () => require('crypto').randomUUID()],
270
+ ['crypto.randomUUID()', () => require('crypto').randomUUID()],
271
+ ['Math.random', () => Math.random()],
272
+ ['Math.random()', () => Math.random()],
273
+ ]);
274
+
275
+ /**
276
+ * Safe root objects that are allowed in callable expressions.
277
+ */
278
+ const SAFE_GLOBALS = new Set(['Date', 'Math', 'crypto', 'JSON', 'Number', 'String', 'Boolean']);
279
+
280
+ /**
281
+ * Resolve a stored `oneOffDefault` descriptor into a concrete value or function.
282
+ *
283
+ * Called at migration time (inside AddField.up()), NOT at makemigrations time.
284
+ *
285
+ * @param {{ kind: 'literal'|'callable', value?: *, expression?: string }} descriptor
286
+ * @returns {* | () => *} — a literal value, or a zero-arg function for callables
287
+ */
288
+ function resolveDefault(descriptor) {
289
+ if (!descriptor) return undefined;
290
+
291
+ // Legacy: plain primitive stored directly (backward compat with old migrations)
292
+ if (typeof descriptor !== 'object' || !('kind' in descriptor)) {
293
+ return descriptor;
294
+ }
295
+
296
+ if (descriptor.kind === 'literal') {
297
+ return descriptor.value;
298
+ }
299
+
300
+ if (descriptor.kind === 'callable') {
301
+ const expr = descriptor.expression;
302
+
303
+ // Known safe callable → return the pre-registered function
304
+ if (CALLABLE_REFS.has(expr)) {
305
+ return CALLABLE_REFS.get(expr);
306
+ }
307
+
308
+ // Arrow function or function expression → compile in safe context
309
+ try {
310
+ // eslint-disable-next-line no-new-func
311
+ const fn = new Function('Date', 'Math', 'crypto',
312
+ `'use strict'; return (${expr});`
313
+ )(Date, Math, require('crypto'));
314
+ if (typeof fn === 'function') return fn;
315
+ // Expression evaluated to a value — treat as literal
316
+ return () => fn;
317
+ } catch (e) {
318
+ throw new Error(`Cannot resolve callable default "${expr}": ${e.message}`);
319
+ }
320
+ }
321
+
322
+ throw new Error(`Unknown oneOffDefault descriptor kind: "${descriptor.kind}"`);
323
+ }
324
+
325
+ module.exports = { DefaultValueParser, resolveDefault, CALLABLE_REFS, SAFE_GLOBALS };
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+ const { DefaultValueParser } = require('./DefaultValueParser');
5
+
6
+ /**
7
+ * InteractiveResolver
8
+ *
9
+ * During makemigrations, when a new non-nullable field without a default
10
+ * is being added to an existing table, this resolver prompts the developer.
11
+ *
12
+ * Options:
13
+ * 1) Provide a one-off default value or expression
14
+ * — Supports literals (42, 'hello', true) and callables (Date.now,
15
+ * crypto.randomUUID, () => ...)
16
+ * — Stored in migration file as code, NOT evaluated at makemigrations time
17
+ * — Applied at migrate time: callable = per row, literal = single UPDATE
18
+ *
19
+ * 2) Make the field nullable temporarily
20
+ * — Safest option, can be tightened later with a follow-up migration
21
+ *
22
+ * 3) Abort
23
+ * — Developer must fix their model first, then re-run makemigrations
24
+ *
25
+ * In non-interactive mode (CI / --noinput):
26
+ * Throws with a clear message naming the field and showing all three fixes.
27
+ */
28
+ class InteractiveResolver {
29
+ constructor(options = {}) {
30
+ this._nonInteractive = options.nonInteractive || !process.stdin.isTTY;
31
+ this._parser = new DefaultValueParser();
32
+ }
33
+
34
+ /**
35
+ * Resolve a single dangerous AddField op interactively.
36
+ *
37
+ * @param {object} op — { type: 'AddField', table, column, field, _needsDefault: true }
38
+ * @returns {object} — resolved op
39
+ * @throws {Error} — on abort
40
+ */
41
+ async resolve(op) {
42
+ const { table, column, field } = op;
43
+
44
+ if (this._nonInteractive) {
45
+ throw new Error(
46
+ `\nField "${table}.${column}" is non-nullable with no default.\n` +
47
+ `\nIn non-interactive mode, resolve this before running makemigrations:\n` +
48
+ ` Option A: Add a default to your model:\n` +
49
+ ` ${column}: fields.${field.type || 'string'}({ default: 'some_value' })\n` +
50
+ ` Option B: Make the field nullable:\n` +
51
+ ` ${column}: fields.${field.type || 'string'}({ nullable: true })\n` +
52
+ ` Option C: Run makemigrations interactively to provide a one-off default.\n`
53
+ );
54
+ }
55
+
56
+ this._print('');
57
+ this._print(` It is impossible to add a non-nullable field '${column}' to the`);
58
+ this._print(` '${table}' table without specifying a default. This is because the`);
59
+ this._print(` database needs something to populate existing rows.`);
60
+ this._print('');
61
+ this._print(` Please select a fix:`);
62
+ this._print(` 1) Provide a one-off default now (used only for existing rows)`);
63
+ this._print(` 2) Quit and make '${column}' nullable in your model (recommended)`);
64
+ this._print(` 3) Quit and add a permanent default to your model`);
65
+ this._print('');
66
+
67
+ const choice = await this._prompt(' Select an option: ');
68
+ const trimmed = choice.trim();
69
+
70
+ if (trimmed === '2') {
71
+ this._print('');
72
+ this._print(` Quitting. Add nullable=true to '${column}' in your model:`);
73
+ this._print(` ${column}: fields.${field.type || 'string'}({ nullable: true })`);
74
+ this._print('');
75
+ throw new Error(
76
+ `Aborted. Make '${column}' nullable in your model, then re-run makemigrations.`
77
+ );
78
+ }
79
+
80
+ if (trimmed === '3') {
81
+ this._print('');
82
+ this._print(` Quitting. Add a default to '${column}' in your model:`);
83
+ this._print(` ${column}: fields.${field.type || 'string'}({ default: 'your_value' })`);
84
+ this._print('');
85
+ throw new Error(
86
+ `Aborted. Add a default to '${column}' in your model, then re-run makemigrations.`
87
+ );
88
+ }
89
+
90
+ if (trimmed === '1') {
91
+ return this._promptForDefault(op);
92
+ }
93
+
94
+ // Unrecognised — re-ask
95
+ this._print(` Invalid choice "${trimmed}". Please enter 1, 2, or 3.`);
96
+ return this.resolve(op);
97
+ }
98
+
99
+ /**
100
+ * Resolve all dangerous ops in a list.
101
+ */
102
+ async resolveAll(ops) {
103
+ const resolved = [];
104
+ for (const op of ops) {
105
+ if (op._needsDefault) {
106
+ resolved.push(await this.resolve(op));
107
+ } else {
108
+ resolved.push(op);
109
+ }
110
+ }
111
+ return resolved;
112
+ }
113
+
114
+ // ─── Default value prompt ─────────────────────────────────────────────────
115
+
116
+ async _promptForDefault(op) {
117
+ const { table, column, field } = op;
118
+
119
+ this._print('');
120
+ this._print(` Please enter the default value for '${column}' (${field.type}).`);
121
+ this._print(` This will ONLY be used to populate existing rows — it will not`);
122
+ this._print(` be added to your model definition.`);
123
+ this._print('');
124
+ this._print(` You can enter:`);
125
+ this._print(` A literal value: 42 | 'hello' | true | null`);
126
+ this._print(` A callable: Date.now | crypto.randomUUID | () => new Date().toISOString()`);
127
+ this._print(` (Callables are called once per row at migrate time)`);
128
+ this._print('');
129
+
130
+ const raw = await this._prompt(` Enter default for '${column}': `);
131
+ const trimmed = raw.trim();
132
+
133
+ if (!trimmed) {
134
+ this._print(` No value entered. Please try again.`);
135
+ return this._promptForDefault(op);
136
+ }
137
+
138
+ let parsed;
139
+ try {
140
+ parsed = this._parser.parse(trimmed, field.type);
141
+ } catch (err) {
142
+ this._print('');
143
+ this._print(` ✗ ${err.message}`);
144
+ this._print('');
145
+ return this._promptForDefault(op);
146
+ }
147
+
148
+ // ── Confirm with the developer ────────────────────────────────────────
149
+ this._print('');
150
+ if (parsed.kind === 'callable') {
151
+ this._print(` ✔ Callable: ${parsed.expression}`);
152
+ this._print(` Each existing row will get its own value when migrate runs.`);
153
+ } else {
154
+ this._print(` ✔ Literal: ${parsed.expression}`);
155
+ this._print(` All existing rows will be set to this value.`);
156
+ }
157
+ this._print('');
158
+
159
+ const confirm = await this._prompt(` Use this? [Y/n]: `);
160
+ if (confirm.trim().toLowerCase() === 'n') {
161
+ return this._promptForDefault(op);
162
+ }
163
+
164
+ return {
165
+ ...op,
166
+ oneOffDefault: parsed, // { kind, value?, expression }
167
+ _needsDefault: false,
168
+ };
169
+ }
170
+
171
+ // ─── Internals ─────────────────────────────────────────────────────────────
172
+
173
+ _print(msg) {
174
+ process.stdout.write(msg + '\n');
175
+ }
176
+
177
+ _prompt(question) {
178
+ return new Promise((resolve) => {
179
+ const rl = readline.createInterface({
180
+ input: process.stdin,
181
+ output: process.stdout,
182
+ });
183
+ rl.question(question, (answer) => {
184
+ rl.close();
185
+ resolve(answer);
186
+ });
187
+ });
188
+ }
189
+ }
190
+
191
+ module.exports = InteractiveResolver;