@nitronjs/framework 0.2.27 → 0.3.0

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 (58) hide show
  1. package/README.md +260 -170
  2. package/lib/Auth/Auth.js +2 -2
  3. package/lib/Build/CssBuilder.js +5 -7
  4. package/lib/Build/EffectivePropUsage.js +174 -0
  5. package/lib/Build/FactoryTransform.js +1 -21
  6. package/lib/Build/FileAnalyzer.js +1 -32
  7. package/lib/Build/Manager.js +354 -58
  8. package/lib/Build/PropUsageAnalyzer.js +1189 -0
  9. package/lib/Build/jsxRuntime.js +25 -155
  10. package/lib/Build/plugins.js +212 -146
  11. package/lib/Build/propUtils.js +70 -0
  12. package/lib/Console/Commands/DevCommand.js +30 -10
  13. package/lib/Console/Commands/MakeCommand.js +8 -1
  14. package/lib/Console/Output.js +0 -2
  15. package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
  16. package/lib/Console/Stubs/vendor-dev.tsx +30 -41
  17. package/lib/Console/Stubs/vendor.tsx +25 -1
  18. package/lib/Core/Config.js +0 -6
  19. package/lib/Core/Paths.js +0 -19
  20. package/lib/Database/Migration/Checksum.js +0 -3
  21. package/lib/Database/Migration/MigrationRepository.js +0 -8
  22. package/lib/Database/Migration/MigrationRunner.js +1 -2
  23. package/lib/Database/Model.js +19 -11
  24. package/lib/Database/QueryBuilder.js +25 -4
  25. package/lib/Database/Schema/Blueprint.js +10 -0
  26. package/lib/Database/Schema/Manager.js +2 -0
  27. package/lib/Date/DateTime.js +1 -1
  28. package/lib/Dev/DevContext.js +44 -0
  29. package/lib/Dev/DevErrorPage.js +990 -0
  30. package/lib/Dev/DevIndicator.js +836 -0
  31. package/lib/HMR/Server.js +16 -37
  32. package/lib/Http/Server.js +171 -23
  33. package/lib/Logging/Log.js +34 -2
  34. package/lib/Mail/Mail.js +41 -10
  35. package/lib/Route/Router.js +43 -19
  36. package/lib/Runtime/Entry.js +10 -6
  37. package/lib/Session/Manager.js +103 -1
  38. package/lib/Session/Session.js +0 -4
  39. package/lib/Support/Str.js +6 -4
  40. package/lib/Translation/Lang.js +376 -32
  41. package/lib/Translation/pluralize.js +81 -0
  42. package/lib/Validation/MagicBytes.js +120 -0
  43. package/lib/Validation/Validator.js +46 -29
  44. package/lib/View/Client/hmr-client.js +100 -90
  45. package/lib/View/Client/spa.js +121 -50
  46. package/lib/View/ClientManifest.js +60 -0
  47. package/lib/View/FlightRenderer.js +100 -0
  48. package/lib/View/Layout.js +0 -3
  49. package/lib/View/PropFilter.js +81 -0
  50. package/lib/View/View.js +230 -495
  51. package/lib/index.d.ts +22 -1
  52. package/package.json +2 -2
  53. package/skeleton/config/app.js +1 -0
  54. package/skeleton/config/server.js +13 -0
  55. package/skeleton/config/session.js +3 -0
  56. package/lib/Build/HydrationBuilder.js +0 -190
  57. package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
  58. package/lib/Console/Stubs/page-hydration.tsx +0 -53
