@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.
- package/README.md +260 -170
- package/lib/Auth/Auth.js +2 -2
- package/lib/Build/CssBuilder.js +5 -7
- package/lib/Build/EffectivePropUsage.js +174 -0
- package/lib/Build/FactoryTransform.js +1 -21
- package/lib/Build/FileAnalyzer.js +1 -32
- package/lib/Build/Manager.js +354 -58
- package/lib/Build/PropUsageAnalyzer.js +1189 -0
- package/lib/Build/jsxRuntime.js +25 -155
- package/lib/Build/plugins.js +212 -146
- package/lib/Build/propUtils.js +70 -0
- package/lib/Console/Commands/DevCommand.js +30 -10
- package/lib/Console/Commands/MakeCommand.js +8 -1
- package/lib/Console/Output.js +0 -2
- package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
- package/lib/Console/Stubs/vendor-dev.tsx +30 -41
- package/lib/Console/Stubs/vendor.tsx +25 -1
- package/lib/Core/Config.js +0 -6
- package/lib/Core/Paths.js +0 -19
- package/lib/Database/Migration/Checksum.js +0 -3
- package/lib/Database/Migration/MigrationRepository.js +0 -8
- package/lib/Database/Migration/MigrationRunner.js +1 -2
- package/lib/Database/Model.js +19 -11
- package/lib/Database/QueryBuilder.js +25 -4
- package/lib/Database/Schema/Blueprint.js +10 -0
- package/lib/Database/Schema/Manager.js +2 -0
- package/lib/Date/DateTime.js +1 -1
- package/lib/Dev/DevContext.js +44 -0
- package/lib/Dev/DevErrorPage.js +990 -0
- package/lib/Dev/DevIndicator.js +836 -0
- package/lib/HMR/Server.js +16 -37
- package/lib/Http/Server.js +171 -23
- package/lib/Logging/Log.js +34 -2
- package/lib/Mail/Mail.js +41 -10
- package/lib/Route/Router.js +43 -19
- package/lib/Runtime/Entry.js +10 -6
- package/lib/Session/Manager.js +103 -1
- package/lib/Session/Session.js +0 -4
- package/lib/Support/Str.js +6 -4
- package/lib/Translation/Lang.js +376 -32
- package/lib/Translation/pluralize.js +81 -0
- package/lib/Validation/MagicBytes.js +120 -0
- package/lib/Validation/Validator.js +46 -29
- package/lib/View/Client/hmr-client.js +100 -90
- package/lib/View/Client/spa.js +121 -50
- package/lib/View/ClientManifest.js +60 -0
- package/lib/View/FlightRenderer.js +100 -0
- package/lib/View/Layout.js +0 -3
- package/lib/View/PropFilter.js +81 -0
- package/lib/View/View.js +230 -495
- package/lib/index.d.ts +22 -1
- package/package.json +2 -2
- package/skeleton/config/app.js +1 -0
- package/skeleton/config/server.js +13 -0
- package/skeleton/config/session.js +3 -0
- package/lib/Build/HydrationBuilder.js +0 -190
- package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
- 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
|
-
|
|
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("
|
|
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
|
-
|
|
148
|
-
this.#send("
|
|
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
|
|
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.
|
|
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`;
|
package/lib/Console/Output.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
(
|
|
71
|
+
const webpackChunkLoad = function(chunkId: string) {
|
|
72
|
+
const filename = webpackRequire.u(chunkId);
|
|
92
73
|
|
|
93
|
-
(
|
|
94
|
-
|
|
74
|
+
return import('/storage/' + filename).then((mod: any) => {
|
|
75
|
+
moduleRegistry[chunkId] = mod;
|
|
76
|
+
});
|
|
95
77
|
};
|
|
96
78
|
|
|
97
|
-
(window
|
|
98
|
-
|
|
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
|
});
|
package/lib/Core/Config.js
CHANGED
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.
|
|
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}`);
|
package/lib/Database/Model.js
CHANGED
|
@@ -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 =
|
|
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,
|
|
135
|
+
return DB.table(this.table, this).where(column, operator);
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
return DB.table(this.table,
|
|
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,
|
|
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,
|
|
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,
|
|
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 =
|
|
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
|
-
*
|
|
236
|
+
* Serialize model to plain object.
|
|
237
|
+
* Called automatically by JSON.stringify() and RSC Flight serializer.
|
|
230
238
|
* @returns {Object}
|
|
231
239
|
*/
|
|
232
|
-
|
|
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 =
|
|
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,
|
|
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(
|
|
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
|
}
|