tina4-nodejs 3.0.0-rc.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.
- package/BENCHMARK_REPORT.md +96 -0
- package/CARBONAH.md +140 -0
- package/CLAUDE.md +599 -0
- package/COMPARISON.md +194 -0
- package/README.md +595 -0
- package/package.json +59 -0
- package/packages/cli/src/bin.ts +110 -0
- package/packages/cli/src/commands/init.ts +194 -0
- package/packages/cli/src/commands/migrate.ts +96 -0
- package/packages/cli/src/commands/migrateCreate.ts +59 -0
- package/packages/cli/src/commands/routes.ts +61 -0
- package/packages/cli/src/commands/serve.ts +58 -0
- package/packages/cli/src/commands/test.ts +83 -0
- package/packages/core/gallery/auth/meta.json +1 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
- package/packages/core/gallery/database/meta.json +1 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
- package/packages/core/gallery/error-overlay/meta.json +1 -0
- package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
- package/packages/core/gallery/orm/meta.json +1 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
- package/packages/core/gallery/queue/meta.json +1 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
- package/packages/core/gallery/rest-api/meta.json +1 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
- package/packages/core/gallery/templates/meta.json +1 -0
- package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
- package/packages/core/public/css/tina4.css +2463 -0
- package/packages/core/public/css/tina4.min.css +1 -0
- package/packages/core/public/favicon.ico +0 -0
- package/packages/core/public/images/logo.svg +5 -0
- package/packages/core/public/images/tina4-logo-icon.webp +0 -0
- package/packages/core/public/js/frond.min.js +420 -0
- package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
- package/packages/core/public/js/tina4.min.js +93 -0
- package/packages/core/public/swagger/index.html +90 -0
- package/packages/core/public/swagger/oauth2-redirect.html +63 -0
- package/packages/core/src/ai.ts +359 -0
- package/packages/core/src/api.ts +248 -0
- package/packages/core/src/auth.ts +287 -0
- package/packages/core/src/cache.ts +121 -0
- package/packages/core/src/constants.ts +48 -0
- package/packages/core/src/container.ts +90 -0
- package/packages/core/src/devAdmin.ts +2024 -0
- package/packages/core/src/devMailbox.ts +316 -0
- package/packages/core/src/dotenv.ts +172 -0
- package/packages/core/src/errorOverlay.test.ts +122 -0
- package/packages/core/src/errorOverlay.ts +278 -0
- package/packages/core/src/events.ts +112 -0
- package/packages/core/src/fakeData.ts +309 -0
- package/packages/core/src/graphql.ts +812 -0
- package/packages/core/src/health.ts +31 -0
- package/packages/core/src/htmlElement.ts +172 -0
- package/packages/core/src/i18n.ts +136 -0
- package/packages/core/src/index.ts +88 -0
- package/packages/core/src/logger.ts +226 -0
- package/packages/core/src/messenger.ts +822 -0
- package/packages/core/src/middleware.ts +138 -0
- package/packages/core/src/queue.ts +481 -0
- package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
- package/packages/core/src/rateLimiter.ts +107 -0
- package/packages/core/src/request.ts +189 -0
- package/packages/core/src/response.ts +146 -0
- package/packages/core/src/routeDiscovery.ts +87 -0
- package/packages/core/src/router.ts +398 -0
- package/packages/core/src/scss.ts +366 -0
- package/packages/core/src/server.ts +610 -0
- package/packages/core/src/service.ts +380 -0
- package/packages/core/src/session.ts +480 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
- package/packages/core/src/static.ts +58 -0
- package/packages/core/src/testing.ts +233 -0
- package/packages/core/src/types.ts +98 -0
- package/packages/core/src/watcher.ts +37 -0
- package/packages/core/src/websocket.ts +408 -0
- package/packages/core/src/wsdl.ts +546 -0
- package/packages/core/templates/errors/302.twig +14 -0
- package/packages/core/templates/errors/401.twig +9 -0
- package/packages/core/templates/errors/403.twig +29 -0
- package/packages/core/templates/errors/404.twig +29 -0
- package/packages/core/templates/errors/500.twig +38 -0
- package/packages/core/templates/errors/502.twig +9 -0
- package/packages/core/templates/errors/503.twig +12 -0
- package/packages/core/templates/errors/base.twig +37 -0
- package/packages/frond/src/engine.ts +1475 -0
- package/packages/frond/src/index.ts +2 -0
- package/packages/orm/src/adapters/firebird.ts +455 -0
- package/packages/orm/src/adapters/mssql.ts +440 -0
- package/packages/orm/src/adapters/mysql.ts +355 -0
- package/packages/orm/src/adapters/postgres.ts +362 -0
- package/packages/orm/src/adapters/sqlite.ts +270 -0
- package/packages/orm/src/autoCrud.ts +231 -0
- package/packages/orm/src/baseModel.ts +536 -0
- package/packages/orm/src/database.ts +321 -0
- package/packages/orm/src/fakeData.ts +118 -0
- package/packages/orm/src/index.ts +49 -0
- package/packages/orm/src/migration.ts +392 -0
- package/packages/orm/src/model.ts +56 -0
- package/packages/orm/src/query.ts +113 -0
- package/packages/orm/src/seeder.ts +120 -0
- package/packages/orm/src/sqlTranslation.ts +272 -0
- package/packages/orm/src/types.ts +110 -0
- package/packages/orm/src/validation.ts +93 -0
- package/packages/swagger/src/generator.ts +189 -0
- package/packages/swagger/src/index.ts +2 -0
- package/packages/swagger/src/ui.ts +48 -0
- package/skills/tina4-developer.skill +0 -0
- package/skills/tina4-js.skill +0 -0
- package/skills/tina4-maintainer.skill +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { initProject } from "./commands/init.js";
|
|
2
|
+
import { serveProject } from "./commands/serve.js";
|
|
3
|
+
import { runMigrations } from "./commands/migrate.js";
|
|
4
|
+
import { createMigration } from "./commands/migrateCreate.js";
|
|
5
|
+
import { listRoutes } from "./commands/routes.js";
|
|
6
|
+
import { runTests } from "./commands/test.js";
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const command = args[0];
|
|
10
|
+
|
|
11
|
+
const HELP = `
|
|
12
|
+
tina4nodejs — This is not a framework.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
tina4nodejs init [dir] Create a new Tina4 project (default: current directory)
|
|
16
|
+
tina4nodejs serve Start the dev server with hot-reload
|
|
17
|
+
tina4nodejs migrate Run pending SQL migrations
|
|
18
|
+
tina4nodejs migrate:create <desc> Create a new migration file
|
|
19
|
+
tina4nodejs routes List all registered routes
|
|
20
|
+
tina4nodejs test [file] Run project tests
|
|
21
|
+
tina4nodejs ai Detect AI coding tools and install context
|
|
22
|
+
tina4nodejs help Show this help message
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--port <number> Server port (default: 7148)
|
|
26
|
+
--all Install AI context for all tools (with ai command)
|
|
27
|
+
--force Overwrite existing AI context files (with ai command)
|
|
28
|
+
--help Show this help message
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
async function main(): Promise<void> {
|
|
32
|
+
switch (command) {
|
|
33
|
+
case "init": {
|
|
34
|
+
const name = args[1] || ".";
|
|
35
|
+
await initProject(name);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "serve": {
|
|
39
|
+
const portIndex = args.indexOf("--port");
|
|
40
|
+
const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 7148;
|
|
41
|
+
await serveProject({ port });
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case "migrate": {
|
|
45
|
+
await runMigrations(args[1]);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "migrate:create": {
|
|
49
|
+
const description = args.slice(1).join(" ");
|
|
50
|
+
await createMigration(description || undefined);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "routes": {
|
|
54
|
+
await listRoutes();
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case "test": {
|
|
58
|
+
await runTests(args[1]);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "ai": {
|
|
62
|
+
const { detectAi, installAiContext, installAllAiContext, aiStatusReport } = await import("../../core/src/ai.js");
|
|
63
|
+
const root = args[1] || ".";
|
|
64
|
+
const installAll = args.includes("--all");
|
|
65
|
+
const force = args.includes("--force");
|
|
66
|
+
|
|
67
|
+
// Show status
|
|
68
|
+
console.log(aiStatusReport(root));
|
|
69
|
+
|
|
70
|
+
// Install context
|
|
71
|
+
if (installAll) {
|
|
72
|
+
const created = installAllAiContext(root, force);
|
|
73
|
+
if (created.length > 0) {
|
|
74
|
+
console.log("Installed AI context files:");
|
|
75
|
+
for (const f of created) {
|
|
76
|
+
console.log(` + ${f}`);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
console.log("All AI context files already exist (use --force to overwrite).");
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
const created = installAiContext(root, { force });
|
|
83
|
+
if (created.length > 0) {
|
|
84
|
+
console.log("Installed AI context files:");
|
|
85
|
+
for (const f of created) {
|
|
86
|
+
console.log(` + ${f}`);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
console.log("No new AI context files needed.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case "help":
|
|
95
|
+
case "--help":
|
|
96
|
+
case "-h":
|
|
97
|
+
case undefined:
|
|
98
|
+
console.log(HELP);
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
console.error(`Unknown command: ${command}`);
|
|
102
|
+
console.log(HELP);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
main().catch((err) => {
|
|
108
|
+
console.error(err);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve, basename } from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export async function initProject(name: string): Promise<void> {
|
|
6
|
+
const targetDir = name === "." ? process.cwd() : resolve(name);
|
|
7
|
+
const projectName = name === "." ? basename(process.cwd()) : basename(name);
|
|
8
|
+
|
|
9
|
+
if (name !== "." && existsSync(targetDir)) {
|
|
10
|
+
console.error(`Error: Directory "${targetDir}" already exists.`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log(`\n Creating Tina4 project: ${projectName}\n`);
|
|
15
|
+
|
|
16
|
+
// Create directory structure
|
|
17
|
+
const dirs = [
|
|
18
|
+
"",
|
|
19
|
+
"src",
|
|
20
|
+
"src/routes",
|
|
21
|
+
"src/routes/api",
|
|
22
|
+
"src/routes/api/hello",
|
|
23
|
+
"src/models",
|
|
24
|
+
"src/templates",
|
|
25
|
+
"public",
|
|
26
|
+
"data",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const dir of dirs) {
|
|
30
|
+
mkdirSync(join(targetDir, dir), { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// package.json
|
|
34
|
+
writeFileSync(
|
|
35
|
+
join(targetDir, "package.json"),
|
|
36
|
+
JSON.stringify(
|
|
37
|
+
{
|
|
38
|
+
name: projectName,
|
|
39
|
+
version: "0.0.1",
|
|
40
|
+
private: true,
|
|
41
|
+
type: "module",
|
|
42
|
+
scripts: {
|
|
43
|
+
dev: "tina4 serve",
|
|
44
|
+
serve: "tina4 serve",
|
|
45
|
+
},
|
|
46
|
+
dependencies: {
|
|
47
|
+
"tina4-nodejs": "^0.0.1",
|
|
48
|
+
},
|
|
49
|
+
devDependencies: {
|
|
50
|
+
typescript: "^5.7.0",
|
|
51
|
+
tsx: "^4.19.0",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
null,
|
|
55
|
+
2
|
|
56
|
+
) + "\n"
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// tsconfig.json
|
|
60
|
+
writeFileSync(
|
|
61
|
+
join(targetDir, "tsconfig.json"),
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
{
|
|
64
|
+
compilerOptions: {
|
|
65
|
+
target: "ES2022",
|
|
66
|
+
module: "Node16",
|
|
67
|
+
moduleResolution: "Node16",
|
|
68
|
+
strict: true,
|
|
69
|
+
esModuleInterop: true,
|
|
70
|
+
skipLibCheck: true,
|
|
71
|
+
outDir: "./dist",
|
|
72
|
+
rootDir: "./src",
|
|
73
|
+
},
|
|
74
|
+
include: ["src"],
|
|
75
|
+
},
|
|
76
|
+
null,
|
|
77
|
+
2
|
|
78
|
+
) + "\n"
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// .gitignore
|
|
82
|
+
writeFileSync(
|
|
83
|
+
join(targetDir, ".gitignore"),
|
|
84
|
+
`node_modules/
|
|
85
|
+
dist/
|
|
86
|
+
*.db
|
|
87
|
+
*.sqlite
|
|
88
|
+
.env
|
|
89
|
+
.DS_Store
|
|
90
|
+
data/
|
|
91
|
+
`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Sample route: GET /api/hello
|
|
95
|
+
writeFileSync(
|
|
96
|
+
join(targetDir, "src/routes/api/hello/get.ts"),
|
|
97
|
+
`import type { Tina4Request, Tina4Response } from "tina4-nodejs";
|
|
98
|
+
|
|
99
|
+
export const meta = {
|
|
100
|
+
summary: "Hello World",
|
|
101
|
+
tags: ["Example"],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default async function (req: Tina4Request, res: Tina4Response): Promise<void> {
|
|
105
|
+
res.json({ message: "Hello from Tina4!", timestamp: new Date().toISOString() });
|
|
106
|
+
}
|
|
107
|
+
`
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Sample model: Example
|
|
111
|
+
writeFileSync(
|
|
112
|
+
join(targetDir, "src/models/Example.ts"),
|
|
113
|
+
`export default class Example {
|
|
114
|
+
static tableName = "examples";
|
|
115
|
+
|
|
116
|
+
static fields = {
|
|
117
|
+
id: { type: "integer" as const, primaryKey: true, autoIncrement: true },
|
|
118
|
+
name: { type: "string" as const, required: true, maxLength: 255 },
|
|
119
|
+
description: { type: "text" as const },
|
|
120
|
+
active: { type: "boolean" as const, default: true },
|
|
121
|
+
createdAt: { type: "datetime" as const, default: "now" },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
`
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Sample template
|
|
128
|
+
writeFileSync(
|
|
129
|
+
join(targetDir, "src/templates/welcome.html.twig"),
|
|
130
|
+
`<!DOCTYPE html>
|
|
131
|
+
<html>
|
|
132
|
+
<head>
|
|
133
|
+
<title>{{ title }}</title>
|
|
134
|
+
</head>
|
|
135
|
+
<body>
|
|
136
|
+
<h1>Welcome to {{ name }}</h1>
|
|
137
|
+
<p>This is not a framework.</p>
|
|
138
|
+
</body>
|
|
139
|
+
</html>
|
|
140
|
+
`
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Static index page
|
|
144
|
+
writeFileSync(
|
|
145
|
+
join(targetDir, "public/index.html"),
|
|
146
|
+
`<!DOCTYPE html>
|
|
147
|
+
<html>
|
|
148
|
+
<head>
|
|
149
|
+
<title>Tina4</title>
|
|
150
|
+
<style>
|
|
151
|
+
body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }
|
|
152
|
+
h1 { font-size: 2.5em; margin-bottom: 0.2em; }
|
|
153
|
+
p { color: #666; font-size: 1.1em; }
|
|
154
|
+
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
|
|
155
|
+
a { color: #2563eb; }
|
|
156
|
+
</style>
|
|
157
|
+
</head>
|
|
158
|
+
<body>
|
|
159
|
+
<h1>tina4</h1>
|
|
160
|
+
<p><em>This is not a framework.</em></p>
|
|
161
|
+
<p>Your API is running. Try:</p>
|
|
162
|
+
<ul>
|
|
163
|
+
<li><a href="/api/hello">GET /api/hello</a></li>
|
|
164
|
+
<li><a href="/swagger">API Documentation</a></li>
|
|
165
|
+
</ul>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
`
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
console.log(" Installing dependencies...\n");
|
|
172
|
+
|
|
173
|
+
let npmOk = true;
|
|
174
|
+
try {
|
|
175
|
+
execSync("npm install", { cwd: targetDir, stdio: "inherit" });
|
|
176
|
+
} catch {
|
|
177
|
+
npmOk = false;
|
|
178
|
+
console.log("\n Note: npm install failed — the package may not be published yet.");
|
|
179
|
+
console.log(" Your project files have been created. You can install dependencies later.\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const absPath = resolve(targetDir);
|
|
183
|
+
const cdStep = name === "." ? "" : ` cd ${absPath}\n`;
|
|
184
|
+
console.log(`
|
|
185
|
+
Done! Your Tina4 project is ready.
|
|
186
|
+
|
|
187
|
+
Next steps:
|
|
188
|
+
${cdStep} npm install
|
|
189
|
+
npx tina4nodejs serve
|
|
190
|
+
|
|
191
|
+
Your API will be running at http://localhost:7148
|
|
192
|
+
Swagger docs at http://localhost:7148/swagger
|
|
193
|
+
`);
|
|
194
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: migrate — Run pending SQL migration files.
|
|
3
|
+
*
|
|
4
|
+
* Scans the migrations/ directory for .sql files, executes them in order,
|
|
5
|
+
* and records each as applied via the ORM migration tracker.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
export async function runMigrations(migrationDir?: string): Promise<void> {
|
|
11
|
+
const dir = resolve(migrationDir ?? "migrations");
|
|
12
|
+
|
|
13
|
+
if (!existsSync(dir)) {
|
|
14
|
+
console.log(" No migrations/ directory found. Nothing to run.");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Initialise the database so the adapter is available
|
|
19
|
+
let initDatabase: typeof import("@tina4/orm").initDatabase;
|
|
20
|
+
let ensureMigrationTable: typeof import("@tina4/orm").ensureMigrationTable;
|
|
21
|
+
let isMigrationApplied: typeof import("@tina4/orm").isMigrationApplied;
|
|
22
|
+
let recordMigration: typeof import("@tina4/orm").recordMigration;
|
|
23
|
+
let getNextBatch: typeof import("@tina4/orm").getNextBatch;
|
|
24
|
+
let getAdapter: typeof import("@tina4/orm").getAdapter;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const orm = await import("@tina4/orm");
|
|
28
|
+
initDatabase = orm.initDatabase;
|
|
29
|
+
ensureMigrationTable = orm.ensureMigrationTable;
|
|
30
|
+
isMigrationApplied = orm.isMigrationApplied;
|
|
31
|
+
recordMigration = orm.recordMigration;
|
|
32
|
+
getNextBatch = orm.getNextBatch;
|
|
33
|
+
getAdapter = orm.getAdapter;
|
|
34
|
+
} catch {
|
|
35
|
+
console.error(" Error: @tina4/orm is required to run migrations.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Ensure database is initialised (uses DATABASE_URL or defaults to sqlite)
|
|
40
|
+
try {
|
|
41
|
+
initDatabase();
|
|
42
|
+
} catch {
|
|
43
|
+
// Adapter may already be set — ignore
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
ensureMigrationTable();
|
|
47
|
+
|
|
48
|
+
// Collect .sql files sorted alphabetically
|
|
49
|
+
const files = readdirSync(dir)
|
|
50
|
+
.filter((f) => f.endsWith(".sql"))
|
|
51
|
+
.sort();
|
|
52
|
+
|
|
53
|
+
if (files.length === 0) {
|
|
54
|
+
console.log(" No .sql migration files found.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const batch = getNextBatch();
|
|
59
|
+
let applied = 0;
|
|
60
|
+
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const name = file.replace(/\.sql$/, "");
|
|
63
|
+
|
|
64
|
+
if (isMigrationApplied(name)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const sql = readFileSync(join(dir, file), "utf-8").trim();
|
|
69
|
+
if (!sql) continue;
|
|
70
|
+
|
|
71
|
+
console.log(` Migrating: ${file}`);
|
|
72
|
+
|
|
73
|
+
const adapter = getAdapter();
|
|
74
|
+
// Split on semicolons and execute each statement
|
|
75
|
+
const statements = sql.split(";").map((s) => s.trim()).filter(Boolean);
|
|
76
|
+
|
|
77
|
+
for (const stmt of statements) {
|
|
78
|
+
try {
|
|
79
|
+
adapter.execute(stmt);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
82
|
+
console.error(` Error in ${file}: ${msg}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
recordMigration(name, batch);
|
|
88
|
+
applied++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (applied === 0) {
|
|
92
|
+
console.log(" Nothing to migrate — all migrations already applied.");
|
|
93
|
+
} else {
|
|
94
|
+
console.log(` Applied ${applied} migration(s) (batch ${batch}).`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: migrate:create — Create a new SQL migration file.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* tina4 migrate:create "create users table"
|
|
6
|
+
* tina4 migrate:create add_email_to_users
|
|
7
|
+
*
|
|
8
|
+
* Creates migrations/000001_description.sql (next sequence number).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
export async function createMigration(description?: string): Promise<void> {
|
|
14
|
+
if (!description) {
|
|
15
|
+
console.error(" Usage: tina4 migrate:create <description>");
|
|
16
|
+
console.error(' Example: tina4 migrate:create "create users table"');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const dir = resolve("migrations");
|
|
21
|
+
|
|
22
|
+
// Ensure migrations/ exists
|
|
23
|
+
if (!existsSync(dir)) {
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Determine the next sequence number
|
|
28
|
+
const existing = existsSync(dir)
|
|
29
|
+
? readdirSync(dir).filter((f) => f.endsWith(".sql")).sort()
|
|
30
|
+
: [];
|
|
31
|
+
|
|
32
|
+
let nextSeq = 1;
|
|
33
|
+
if (existing.length > 0) {
|
|
34
|
+
const last = existing[existing.length - 1];
|
|
35
|
+
const match = last.match(/^(\d+)/);
|
|
36
|
+
if (match) {
|
|
37
|
+
nextSeq = parseInt(match[1], 10) + 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Sanitise description for filename
|
|
42
|
+
const safeName = description
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
45
|
+
.replace(/^_|_$/g, "");
|
|
46
|
+
|
|
47
|
+
const seqStr = String(nextSeq).padStart(6, "0");
|
|
48
|
+
const fileName = `${seqStr}_${safeName}.sql`;
|
|
49
|
+
const filePath = join(dir, fileName);
|
|
50
|
+
|
|
51
|
+
const template = `-- Migration: ${description}
|
|
52
|
+
-- Created: ${new Date().toISOString()}
|
|
53
|
+
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
writeFileSync(filePath, template, "utf-8");
|
|
57
|
+
console.log(` Created migration: ${fileName}`);
|
|
58
|
+
console.log(` Path: ${filePath}`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: routes — List all registered routes.
|
|
3
|
+
*
|
|
4
|
+
* Discovers routes from src/routes/ and displays them in a table.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
export async function listRoutes(): Promise<void> {
|
|
10
|
+
const routesDir = resolve("src/routes");
|
|
11
|
+
|
|
12
|
+
if (!existsSync(routesDir)) {
|
|
13
|
+
console.error(" No src/routes/ directory found. Are you in a Tina4 project?");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let discoverRoutes: typeof import("@tina4/core").discoverRoutes;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const core = await import("@tina4/core");
|
|
21
|
+
discoverRoutes = core.discoverRoutes;
|
|
22
|
+
} catch {
|
|
23
|
+
console.error(" Error: @tina4/core is required to list routes.");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const routes = await discoverRoutes(routesDir);
|
|
28
|
+
|
|
29
|
+
if (routes.length === 0) {
|
|
30
|
+
console.log(" No routes found.");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Sort by path then method
|
|
35
|
+
routes.sort((a: { path: string; method: string }, b: { path: string; method: string }) => {
|
|
36
|
+
const pathCmp = a.path.localeCompare(b.path);
|
|
37
|
+
return pathCmp !== 0 ? pathCmp : a.method.localeCompare(b.method);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
console.log("");
|
|
41
|
+
console.log(" Registered Routes:");
|
|
42
|
+
console.log(" " + "-".repeat(70));
|
|
43
|
+
console.log(
|
|
44
|
+
" " +
|
|
45
|
+
"METHOD".padEnd(10) +
|
|
46
|
+
"PATH".padEnd(40) +
|
|
47
|
+
"SUMMARY"
|
|
48
|
+
);
|
|
49
|
+
console.log(" " + "-".repeat(70));
|
|
50
|
+
|
|
51
|
+
for (const route of routes) {
|
|
52
|
+
const method = route.method.toUpperCase().padEnd(10);
|
|
53
|
+
const path = route.path.padEnd(40);
|
|
54
|
+
const summary = route.meta?.summary ?? "";
|
|
55
|
+
console.log(` ${method}${path}${summary}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(" " + "-".repeat(70));
|
|
59
|
+
console.log(` Total: ${routes.length} route(s)`);
|
|
60
|
+
console.log("");
|
|
61
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
export interface ServeOptions {
|
|
5
|
+
port?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function serveProject(options: ServeOptions): Promise<void> {
|
|
9
|
+
const port = options.port ?? 7148;
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
|
|
12
|
+
const routesDir = resolve(cwd, "src/routes");
|
|
13
|
+
const modelsDir = resolve(cwd, "src/models");
|
|
14
|
+
const templatesDir = resolve(cwd, "src/templates");
|
|
15
|
+
const staticDir = resolve(cwd, "public");
|
|
16
|
+
|
|
17
|
+
if (!existsSync(routesDir) && !existsSync(modelsDir)) {
|
|
18
|
+
console.error(" Error: Not a Tina4 project. Run this from a project created with 'tina4 init'.");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { startServer } = await import("@tina4/core");
|
|
23
|
+
const { watchForChanges } = await import("@tina4/core/src/watcher.js");
|
|
24
|
+
|
|
25
|
+
const server = await startServer({
|
|
26
|
+
port,
|
|
27
|
+
routesDir,
|
|
28
|
+
modelsDir,
|
|
29
|
+
templatesDir,
|
|
30
|
+
staticDir,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Watch for file changes and reload routes
|
|
34
|
+
const watcher = watchForChanges([routesDir, modelsDir, templatesDir], async () => {
|
|
35
|
+
try {
|
|
36
|
+
const { discoverRoutes } = await import("@tina4/core");
|
|
37
|
+
const routes = await discoverRoutes(routesDir);
|
|
38
|
+
server.router.clear();
|
|
39
|
+
for (const route of routes) {
|
|
40
|
+
server.router.addRoute(route);
|
|
41
|
+
}
|
|
42
|
+
console.log(` Reloaded ${routes.length} route(s)`);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(" Error reloading routes:", err);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Graceful shutdown
|
|
49
|
+
const shutdown = () => {
|
|
50
|
+
console.log("\n Shutting down...");
|
|
51
|
+
watcher.close();
|
|
52
|
+
server.close();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
process.on("SIGINT", shutdown);
|
|
57
|
+
process.on("SIGTERM", shutdown);
|
|
58
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: test — Run project tests.
|
|
3
|
+
*
|
|
4
|
+
* Looks for test files and executes them with tsx.
|
|
5
|
+
* Supports: test/integration.ts, test/*.ts, tests/*.ts, *.test.ts patterns.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
8
|
+
import { resolve, join } from "node:path";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
export async function runTests(testPath?: string): Promise<void> {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
|
|
14
|
+
// If a specific test file is provided, run it directly
|
|
15
|
+
if (testPath) {
|
|
16
|
+
const file = resolve(testPath);
|
|
17
|
+
if (!existsSync(file)) {
|
|
18
|
+
console.error(` Error: Test file not found: ${testPath}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
console.log(` Running: ${testPath}\n`);
|
|
22
|
+
try {
|
|
23
|
+
execSync(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
|
|
24
|
+
} catch {
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Auto-discover test files
|
|
31
|
+
const candidates = [
|
|
32
|
+
"test/integration.ts",
|
|
33
|
+
"test",
|
|
34
|
+
"tests",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
let testFiles: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const candidate of candidates) {
|
|
40
|
+
const fullPath = resolve(cwd, candidate);
|
|
41
|
+
if (!existsSync(fullPath)) continue;
|
|
42
|
+
|
|
43
|
+
// If it's a file, run it
|
|
44
|
+
if (candidate.endsWith(".ts")) {
|
|
45
|
+
testFiles.push(fullPath);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If it's a directory, collect all .ts and .test.ts files
|
|
50
|
+
try {
|
|
51
|
+
const files = readdirSync(fullPath)
|
|
52
|
+
.filter((f) => f.endsWith(".ts"))
|
|
53
|
+
.map((f) => join(fullPath, f));
|
|
54
|
+
testFiles.push(...files);
|
|
55
|
+
} catch {
|
|
56
|
+
// skip
|
|
57
|
+
}
|
|
58
|
+
if (testFiles.length > 0) break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (testFiles.length === 0) {
|
|
62
|
+
console.log(" No test files found.");
|
|
63
|
+
console.log(" Looked in: test/integration.ts, test/*.ts, tests/*.ts");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(` Found ${testFiles.length} test file(s)\n`);
|
|
68
|
+
|
|
69
|
+
let failed = false;
|
|
70
|
+
for (const file of testFiles) {
|
|
71
|
+
const relative = file.replace(cwd + "/", "");
|
|
72
|
+
console.log(` Running: ${relative}`);
|
|
73
|
+
try {
|
|
74
|
+
execSync(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
|
|
75
|
+
} catch {
|
|
76
|
+
failed = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (failed) {
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name": "Auth", "description": "JWT login form with token display", "try_url": "/gallery/auth"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Gallery: Auth — login endpoint, returns a JWT token. */
|
|
2
|
+
import type { Tina4Request, Tina4Response } from "@tina4/core";
|
|
3
|
+
|
|
4
|
+
export default async function (req: Tina4Request, res: Tina4Response) {
|
|
5
|
+
const body = (req.body as Record<string, unknown>) ?? {};
|
|
6
|
+
const username = (body.username as string) ?? "";
|
|
7
|
+
const password = (body.password as string) ?? "";
|
|
8
|
+
|
|
9
|
+
if (username && password) {
|
|
10
|
+
// In a real app: import { Auth } from "@tina4/core";
|
|
11
|
+
// const auth = new Auth();
|
|
12
|
+
// const token = auth.createToken({ username, role: "user" });
|
|
13
|
+
// For the gallery demo, generate a simple base64 token
|
|
14
|
+
const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
|
|
15
|
+
const payload = Buffer.from(JSON.stringify({ username, role: "user", iat: Math.floor(Date.now() / 1000) })).toString("base64url");
|
|
16
|
+
const signature = Buffer.from("gallery-demo-signature").toString("base64url");
|
|
17
|
+
const token = `${header}.${payload}.${signature}`;
|
|
18
|
+
return res.json({ token, message: `Welcome ${username}!` });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return res.json({ error: "Username and password required" }, 401);
|
|
22
|
+
}
|