@@ -0,0 +1,70 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+
4
+ const EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
5
+
6
+ /**
7
+ * Merges two usage trees (union of keys).
8
+ * true + anything → true (whole usage wins).
9
+ * @param {object|true} a
10
+ * @param {object|true} b
11
+ * @returns {object|true}
12
+ */
13
+ export function mergeUsageTrees(a, b) {
14
+ if (a === true || b === true) return true;
15
+ if (!a || typeof a !== "object") return b;
16
+ if (!b || typeof b !== "object") return a;
17
+
18
+ const merged = Object.create(null);
19
+ Object.assign(merged, a);
20
+
21
+ for (const key of Object.keys(b)) {
22
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
23
+ if (merged[key] === true) continue;
24
+
25
+ if (!merged[key]) {
26
+ merged[key] = b[key];
27
+ }
28
+ else if (typeof merged[key] === "object" && typeof b[key] === "object") {
29
+ merged[key] = mergeUsageTrees(merged[key], b[key]);
30
+ }
31
+ else {
32
+ merged[key] = true;
33
+ }
34
+ }
35
+
36
+ return merged;
37
+ }
38
+
39
+ /**
40
+ * Resolves a relative import path to an absolute file path.
41
+ * Checks extensions (.tsx, .ts, .jsx, .js) and directory index files.
42
+ * @param {string} importPath - e.g. "./UserDropdown"
43
+ * @param {string} fromDir - Directory of the importing file.
44
+ * @returns {string|null}
45
+ */
46
+ export function resolveComponentPath(importPath, fromDir) {
47
+ if (!importPath.startsWith(".")) return null;
48
+
49
+ const basePath = path.resolve(fromDir, importPath);
50
+
51
+ for (const ext of EXTENSIONS) {
52
+ const full = basePath + ext;
53
+ if (fs.existsSync(full)) return full;
54
+ }
55
+
56
+ if (fs.existsSync(basePath)) {
57
+ const stat = fs.statSync(basePath);
58
+
59
+ if (stat.isDirectory()) {
60
+ for (const ext of EXTENSIONS) {
61
+ const indexPath = path.join(basePath, "index" + ext);
62
+ if (fs.existsSync(indexPath)) return indexPath;
63
+ }
64
+ }
65
+
66
+ return basePath;
67
+ }
68
+
69
+ return null;
70
+ }
@@ -7,6 +7,7 @@ import fs from "fs";
7
7
  import Paths from "../../Core/Paths.js";
8
8
  import Environment from "../../Core/Environment.js";
9
9
  import Builder from "../../Build/Manager.js";
10
+ import Layout from "../../View/Layout.js";
10
11
 
11
12
  dotenv.config({ quiet: true });
12
13
  Environment.setDev(true);
@@ -35,6 +36,7 @@ class DevServer {
35
36
  #building = false;
36
37
  #builder = new Builder();
37
38
  #debounce = { build: null, restart: null };
39
+ #banner = null;
38
40
 
39
41
  #log(icon, msg, extra) {
40
42
  const time = new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
@@ -79,7 +81,11 @@ class DevServer {
79
81
  if (code && code !== 0) this.#log("err", `Exit code ${code}`);
80
82
  this.#proc = null;
81
83
  });
82
- setTimeout(resolve, 500);
84
+ this.#proc.on("message", msg => {
85
+ if (msg?.type === "banner") this.#banner = msg.text;
86
+ if (msg?.type === "ready") resolve();
87
+ });
88
+ setTimeout(resolve, 5000);
83
89
  });
84
90
  }
85
91
 
@@ -133,7 +139,7 @@ class DevServer {
133
139
  clearTimeout(this.#debounce.build);
134
140
  this.#debounce.build = setTimeout(async () => {
135
141
  const r = await this.#build("css");
136
- if (r.success) this.#send("css", { filePath });
142
+ if (r.success) this.#send("change", { changeType: "css", file: rel });
137
143
  }, 100);
138
144
  return;
139
145
  }
@@ -144,8 +150,12 @@ class DevServer {
144
150
  this.#debounce.build = setTimeout(async () => {
145
151
  const r = await this.#build();
146
152
  if (r.success) {
147
- if (r.cssChanged) this.#send("css", {});
148
- this.#send("view", { filePath });
153
+ const changeType = detectChangeType(filePath);
154
+ this.#send("change", {
155
+ changeType,
156
+ cssChanged: r.cssChanged || false,
157
+ file: rel
158
+ });
149
159
  }
150
160
  }, 100);
151
161
  return;
@@ -166,7 +176,7 @@ class DevServer {
166
176
  this.#log("info", `Unmatched: ${rel}`);
167
177
  }
168
178
 
