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,380 @@
1
+ /**
2
+ * Service Runner — in-process background services using Node.js timers.
3
+ * Zero dependencies. Supports cron timing, simple intervals, and daemon mode.
4
+ */
5
+ import { readdirSync, statSync, watchFile, unwatchFile } from "node:fs";
6
+ import { join, extname } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+
9
+ // ─── Types ───────────────────────────────────────────────────────────────────
10
+
11
+ export interface ServiceOptions {
12
+ timing?: string; // cron: "*/5 * * * *"
13
+ daemon?: boolean; // continuous mode
14
+ interval?: number; // simple interval in seconds (alternative to cron)
15
+ maxRetries?: number; // restart on crash, default 3
16
+ }
17
+
18
+ export interface ServiceContext {
19
+ running: boolean;
20
+ lastRun: Date | null;
21
+ name: string;
22
+ }
23
+
24
+ export type ServiceHandler = (context: ServiceContext) => Promise<void> | void;
25
+
26
+ export interface ServiceInfo {
27
+ name: string;
28
+ options: ServiceOptions;
29
+ running: boolean;
30
+ lastRun: Date | null;
31
+ retries: number;
32
+ }
33
+
34
+ // ─── Internal state ──────────────────────────────────────────────────────────
35
+
36
+ interface RegisteredService {
37
+ name: string;
38
+ handler: ServiceHandler;
39
+ options: ServiceOptions;
40
+ context: ServiceContext;
41
+ timerId: ReturnType<typeof setInterval> | null;
42
+ retries: number;
43
+ }
44
+
45
+ const registry = new Map<string, RegisteredService>();
46
+ const watchedFiles = new Set<string>();
47
+
48
+ // ─── Cron parser ─────────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Parse a single cron field and check if the given value matches.
52
+ * Supports: * (every), N/n (step), N,N,N (list), N-N (range), plain number.
53
+ */
54
+ export function matchCronField(field: string, value: number): boolean {
55
+ // wildcard
56
+ if (field === "*") return true;
57
+
58
+ // step: */5 or N/5
59
+ if (field.includes("/")) {
60
+ const [base, stepStr] = field.split("/");
61
+ const step = parseInt(stepStr, 10);
62
+ if (isNaN(step) || step <= 0) return false;
63
+ if (base === "*") return value % step === 0;
64
+ const start = parseInt(base, 10);
65
+ if (isNaN(start)) return false;
66
+ return value >= start && (value - start) % step === 0;
67
+ }
68
+
69
+ // list: 1,15,30
70
+ if (field.includes(",")) {
71
+ return field.split(",").some((part) => matchCronField(part.trim(), value));
72
+ }
73
+
74
+ // range: 1-5
75
+ if (field.includes("-")) {
76
+ const [loStr, hiStr] = field.split("-");
77
+ const lo = parseInt(loStr, 10);
78
+ const hi = parseInt(hiStr, 10);
79
+ if (isNaN(lo) || isNaN(hi)) return false;
80
+ return value >= lo && value <= hi;
81
+ }
82
+
83
+ // exact number
84
+ return parseInt(field, 10) === value;
85
+ }
86
+
87
+ /**
88
+ * Check whether a Date matches a 5-field cron expression.
89
+ * Fields: minute hour dayOfMonth month dayOfWeek
90
+ */
91
+ export function matchesCron(expression: string, date: Date): boolean {
92
+ const fields = expression.trim().split(/\s+/);
93
+ if (fields.length !== 5) return false;
94
+
95
+ const minute = date.getMinutes();
96
+ const hour = date.getHours();
97
+ const dayOfMonth = date.getDate();
98
+ const month = date.getMonth() + 1; // JS months are 0-based
99
+ const dayOfWeek = date.getDay(); // 0 = Sunday
100
+
101
+ return (
102
+ matchCronField(fields[0], minute) &&
103
+ matchCronField(fields[1], hour) &&
104
+ matchCronField(fields[2], dayOfMonth) &&
105
+ matchCronField(fields[3], month) &&
106
+ matchCronField(fields[4], dayOfWeek)
107
+ );
108
+ }
109
+
110
+ // ─── Execution helpers ───────────────────────────────────────────────────────
111
+
112
+ async function executeHandler(svc: RegisteredService): Promise<void> {
113
+ try {
114
+ await svc.handler(svc.context);
115
+ svc.context.lastRun = new Date();
116
+ svc.retries = 0; // reset retries on success
117
+ } catch (err) {
118
+ const maxRetries = svc.options.maxRetries ?? 3;
119
+ svc.retries++;
120
+ if (svc.retries >= maxRetries) {
121
+ svc.context.running = false;
122
+ if (svc.timerId) {
123
+ clearInterval(svc.timerId);
124
+ svc.timerId = null;
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ function startCronService(svc: RegisteredService): void {
131
+ const checkIntervalMs = parseInt(
132
+ process.env.TINA4_SERVICE_INTERVAL ?? "1000",
133
+ 10,
134
+ );
135
+ let lastMinuteRun = -1;
136
+
137
+ svc.timerId = setInterval(() => {
138
+ if (!svc.context.running) return;
139
+ const now = new Date();
140
+ const currentMinute = now.getMinutes();
141
+ // Avoid running the same minute twice
142
+ if (currentMinute === lastMinuteRun) return;
143
+ if (matchesCron(svc.options.timing!, now)) {
144
+ lastMinuteRun = currentMinute;
145
+ executeHandler(svc);
146
+ }
147
+ }, checkIntervalMs);
148
+ }
149
+
150
+ function startIntervalService(svc: RegisteredService): void {
151
+ const intervalMs = (svc.options.interval ?? 60) * 1000;
152
+ svc.timerId = setInterval(() => {
153
+ if (!svc.context.running) return;
154
+ executeHandler(svc);
155
+ }, intervalMs);
156
+ }
157
+
158
+ function startDaemonService(svc: RegisteredService): void {
159
+ // Daemon runs once; the handler manages its own loop using context.running
160
+ executeHandler(svc);
161
+ }
162
+
163
+ // ─── ServiceRunner ───────────────────────────────────────────────────────────
164
+
165
+ export class ServiceRunner {
166
+ /**
167
+ * Register a service with a handler and options.
168
+ */
169
+ static register(
170
+ name: string,
171
+ handler: ServiceHandler,
172
+ options: ServiceOptions = {},
173
+ ): void {
174
+ const context: ServiceContext = {
175
+ running: false,
176
+ lastRun: null,
177
+ name,
178
+ };
179
+ registry.set(name, {
180
+ name,
181
+ handler,
182
+ options,
183
+ context,
184
+ timerId: null,
185
+ retries: 0,
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Discover services from a directory. Each file should export
191
+ * { name, handler, timing?, interval?, daemon?, maxRetries? }.
192
+ */
193
+ static async discover(serviceDir?: string): Promise<ServiceInfo[]> {
194
+ const dir =
195
+ serviceDir ?? process.env.TINA4_SERVICE_DIR ?? "src/services";
196
+ const discovered: ServiceInfo[] = [];
197
+
198
+ let entries: string[];
199
+ try {
200
+ entries = readdirSync(dir);
201
+ } catch {
202
+ return discovered;
203
+ }
204
+
205
+ for (const entry of entries) {
206
+ const ext = extname(entry);
207
+ if (ext !== ".ts" && ext !== ".js") continue;
208
+
209
+ const fullPath = join(dir, entry);
210
+ const stat = statSync(fullPath);
211
+ if (!stat.isFile()) continue;
212
+
213
+ try {
214
+ const fileUrl = pathToFileURL(fullPath).href;
215
+ const mod = await import(fileUrl);
216
+ const exp = mod.default ?? mod;
217
+
218
+ if (exp.name && typeof exp.handler === "function") {
219
+ const opts: ServiceOptions = {
220
+ timing: exp.timing,
221
+ daemon: exp.daemon,
222
+ interval: exp.interval,
223
+ maxRetries: exp.maxRetries,
224
+ };
225
+ ServiceRunner.register(exp.name, exp.handler, opts);
226
+ discovered.push({
227
+ name: exp.name,
228
+ options: opts,
229
+ running: false,
230
+ lastRun: null,
231
+ retries: 0,
232
+ });
233
+ }
234
+ } catch {
235
+ // skip files that fail to import
236
+ }
237
+ }
238
+
239
+ return discovered;
240
+ }
241
+
242
+ /**
243
+ * Start all registered services, or a specific one by name.
244
+ */
245
+ static start(name?: string): void {
246
+ const targets = name
247
+ ? [registry.get(name)].filter(Boolean) as RegisteredService[]
248
+ : Array.from(registry.values());
249
+
250
+ for (const svc of targets) {
251
+ if (svc.context.running) continue;
252
+ svc.context.running = true;
253
+ svc.retries = 0;
254
+
255
+ if (svc.options.daemon) {
256
+ startDaemonService(svc);
257
+ } else if (svc.options.timing) {
258
+ startCronService(svc);
259
+ } else if (svc.options.interval != null) {
260
+ startIntervalService(svc);
261
+ } else {
262
+ // Default: run once immediately
263
+ executeHandler(svc);
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Stop all running services, or a specific one by name.
270
+ */
271
+ static stop(name?: string): void {
272
+ const targets = name
273
+ ? [registry.get(name)].filter(Boolean) as RegisteredService[]
274
+ : Array.from(registry.values());
275
+
276
+ for (const svc of targets) {
277
+ svc.context.running = false;
278
+ if (svc.timerId) {
279
+ clearInterval(svc.timerId);
280
+ svc.timerId = null;
281
+ }
282
+ }
283
+ }
284
+
285
+ /**
286
+ * List all registered services with their current state.
287
+ */
288
+ static list(): ServiceInfo[] {
289
+ return Array.from(registry.values()).map((svc) => ({
290
+ name: svc.name,
291
+ options: svc.options,
292
+ running: svc.context.running,
293
+ lastRun: svc.context.lastRun,
294
+ retries: svc.retries,
295
+ }));
296
+ }
297
+
298
+ /**
299
+ * Check if a specific service is running.
300
+ */
301
+ static isRunning(name: string): boolean {
302
+ const svc = registry.get(name);
303
+ return svc?.context.running ?? false;
304
+ }
305
+
306
+ /**
307
+ * Remove a service from the registry (stops it first if running).
308
+ */
309
+ static remove(name: string): boolean {
310
+ const svc = registry.get(name);
311
+ if (!svc) return false;
312
+ ServiceRunner.stop(name);
313
+ return registry.delete(name);
314
+ }
315
+
316
+ /**
317
+ * Clear all registered services (stops them all first).
318
+ */
319
+ static clear(): void {
320
+ ServiceRunner.stop();
321
+ registry.clear();
322
+ }
323
+
324
+ /**
325
+ * Watch service files for changes and hot-reload in dev mode.
326
+ */
327
+ static watch(serviceDir?: string): void {
328
+ const dir =
329
+ serviceDir ?? process.env.TINA4_SERVICE_DIR ?? "src/services";
330
+
331
+ let entries: string[];
332
+ try {
333
+ entries = readdirSync(dir);
334
+ } catch {
335
+ return;
336
+ }
337
+
338
+ for (const entry of entries) {
339
+ const ext = extname(entry);
340
+ if (ext !== ".ts" && ext !== ".js") continue;
341
+ const fullPath = join(dir, entry);
342
+ if (watchedFiles.has(fullPath)) continue;
343
+ watchedFiles.add(fullPath);
344
+
345
+ watchFile(fullPath, { interval: 1000 }, async () => {
346
+ // Re-discover and restart
347
+ const fileUrl = pathToFileURL(fullPath).href;
348
+ try {
349
+ // Bust module cache by appending timestamp
350
+ const mod = await import(fileUrl + "?t=" + Date.now());
351
+ const exp = mod.default ?? mod;
352
+ if (exp.name && typeof exp.handler === "function") {
353
+ ServiceRunner.stop(exp.name);
354
+ ServiceRunner.remove(exp.name);
355
+ const opts: ServiceOptions = {
356
+ timing: exp.timing,
357
+ daemon: exp.daemon,
358
+ interval: exp.interval,
359
+ maxRetries: exp.maxRetries,
360
+ };
361
+ ServiceRunner.register(exp.name, exp.handler, opts);
362
+ ServiceRunner.start(exp.name);
363
+ }
364
+ } catch {
365
+ // skip
366
+ }
367
+ });
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Stop watching service files.
373
+ */
374
+ static unwatch(): void {
375
+ for (const filePath of watchedFiles) {
376
+ unwatchFile(filePath);
377
+ }
378
+ watchedFiles.clear();
379
+ }
380
+ }