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.
Files changed (119) hide show
  1. package/BENCHMARK_REPORT.md +96 -0
  2. package/CARBONAH.md +140 -0
  3. package/CLAUDE.md +599 -0
  4. package/COMPARISON.md +194 -0
  5. package/README.md +595 -0
  6. package/package.json +59 -0
  7. package/packages/cli/src/bin.ts +110 -0
  8. package/packages/cli/src/commands/init.ts +194 -0
  9. package/packages/cli/src/commands/migrate.ts +96 -0
  10. package/packages/cli/src/commands/migrateCreate.ts +59 -0
  11. package/packages/cli/src/commands/routes.ts +61 -0
  12. package/packages/cli/src/commands/serve.ts +58 -0
  13. package/packages/cli/src/commands/test.ts +83 -0
  14. package/packages/core/gallery/auth/meta.json +1 -0
  15. package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
  16. package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
  17. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
  18. package/packages/core/gallery/database/meta.json +1 -0
  19. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
  20. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
  21. package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
  22. package/packages/core/gallery/error-overlay/meta.json +1 -0
  23. package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
  24. package/packages/core/gallery/orm/meta.json +1 -0
  25. package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
  26. package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
  27. package/packages/core/gallery/queue/meta.json +1 -0
  28. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
  29. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
  30. package/packages/core/gallery/rest-api/meta.json +1 -0
  31. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
  32. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
  33. package/packages/core/gallery/templates/meta.json +1 -0
  34. package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
  35. package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
  36. package/packages/core/public/css/tina4.css +2463 -0
  37. package/packages/core/public/css/tina4.min.css +1 -0
  38. package/packages/core/public/favicon.ico +0 -0
  39. package/packages/core/public/images/logo.svg +5 -0
  40. package/packages/core/public/images/tina4-logo-icon.webp +0 -0
  41. package/packages/core/public/js/frond.min.js +420 -0
  42. package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
  43. package/packages/core/public/js/tina4.min.js +93 -0
  44. package/packages/core/public/swagger/index.html +90 -0
  45. package/packages/core/public/swagger/oauth2-redirect.html +63 -0
  46. package/packages/core/src/ai.ts +359 -0
  47. package/packages/core/src/api.ts +248 -0
  48. package/packages/core/src/auth.ts +287 -0
  49. package/packages/core/src/cache.ts +121 -0
  50. package/packages/core/src/constants.ts +48 -0
  51. package/packages/core/src/container.ts +90 -0
  52. package/packages/core/src/devAdmin.ts +2024 -0
  53. package/packages/core/src/devMailbox.ts +316 -0
  54. package/packages/core/src/dotenv.ts +172 -0
  55. package/packages/core/src/errorOverlay.test.ts +122 -0
  56. package/packages/core/src/errorOverlay.ts +278 -0
  57. package/packages/core/src/events.ts +112 -0
  58. package/packages/core/src/fakeData.ts +309 -0
  59. package/packages/core/src/graphql.ts +812 -0
  60. package/packages/core/src/health.ts +31 -0
  61. package/packages/core/src/htmlElement.ts +172 -0
  62. package/packages/core/src/i18n.ts +136 -0
  63. package/packages/core/src/index.ts +88 -0
  64. package/packages/core/src/logger.ts +226 -0
  65. package/packages/core/src/messenger.ts +822 -0
  66. package/packages/core/src/middleware.ts +138 -0
  67. package/packages/core/src/queue.ts +481 -0
  68. package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
  69. package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
  70. package/packages/core/src/rateLimiter.ts +107 -0
  71. package/packages/core/src/request.ts +189 -0
  72. package/packages/core/src/response.ts +146 -0
  73. package/packages/core/src/routeDiscovery.ts +87 -0
  74. package/packages/core/src/router.ts +398 -0
  75. package/packages/core/src/scss.ts +366 -0
  76. package/packages/core/src/server.ts +610 -0
  77. package/packages/core/src/service.ts +380 -0
  78. package/packages/core/src/session.ts +480 -0
  79. package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
  80. package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
  81. package/packages/core/src/static.ts +58 -0
  82. package/packages/core/src/testing.ts +233 -0
  83. package/packages/core/src/types.ts +98 -0
  84. package/packages/core/src/watcher.ts +37 -0
  85. package/packages/core/src/websocket.ts +408 -0
  86. package/packages/core/src/wsdl.ts +546 -0
  87. package/packages/core/templates/errors/302.twig +14 -0
  88. package/packages/core/templates/errors/401.twig +9 -0
  89. package/packages/core/templates/errors/403.twig +29 -0
  90. package/packages/core/templates/errors/404.twig +29 -0
  91. package/packages/core/templates/errors/500.twig +38 -0
  92. package/packages/core/templates/errors/502.twig +9 -0
  93. package/packages/core/templates/errors/503.twig +12 -0
  94. package/packages/core/templates/errors/base.twig +37 -0
  95. package/packages/frond/src/engine.ts +1475 -0
  96. package/packages/frond/src/index.ts +2 -0
  97. package/packages/orm/src/adapters/firebird.ts +455 -0
  98. package/packages/orm/src/adapters/mssql.ts +440 -0
  99. package/packages/orm/src/adapters/mysql.ts +355 -0
  100. package/packages/orm/src/adapters/postgres.ts +362 -0
  101. package/packages/orm/src/adapters/sqlite.ts +270 -0
  102. package/packages/orm/src/autoCrud.ts +231 -0
  103. package/packages/orm/src/baseModel.ts +536 -0
  104. package/packages/orm/src/database.ts +321 -0
  105. package/packages/orm/src/fakeData.ts +118 -0
  106. package/packages/orm/src/index.ts +49 -0
  107. package/packages/orm/src/migration.ts +392 -0
  108. package/packages/orm/src/model.ts +56 -0
  109. package/packages/orm/src/query.ts +113 -0
  110. package/packages/orm/src/seeder.ts +120 -0
  111. package/packages/orm/src/sqlTranslation.ts +272 -0
  112. package/packages/orm/src/types.ts +110 -0
  113. package/packages/orm/src/validation.ts +93 -0
  114. package/packages/swagger/src/generator.ts +189 -0
  115. package/packages/swagger/src/index.ts +2 -0
  116. package/packages/swagger/src/ui.ts +48 -0
  117. package/skills/tina4-developer.skill +0 -0
  118. package/skills/tina4-js.skill +0 -0
  119. package/skills/tina4-maintainer.skill +0 -0