169
- #watch() {
179
+ async #watch() {
170
180
  const candidates = [
171
181
  path.join(Paths.project, "resources/views"),
172
182
  path.join(Paths.project, "resources/css"),
@@ -196,17 +206,21 @@ class DevServer {
196
206
  watcher.on("change", p => this.#onChange(p));
197
207
  watcher.on("add", p => this.#onChange(p));
198
208
  watcher.on("error", e => this.#log("err", `Watch error: ${e.message}`));
199
- watcher.on("ready", () => {
200
- this.#log("watch", `Watching ${watchPaths.length} directories`);
201
- });
202
209
 
203
- return watcher;
210
+ return new Promise(resolve => {
211
+ watcher.on("ready", () => {
212
+ this.#log("watch", `Watching ${watchPaths.length} directories`);
213
+ resolve(watcher);
214
+ });
215
+ });
204
216
  }
205
217
 
206
218
  async start() {
207
219
  await this.#build();
208
220
  await this.#start();
209
- const watcher = this.#watch();
221
+ const watcher = await this.#watch();
222
+
223
+ if (this.#banner) console.log(this.#banner);
210
224
 
211
225
  const exit = async () => {
212
226
  console.log();
@@ -220,6 +234,12 @@ class DevServer {
220
234
  }
221
235
  }
222
236
 
237
+ function detectChangeType(filePath) {
238
+ if (Layout.isLayout(filePath)) return "layout";
239
+
240
+ return "page";
241
+ }
242
+
223
243
  export default async function Dev() {
224
244
  await new DevServer().start();
225
245
  }
@@ -64,7 +64,14 @@ export default async function make(type, rawName) {
64
64
 
65
65
  const parts = rawName.split("/");
66
66
  const subDirs = parts.slice(0, -1).join("/");
67
- const outputDir = path.join(Paths.project, baseDirs[type]);
67
+ const outputDir = path.resolve(Paths.project, baseDirs[type]);
68
+ const resolvedSubDir = path.resolve(outputDir, subDirs);
69
+
70
+ if (!resolvedSubDir.startsWith(outputDir)) {
71
+ Output.error("Invalid path: name cannot contain path traversal (e.g. '../').");
72
+
73
+ return false;
74
+ }
68
75
 
69
76
  let className = parts[parts.length - 1];
70
77
  let fileName = `${className}.js`;
@@ -57,7 +57,6 @@ const step = (icon, color, action, target, suffix = "") => {
57
57
  const success = (msg) => log(`${C.green}${I.success}${C.reset} ${msg}`);
58
58
  const error = (msg) => log(`${C.red}${I.error}${C.reset} ${msg}`);
59
59
  const warn = (msg) => log(`${C.yellow}${I.warn}${C.reset} ${msg}`);
60
- const info = (msg) => log(`${C.blue}${I.info}${C.reset} ${msg}`);
61
60
  const dim = (msg) => log(`${C.dim}${msg}${C.reset}`);
62
61
  const newline = () => log();
63
62
  const errorDetail = (msg) => log(` ${C.dim}${msg}${C.reset}`);
@@ -119,7 +118,6 @@ export default {
119
118
  success,
120
119
  error,
121
120
  warn,
122
- info,
123
121
  dim,
124
122
  newline,
125
123
  errorDetail,
@@ -0,0 +1,74 @@
1
+ import React from "react";
2
+ import { hydrateRoot } from "react-dom/client";
3
+ // @ts-ignore — no type declarations for this package
4
+ import { createFromReadableStream } from "react-server-dom-webpack/client.browser";
5
+
6
+ declare global {
7
+ interface Window {
8
+ __NITRON_FLIGHT__?: string;
9
+ __NITRON_RSC__?: {
10
+ root: any;
11
+ navigate: (payload: string) => void;
12
+ };
13
+ }
14
+ }
15
+
16
+ // Convert Flight payload string to a ReadableStream
17
+ function payloadToStream(payload: string): ReadableStream<Uint8Array> {
18
+ return new ReadableStream({
19
+ start(controller) {
20
+ controller.enqueue(new TextEncoder().encode(payload));
21
+ controller.close();
22
+ }
23
+ });
24
+ }
25
+
26
+ // Navigate to a new page using RSC payload (called by spa.js)
27
+ function navigateWithPayload(payload: string) {
28
+ const rsc = window.__NITRON_RSC__;
29
+
30
+ if (!rsc || !rsc.root) return;
31
+
32
+ const stream = payloadToStream(payload);
33
+ const response = createFromReadableStream(stream);
34
+
35
+ function Root() {
36
+ return React.use(response);
37
+ }
38
+
39
+ rsc.root.render(React.createElement(Root));
40
+ }
41
+
42
+ function mount() {
43
+ const payload = window.__NITRON_FLIGHT__;
44
+
45
+ if (!payload) return;
46
+
47
+ const stream = payloadToStream(payload);
48
+ const rscResponse = createFromReadableStream(stream);
49
+
50
+ function Root(): React.ReactNode {
51
+ return React.use(rscResponse) as React.ReactNode;
52
+ }
53
+
54
+ const container = document.getElementById("app");
55
+
56
+ if (!container) return;
57
+
58
+ const root = hydrateRoot(container, React.createElement(Root));
59
+
60
+ // Expose RSC functions for SPA navigation
61
+ window.__NITRON_RSC__ = {
62
+ root,
63
+ navigate: navigateWithPayload
64
+ };
65
+
66
+ delete window.__NITRON_FLIGHT__;
67
+ }
68
+
69
+ if (document.readyState === "loading") {
70
+ document.addEventListener("DOMContentLoaded", mount);
71
+ }
72
+ else {
73
+ mount();
74
+ }
@@ -2,15 +2,17 @@ import * as React from 'react';
2
2
  import * as ReactDOM from 'react-dom';
3
3
  import * as ReactDOMClient from 'react-dom/client';
4
4
  import * as OriginalJsx from 'react/jsx-runtime';
5
- import RefreshRuntime from 'react-refresh/runtime';
6
-
7
- // Filter React 19 key warnings from third-party libraries
5
+ // Filter React 19 warnings that are false positives in RSC (Flight protocol)
6
+ // - key-in-props: third-party libraries spreading key in JSX
7
+ // - key-in-list: Flight wire format loses static/dynamic children distinction,
8
+ // causing React to warn about keys on children that were originally static
8
9
  const originalError = console.error;
9
10
  console.error = function(...args: any[]) {
10
11
  const msg = args[0];
11
12
  if (typeof msg === 'string' && (
12
13
  msg.includes('A props object containing a "key" prop is being spread into JSX') ||
13
- msg.includes('`key` is not a prop')
14
+ msg.includes('`key` is not a prop') ||
15
+ msg.includes('Each child in a list should have a unique "key" prop')
14
16
  )) {
15
17
  return;
16
18
  }
@@ -53,47 +55,34 @@ const NitronJSXRuntime = {
53
55
  Fragment: OriginalJsx.Fragment
54
56
  };
55
57
 
56
- Object.assign(window, {
57
- __NITRON_REACT__: React,
58
- __NITRON_REACT_DOM__: ReactDOM,
59
- __NITRON_REACT_DOM_CLIENT__: ReactDOMClient,
60
- __NITRON_JSX_RUNTIME__: NitronJSXRuntime
61
- });
62
-
63
- RefreshRuntime.injectIntoGlobalHook(window);
58
+ // Webpack shims for react-server-dom-webpack/client chunk loading.
59
+ // The Flight client uses __webpack_chunk_load__ to load client component
60
+ // JS files and __webpack_require__ to access their exports.
61
+ const moduleRegistry: Record<string, any> = {};
64
62
 
65
- interface RefreshModule {
66
- performReactRefresh: () => void;
67
- register: (type: any, id: string) => void;
68
- createSignatureFunctionForTransform: () => (type: any, key: string, forceReset?: boolean, getCustomHooks?: () => any[]) => any;
69
- isLikelyComponentType: (type: any) => boolean;
70
- }
63
+ const webpackRequire: any = function(moduleId: string) {
64
+ return moduleRegistry[moduleId];
65
+ };
71
66
 
72
- const NitronRefresh: RefreshModule = {
73
- performReactRefresh: () => {
74
- if ((RefreshRuntime as any).hasUnrecoverableErrors?.()) {
75
- window.location.reload();
76
- return;
77
- }
78
- RefreshRuntime.performReactRefresh();
79
- },
80
- register: (type: any, id: string) => {
81
- RefreshRuntime.register(type, id);
82
- },
83
- createSignatureFunctionForTransform: () => {
84
- return RefreshRuntime.createSignatureFunctionForTransform();
85
- },
86
- isLikelyComponentType: (type: any) => {
87
- return RefreshRuntime.isLikelyComponentType(type);
88
- }
67
+ webpackRequire.u = function(chunkId: string) {
68
+ return chunkId;
89
69
  };
90
70
 
91
- (window as any).__NITRON_REFRESH__ = NitronRefresh;
71
+ const webpackChunkLoad = function(chunkId: string) {
72
+ const filename = webpackRequire.u(chunkId);
92
73
 
93
- (window as any).$RefreshReg$ = (type: any, id: string) => {
94
- RefreshRuntime.register(type, id);
74
+ return import('/storage/' + filename).then((mod: any) => {
75
+ moduleRegistry[chunkId] = mod;
76
+ });
95
77
  };
96
78
 
97
- (window as any).$RefreshSig$ = () => {
98
- return RefreshRuntime.createSignatureFunctionForTransform();
99
- };
79
+ Object.assign(window, {
80
+ __NITRON_REACT__: React,
81
+ __NITRON_REACT_DOM__: ReactDOM,
82
+ __NITRON_REACT_DOM_CLIENT__: ReactDOMClient,
83
+ __NITRON_JSX_RUNTIME__: NitronJSXRuntime,
84
+ __webpack_require__: webpackRequire,
85
+ __webpack_chunk_load__: webpackChunkLoad,
86
+ __webpack_get_script_filename__: webpackRequire.u
87
+ });
88
+
@@ -38,9 +38,33 @@ const NitronJSXRuntime = {
38
38
  Fragment: OriginalJsx.Fragment
39
39
  };
40
40
 
41
+ // Webpack shims for react-server-dom-webpack/client chunk loading.
42
+ // The Flight client uses __webpack_chunk_load__ to load client component
43
+ // JS files and __webpack_require__ to access their exports.
44
+ const moduleRegistry: Record<string, any> = {};
45
+
46
+ const webpackRequire: any = function(moduleId: string) {
47
+ return moduleRegistry[moduleId];
48
+ };
49
+
50
+ webpackRequire.u = function(chunkId: string) {
51
+ return chunkId;
52
+ };
53
+
54
+ const webpackChunkLoad = function(chunkId: string) {
55
+ const filename = webpackRequire.u(chunkId);
56
+
57
+ return import('/storage/' + filename).then((mod: any) => {
58
+ moduleRegistry[chunkId] = mod;
59
+ });
60
+ };
61
+
41
62
  Object.assign(window, {
42
63
  __NITRON_REACT__: React,
43
64
  __NITRON_REACT_DOM__: ReactDOM,
44
65
  __NITRON_REACT_DOM_CLIENT__: ReactDOMClient,
45
- __NITRON_JSX_RUNTIME__: NitronJSXRuntime
66
+ __NITRON_JSX_RUNTIME__: NitronJSXRuntime,
67
+ __webpack_require__: webpackRequire,
68
+ __webpack_chunk_load__: webpackChunkLoad,
69
+ __webpack_get_script_filename__: webpackRequire.u
46
70
  });
@@ -76,12 +76,6 @@ class Config {
76
76
  return this.#configs[name] || {};
77
77
  }
78
78
 
79
- /**
80
- * Check if config system is initialized
81
- */
82
- static isInitialized() {
83
- return this.#initialized;
84
- }
85
79
  }
86
80
 
87
81
  export default Config;
package/lib/Core/Paths.js CHANGED
@@ -64,10 +64,6 @@ class Paths {
64
64
  return path.join(this.#project, "app/Middlewares");
65
65
  }
66
66
 
67
- static get models() {
68
- return path.join(this.#project, "app/Models");
69
- }
70
-
71
67
  static get routes() {
72
68
  return path.join(this.#project, "routes");
73
69
  }
@@ -172,14 +168,6 @@ class Paths {
172
168
  return pathToFileURL(path.join(this.#project, `routes/${name}.js`)).href;
173
169
  }
174
170
 
175
- static migrationUrl(file) {
176
- return pathToFileURL(path.join(this.migrations, file)).href;
177
- }
178
-
179
- static seederUrl(file) {
180
- return pathToFileURL(path.join(this.seeders, file)).href;
181
- }
182
-
183
171
  static kernelUrl() {
184
172
  return pathToFileURL(path.join(this.#project, "app/Kernel.js")).href;
185
173
  }
@@ -192,13 +180,6 @@ class Paths {
192
180
  return path.join(this.#project, ...segments);
193
181
  }
194
182
 
195
- static frameworkJoin(...segments) {
196
- return path.join(this.#framework, ...segments);
197
- }
198
-
199
- static resolve(...segments) {
200
- return path.resolve(this.#project, ...segments);
201
- }
202
183
  }
203
184
 
204
185
  export default Paths;
@@ -10,9 +10,6 @@ class Checksum {
10
10
  return createHash('sha256').update(content.replace(/\r\n/g, '\n').trim(), 'utf8').digest('hex');
11
11
  }
12
12
 
13
- static verify(filePath, expectedChecksum) {
14
- return this.fromFile(filePath) === expectedChecksum;
15
- }
16
13
  }
17
14
 
18
15
  export default Checksum;
@@ -29,10 +29,6 @@ class MigrationRepository {
29
29
  return await this.#getMaxBatch();
30
30
  }
31
31
 
32
- static async getByBatch(batch) {
33
- return await DB.table(this.table).where('batch', batch).orderBy('id', 'desc').get();
34
- }
35
-
36
32
  static async getLastBatches(steps = 1) {
37
33
  const lastBatch = await this.getLastBatchNumber();
38
34
  if (lastBatch === 0) return [];
@@ -56,10 +52,6 @@ class MigrationRepository {
56
52
  return await DB.table(this.table).where('name', name).first();
57
53
  }
58
54
 
59
- static async exists(name) {
60
- return (await this.find(name)) !== null;
61
- }
62
-
63
55
  static async getChecksum(name) {
64
56
  return (await this.find(name))?.checksum || null;
65
57
  }
@@ -156,8 +156,7 @@ class MigrationRunner {
156
156
  }
157
157
 
158
158
  await MigrationRepository.delete(file);
159
- Output.rollback("Rolled back", file);
160
- Output.newline();
159
+ Output.done("Rolled back", file);
161
160
  }
162
161
  catch (rollbackError) {
163
162
  Output.error(`Rollback failed for ${file}: ${rollbackError.message}`);
@@ -27,7 +27,7 @@ class Model {
27
27
  Object.defineProperty(this, '_exists', { value: false, writable: true });
28
28
 
29
29
  Object.assign(this._attributes, attrs);
30
- this._original = { ...this._attributes };
30
+ this._original = structuredClone(this._attributes);
31
31
 
32
32
  return new Proxy(this, {
33
33
  get: (target, prop) => {
@@ -60,6 +60,13 @@ class Model {
60
60
 
61
61
  return true;
62
62
  },
63
+ has: (target, prop) => {
64
+ if (typeof prop === 'symbol' || prop.startsWith('_')) {
65
+ return prop in target;
66
+ }
67
+
68
+ return prop in target._attributes || prop in target;
69
+ },
63
70
  ownKeys: (target) => {
64
71
  return [...Object.keys(target._attributes), ...Object.getOwnPropertyNames(target)];
65
72
  },
@@ -125,10 +132,10 @@ class Model {
125
132
  ensureTable(this);
126
133
 
127
134
  if (arguments.length === 2) {
128
- return DB.table(this.table, null, this).where(column, operator);
135
+ return DB.table(this.table, this).where(column, operator);
129
136
  }
130
137
 
131
- return DB.table(this.table, null, this).where(column, operator, value);
138
+ return DB.table(this.table, this).where(column, operator, value);
132
139
  }
133
140
 
134
141
  /**
@@ -139,7 +146,7 @@ class Model {
139
146
  static select(...columns) {
140
147
  ensureTable(this);
141
148
 
142
- return DB.table(this.table, null, this).select(...columns);
149
+ return DB.table(this.table, this).select(...columns);
143
150
  }
144
151
 
145
152
  /**
@@ -151,7 +158,7 @@ class Model {
151
158
  static orderBy(column, direction = 'ASC') {
152
159
  ensureTable(this);
153
160
 
154
- return DB.table(this.table, null, this).orderBy(column, direction);
161
+ return DB.table(this.table, this).orderBy(column, direction);
155
162
  }
156
163
 
157
164
  /**
@@ -162,7 +169,7 @@ class Model {
162
169
  static limit(value) {
163
170
  ensureTable(this);
164
171
 
165
- return DB.table(this.table, null, this).limit(value);
172
+ return DB.table(this.table, this).limit(value);
166
173
  }
167
174
 
168
175
  // ─────────────────────────────────────────────────────────────────────────────
@@ -204,7 +211,7 @@ class Model {
204
211
  this._exists = true;
205
212
  }
206
213
 
207
- this._original = { ...this._attributes };
214
+ this._original = structuredClone(this._attributes);
208
215
 
209
216
  return this;
210
217
  }
@@ -226,10 +233,11 @@ class Model {
226
233
  }
227
234
 
228
235
  /**
229
- * Convert model attributes to plain object.
236
+ * Serialize model to plain object.
237
+ * Called automatically by JSON.stringify() and RSC Flight serializer.
230
238
  * @returns {Object}
231
239
  */
232
- toObject() {
240
+ toJSON() {
233
241
  return { ...this._attributes };
234
242
  }
235
243
  }
@@ -264,7 +272,7 @@ function hydrate(modelClass, row) {
264
272
  }
265
273
 
266
274
  instance._exists = true;
267
- instance._original = { ...instance._attributes };
275
+ instance._original = structuredClone(instance._attributes);
268
276
 
269
277
  return instance;
270
278
  }
@@ -285,7 +293,7 @@ const QUERY_METHODS = [
285
293
  for (const method of QUERY_METHODS) {
286
294
  Model[method] = function(...args) {
287
295
  ensureTable(this);
288
- return DB.table(this.table, null, this)[method](...args);
296
+ return DB.table(this.table, this)[method](...args);
289
297
  };
290
298
  }
291
299
 
@@ -378,14 +378,17 @@ class QueryBuilder {
378
378
  throw new Error('whereBetween requires array with exactly 2 values');
379
379
  }
380
380
 
381
+ const min = this.#validateWhereValue(values[0]);
382
+ const max = this.#validateWhereValue(values[1]);
383
+
381
384
  this.#wheres.push({
382
385
  type: 'between',
383
386
  column: this.#validateIdentifier(column),
384
- values,
387
+ values: [min, max],
385
388
  boolean: 'AND'
386
389
  });
387
390
 
388
- this.#bindings.push(...values);
391
+ this.#bindings.push(min, max);
389
392
 
390
393
  return this;
391
394
  }
@@ -636,6 +639,7 @@ class QueryBuilder {
636
639
  const validatedCol = this.#validateIdentifier(col);
637
640
  const val = data[col];
638
641
  if (this.#isRawExpression(val)) {
642
+ validateRawExpression(val.value);
639
643
  sets.push(`${validatedCol} = ${val.value}`);
640
644
  }
641
645
 
@@ -661,6 +665,10 @@ class QueryBuilder {
661
665
  }
662
666
 
663
667
  async delete() {
668
+ if (this.#wheres.length === 0) {
669
+ throw new Error("delete() requires at least one WHERE condition. Use truncate() to delete all records.");
670
+ }
671
+
664
672
  const connection = await this.#getConnection();
665
673
  try {
666
674
  const sql = `DELETE FROM ${this.#quoteIdentifier(this.#table)}${this.#compileWheres()}`;
@@ -678,6 +686,21 @@ class QueryBuilder {
678
686
  }
679
687
  }
680
688
 
689
+ async truncate() {
690
+ const connection = await this.#getConnection();
691
+ try {
692
+ await connection.query(`TRUNCATE TABLE ${this.#quoteIdentifier(this.#table)}`);
693
+ }
694
+
695
+ catch (error) {
696
+ throw this.#sanitizeError(error);
697
+ }
698
+
699
+ finally {
700
+ this.#reset();
701
+ }
702
+ }
703
+
681
704
  #toSql() {
682
705
  const distinct = this.#distinctFlag ? 'DISTINCT ' : '';
683
706
  const columns = this.#selectColumns.map(col => {
@@ -730,8 +753,6 @@ class QueryBuilder {
730
753
  return `${boolean}${where.column} IS NOT NULL`;
731
754
  case 'between':
732
755
  return `${boolean}${where.column} BETWEEN ? AND ?`;
733
- case 'raw':
734
- return `${boolean}${where.sql}`;
735
756
  default:
736
757
  return '';
737
758
  }