@@ -0,0 +1,398 @@
1
+ import type { RouteHandler, RouteDefinition, RouteMeta, Middleware, Tina4Request, Tina4Response } from "./types.js";
2
+
3
+ interface MatchResult {
4
+ handler: RouteHandler;
5
+ params: Record<string, string>;
6
+ pattern: string;
7
+ meta?: RouteMeta;
8
+ middlewares?: Middleware[];
9
+ template?: string;
10
+ }
11
+
12
+ interface CompiledRoute {
13
+ pattern: string;
14
+ regex: RegExp;
15
+ paramNames: string[];
16
+ handler: RouteHandler;
17
+ meta?: RouteMeta;
18
+ filePath?: string;
19
+ middlewares?: Middleware[];
20
+ cached?: boolean;
21
+ cacheStore?: Map<string, { data: unknown; expires: number }>;
22
+ cacheTtl?: number;
23
+ template?: string;
24
+ }
25
+
26
+ export interface RouteInfo {
27
+ method: string;
28
+ path: string;
29
+ handler: string;
30
+ middlewareCount: number;
31
+ cached: boolean;
32
+ }
33
+
34
+ export class Router {
35
+ private routes: Map<string, CompiledRoute[]> = new Map();
36
+
37
+ /**
38
+ * Add a raw route definition (used internally and by file-based routing).
39
+ */
40
+ addRoute(definition: RouteDefinition): void {
41
+ const method = definition.method.toUpperCase();
42
+ const { regex, paramNames } = this.compilePattern(definition.pattern);
43
+
44
+ if (!this.routes.has(method)) {
45
+ this.routes.set(method, []);
46
+ }
47
+
48
+ const routes = this.routes.get(method)!;
49
+
50
+ // Remove existing route with same pattern (for hot-reload)
51
+ const existingIndex = routes.findIndex((r) => r.pattern === definition.pattern);
52
+ if (existingIndex !== -1) {
53
+ routes.splice(existingIndex, 1);
54
+ }
55
+
56
+ routes.push({
57
+ pattern: definition.pattern,
58
+ regex,
59
+ paramNames,
60
+ handler: definition.handler,
61
+ meta: definition.meta,
62
+ filePath: definition.filePath,
63
+ middlewares: definition.middlewares,
64
+ template: definition.template,
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Register a GET route programmatically.
70
+ */
71
+ get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
72
+ this.addRoute({ method: "GET", pattern: path, handler, middlewares, meta });
73
+ }
74
+
75
+ /**
76
+ * Register a POST route programmatically.
77
+ */
78
+ post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
79
+ this.addRoute({ method: "POST", pattern: path, handler, middlewares, meta });
80
+ }
81
+
82
+ /**
83
+ * Register a PUT route programmatically.
84
+ */
85
+ put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
86
+ this.addRoute({ method: "PUT", pattern: path, handler, middlewares, meta });
87
+ }
88
+
89
+ /**
90
+ * Register a PATCH route programmatically.
91
+ */
92
+ patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
93
+ this.addRoute({ method: "PATCH", pattern: path, handler, middlewares, meta });
94
+ }
95
+
96
+ /**
97
+ * Register a DELETE route programmatically.
98
+ */
99
+ delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
100
+ this.addRoute({ method: "DELETE", pattern: path, handler, middlewares, meta });
101
+ }
102
+
103
+ /**
104
+ * Register a route that matches ANY HTTP method.
105
+ */
106
+ any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
107
+ for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) {
108
+ this.addRoute({ method, pattern: path, handler, middlewares, meta });
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Create a route group with a shared prefix and optional middlewares.
114
+ */
115
+ group(prefix: string, callback: (group: RouteGroup) => void, middlewares?: Middleware[]): void {
116
+ const group = new RouteGroup(this, prefix, middlewares);
117
+ callback(group);
118
+ }
119
+
120
+ /**
121
+ * Match a request method + pathname to a registered route.
122
+ */
123
+ match(method: string, pathname: string): MatchResult | null {
124
+ const upperMethod = method.toUpperCase();
125
+
126
+ // Try exact method first, then ANY routes are already registered under each method
127
+ const routes = this.routes.get(upperMethod);
128
+ if (!routes) return null;
129
+
130
+ for (const route of routes) {
131
+ const match = route.regex.exec(pathname);
132
+ if (match) {
133
+ const params: Record<string, string> = {};
134
+ for (let i = 0; i < route.paramNames.length; i++) {
135
+ params[route.paramNames[i]] = decodeURIComponent(match[i + 1]);
136
+ }
137
+ return {
138
+ handler: route.handler,
139
+ params,
140
+ pattern: route.pattern,
141
+ meta: route.meta,
142
+ middlewares: route.middlewares,
143
+ template: route.template,
144
+ };
145
+ }
146
+ }
147
+
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * Get all registered route definitions.
153
+ */
154
+ getRoutes(): RouteDefinition[] {
155
+ const all: RouteDefinition[] = [];
156
+ for (const [method, routes] of this.routes) {
157
+ for (const route of routes) {
158
+ all.push({
159
+ method,
160
+ pattern: route.pattern,
161
+ handler: route.handler,
162
+ meta: route.meta,
163
+ filePath: route.filePath,
164
+ middlewares: route.middlewares,
165
+ template: route.template,
166
+ });
167
+ }
168
+ }
169
+ return all;
170
+ }
171
+
172
+ /**
173
+ * List all routes in a debug-friendly format for CLI output.
174
+ */
175
+ listRoutes(): RouteInfo[] {
176
+ const info: RouteInfo[] = [];
177
+ for (const [method, routes] of this.routes) {
178
+ for (const route of routes) {
179
+ info.push({
180
+ method,
181
+ path: route.pattern,
182
+ handler: route.filePath ?? (route.handler.name || "(anonymous)"),
183
+ middlewareCount: route.middlewares?.length ?? 0,
184
+ cached: route.cached ?? false,
185
+ });
186
+ }
187
+ }
188
+ return info;
189
+ }
190
+
191
+ clear(): void {
192
+ this.routes.clear();
193
+ }
194
+
195
+ private compilePattern(pattern: string): { regex: RegExp; paramNames: string[] } {
196
+ const paramNames: string[] = [];
197
+
198
+ // Supports {id} (primary, matches Python), [id] (file-based dirs), and :id (Express-style)
199
+ const regexStr = pattern
200
+ .split("/")
201
+ .map((segment) => {
202
+ // Catch-all: {...slug} or [...slug]
203
+ if (
204
+ (segment.startsWith("{...") && segment.endsWith("}")) ||
205
+ (segment.startsWith("[...") && segment.endsWith("]"))
206
+ ) {
207
+ const name = segment.startsWith("{") ? segment.slice(4, -1) : segment.slice(4, -1);
208
+ paramNames.push(name);
209
+ return "(.+)";
210
+ }
211
+ // Dynamic param: {id} (primary syntax, matching Python)
212
+ if (segment.startsWith("{") && segment.endsWith("}")) {
213
+ const name = segment.slice(1, -1);
214
+ paramNames.push(name);
215
+ return "([^/]+)";
216
+ }
217
+ // Dynamic param: [id] (file-based routing internal syntax)
218
+ if (segment.startsWith("[") && segment.endsWith("]")) {
219
+ const name = segment.slice(1, -1);
220
+ paramNames.push(name);
221
+ return "([^/]+)";
222
+ }
223
+ // Express-style param: :id
224
+ if (segment.startsWith(":")) {
225
+ const name = segment.slice(1);
226
+ paramNames.push(name);
227
+ return "([^/]+)";
228
+ }
229
+ return segment;
230
+ })
231
+ .join("/");
232
+
233
+ return {
234
+ regex: new RegExp(`^${regexStr}$`),
235
+ paramNames,
236
+ };
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Route group for grouping routes under a shared prefix with optional middlewares.
242
+ */
243
+ export class RouteGroup {
244
+ constructor(
245
+ private router: Router,
246
+ private prefix: string,
247
+ private groupMiddlewares?: Middleware[],
248
+ ) {}
249
+
250
+ private mergeMiddlewares(routeMiddlewares?: Middleware[]): Middleware[] | undefined {
251
+ const group = this.groupMiddlewares ?? [];
252
+ const route = routeMiddlewares ?? [];
253
+ const merged = [...group, ...route];
254
+ return merged.length > 0 ? merged : undefined;
255
+ }
256
+
257
+ get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
258
+ this.router.addRoute({
259
+ method: "GET",
260
+ pattern: this.prefix + path,
261
+ handler,
262
+ middlewares: this.mergeMiddlewares(middlewares),
263
+ meta,
264
+ });
265
+ }
266
+
267
+ post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
268
+ this.router.addRoute({
269
+ method: "POST",
270
+ pattern: this.prefix + path,
271
+ handler,
272
+ middlewares: this.mergeMiddlewares(middlewares),
273
+ meta,
274
+ });
275
+ }
276
+
277
+ put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
278
+ this.router.addRoute({
279
+ method: "PUT",
280
+ pattern: this.prefix + path,
281
+ handler,
282
+ middlewares: this.mergeMiddlewares(middlewares),
283
+ meta,
284
+ });
285
+ }
286
+
287
+ patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
288
+ this.router.addRoute({
289
+ method: "PATCH",
290
+ pattern: this.prefix + path,
291
+ handler,
292
+ middlewares: this.mergeMiddlewares(middlewares),
293
+ meta,
294
+ });
295
+ }
296
+
297
+ delete(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
298
+ this.router.addRoute({
299
+ method: "DELETE",
300
+ pattern: this.prefix + path,
301
+ handler,
302
+ middlewares: this.mergeMiddlewares(middlewares),
303
+ meta,
304
+ });
305
+ }
306
+
307
+ any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
308
+ for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) {
309
+ this.router.addRoute({
310
+ method,
311
+ pattern: this.prefix + path,
312
+ handler,
313
+ middlewares: this.mergeMiddlewares(middlewares),
314
+ meta,
315
+ });
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Nested groups.
321
+ */
322
+ group(prefix: string, callback: (group: RouteGroup) => void, middlewares?: Middleware[]): void {
323
+ const nestedGroup = new RouteGroup(
324
+ this.router,
325
+ this.prefix + prefix,
326
+ this.mergeMiddlewares(middlewares),
327
+ );
328
+ callback(nestedGroup);
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Run per-route middleware chain, then call the handler.
334
+ */
335
+ export async function runRouteMiddlewares(
336
+ middlewares: Middleware[],
337
+ req: Tina4Request,
338
+ res: Tina4Response,
339
+ ): Promise<boolean> {
340
+ for (const mw of middlewares) {
341
+ let nextCalled = false;
342
+ await mw(req, res, () => {
343
+ nextCalled = true;
344
+ });
345
+ if (res.raw.writableEnded) return false;
346
+ if (!nextCalled) return false;
347
+ }
348
+ return true;
349
+ }
350
+
351
+ /**
352
+ * Default global router instance.
353
+ * Top-level get(), post(), etc. register routes here.
354
+ * The server merges these routes on startup.
355
+ */
356
+ export const defaultRouter = new Router();
357
+
358
+ /**
359
+ * Top-level route registration functions — mirrors Python's decorator pattern.
360
+ *
361
+ * Usage:
362
+ * import { get, post } from "@tina4/core";
363
+ *
364
+ * get("/hello", async (req, res) => {
365
+ * res.json({ message: "Hello" });
366
+ * });
367
+ *
368
+ * post("/users/{id}", async (req, res) => {
369
+ * res.json({ id: req.params.id }, 201);
370
+ * });
371
+ */
372
+ export function get(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
373
+ defaultRouter.get(path, handler, middlewares, meta);
374
+ }
375
+
376
+ export function post(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
377
+ defaultRouter.post(path, handler, middlewares, meta);
378
+ }
379
+
380
+ export function put(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
381
+ defaultRouter.put(path, handler, middlewares, meta);
382
+ }
383
+
384
+ export function patch(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
385
+ defaultRouter.patch(path, handler, middlewares, meta);
386
+ }
387
+
388
+ // Named "del" to avoid conflict with the "delete" keyword; also exported as "delete" alias below.
389
+ export function del(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
390
+ defaultRouter.delete(path, handler, middlewares, meta);
391
+ }
392
+
393
+ export function any(path: string, handler: RouteHandler, middlewares?: Middleware[], meta?: RouteMeta): void {
394
+ defaultRouter.any(path, handler, middlewares, meta);
395
+ }
396
+
397
+ // Re-export "del" as "delete" for developer convenience (use: import { delete as del } from "@tina4/core")
398
+ export { del as delete };