tina4-nodejs 3.10.85 → 3.10.87
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/CLAUDE.md
CHANGED
|
@@ -154,7 +154,8 @@ Database layer with auto-CRUD generation, seeding, fake data, and SQL translatio
|
|
|
154
154
|
- `seeder.ts` — Database seeding (`seedTable` for raw SQL, `seedOrm` for model-based)
|
|
155
155
|
- `sqlTranslation.ts` — Cross-engine SQL translator (`SQLTranslator`) and TTL query cache (`QueryCache`)
|
|
156
156
|
- **Instance methods:** `save(): this|null` (fluent, null on failure), `delete()`, `forceDelete()`, `restore()`, `load(sql, params?, include?): boolean`, `validate(): string[]`, `toDict(include?)`, `toAssoc(include?)`, `toObject()`, `toArray(): unknown[]`, `toList()`, `toJson(include?)`, `hasOne(class, fk)`, `hasMany(class, fk, limit?, offset?)`, `belongsTo(class, fk)`
|
|
157
|
-
- **Static methods:** `find(id, include?)`, `findById(id, include?)`, `findOrFail(id)`, `create(data)`, `all(where?, params?, include?)`, `select(sql, params?)`, `selectOne(sql, params?, include?)`, `where(conditions, params?, limit?, offset?, include?)`, `count(conditions?, params?)`, `withTrashed(conditions?, params?, limit?, offset?)`, `scope(name, filterSql, params?)` (registers reusable method), `createTable()`, `query()`
|
|
157
|
+
- **Static methods:** `find(id, include?)`, `findById(id, include?)`, `findOrFail(id)`, `create(data)`, `all(where?, params?, include?)`, `select(sql, params?)`, `selectOne(sql, params?, include?)`, `where(conditions, params?, limit?, offset?, include?)`, `count(conditions?, params?)`, `withTrashed(conditions?, params?, limit?, offset?)`, `scope(name, filterSql, params?)` (registers reusable method), `createTable()`, `query()`, `_processForeignKeys()`, `_applyFkRegistry()`
|
|
158
|
+
- **Foreign key auto-wire:** Declare a field with `type: "foreignKey"` and `references: "ModelName"` to auto-wire both `belongsTo` on the declaring model and `hasMany` on the referenced model. Optional `relatedName` overrides the has-many key. Models must be registered via `BaseModel.registerModel(name, class)` for name-based resolution. Example: `user_id: { type: "foreignKey", references: "User" }` → `post.belongsTo(User, "user_id")` and `user.hasMany(Post, "user_id")` both resolve without extra wiring.
|
|
158
159
|
- QueryBuilder supports `toMongo()` for generating MongoDB query documents from the same fluent API
|
|
159
160
|
- `getNextId(table: string, pkColumn?: string, generatorName?: string): Promise<number>` — Race-safe ID generation using atomic sequence table (`tina4_sequences`). SQLite/MySQL/MSSQL use `tina4_sequences` with atomic UPDATE+SELECT. PostgreSQL auto-creates sequences if missing. Firebird uses existing generators (unchanged).
|
|
160
161
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.87",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Tina4 for Node.js/TypeScript — 54 built-in features, zero dependencies",
|
|
6
6
|
"keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
|
|
@@ -37,17 +37,34 @@ export async function serveProject(options: ServeOptions): Promise<void> {
|
|
|
37
37
|
staticDir,
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
// Watch for file changes
|
|
40
|
+
// Watch for file changes.
|
|
41
|
+
//
|
|
42
|
+
// Templates and static assets are re-read from disk every request in dev mode,
|
|
43
|
+
// so we only need to touch the router when a .ts/.js route file actually
|
|
44
|
+
// changes. Clearing the router on every edit (including templates) leaves a
|
|
45
|
+
// brief window where the router is empty — any request hitting that window
|
|
46
|
+
// gets a 404 whose response path bypasses the dev toolbar injection, so the
|
|
47
|
+
// toolbar appears to "vanish" after a hot reload. Route-file-only clearing
|
|
48
|
+
// matches the behaviour of Python's DevReload and the fix made in PHP v3.10.87.
|
|
41
49
|
const noReload = ["true", "1", "yes"].includes((process.env.TINA4_NO_RELOAD ?? "").toLowerCase());
|
|
42
50
|
const watchDirs = [routesDir, ormDir, modelsDir, templatesDir].filter((d) => existsSync(d));
|
|
43
51
|
let watcher: { close: () => void } | null = null;
|
|
44
52
|
if (!noReload) {
|
|
45
|
-
watcher = watchForChanges(watchDirs, async () => {
|
|
53
|
+
watcher = watchForChanges(watchDirs, async ({ code }) => {
|
|
54
|
+
if (!code) {
|
|
55
|
+
// Template/CSS/JS asset change — nothing to do in the server. The
|
|
56
|
+
// browser will re-fetch on its own reload cycle and the request will
|
|
57
|
+
// be served against the existing route set with the toolbar intact.
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
46
60
|
try {
|
|
61
|
+
// Re-discover routes. discoverRoutes() cache-busts imports via ?t=<timestamp>,
|
|
62
|
+
// so the new modules are loaded fresh. Build the new list first, then
|
|
63
|
+
// replace the router's state in one back-to-back block to minimise the
|
|
64
|
+
// window where the router is empty.
|
|
47
65
|
const { discoverRoutes } = await import("../../../core/src/index.js");
|
|
48
|
-
// Clear routes BEFORE re-discovery to avoid stale duplicates
|
|
49
|
-
server.router.clear();
|
|
50
66
|
const routes = await discoverRoutes(routesDir);
|
|
67
|
+
server.router.clear();
|
|
51
68
|
for (const route of routes) {
|
|
52
69
|
server.router.addRoute(route);
|
|
53
70
|
}
|
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
import { watch, existsSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { resolve, extname } from "node:path";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* File types that indicate a code change and require route re-discovery.
|
|
6
|
+
* Template/CSS/JS asset changes don't need the router to be touched.
|
|
7
|
+
*/
|
|
8
|
+
const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Watch directories for file changes.
|
|
12
|
+
*
|
|
13
|
+
* The callback receives `{ code: boolean }` indicating whether any of the
|
|
14
|
+
* changed files were source code (.ts/.js). If only templates/CSS/JS assets
|
|
15
|
+
* changed, `code` is false and the caller should skip route re-discovery —
|
|
16
|
+
* clearing the router for an asset edit leaves a brief window of 404s that
|
|
17
|
+
* make the dev toolbar vanish.
|
|
18
|
+
*/
|
|
4
19
|
export function watchForChanges(
|
|
5
20
|
dirs: string[],
|
|
6
|
-
onChange: () => void
|
|
21
|
+
onChange: (info: { code: boolean }) => void
|
|
7
22
|
): { close: () => void } {
|
|
8
23
|
const watchers: ReturnType<typeof watch>[] = [];
|
|
9
24
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
25
|
+
let codeChangePending = false;
|
|
10
26
|
|
|
11
27
|
const debouncedOnChange = () => {
|
|
12
28
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
13
29
|
debounceTimer = setTimeout(() => {
|
|
14
|
-
|
|
15
|
-
|
|
30
|
+
const code = codeChangePending;
|
|
31
|
+
codeChangePending = false;
|
|
32
|
+
console.log(
|
|
33
|
+
`\n \x1b[33mFile change detected${code ? ", reloading routes" : ""}...\x1b[0m\n`,
|
|
34
|
+
);
|
|
35
|
+
onChange({ code });
|
|
16
36
|
}, 200);
|
|
17
37
|
};
|
|
18
38
|
|
|
19
39
|
for (const dir of dirs) {
|
|
20
40
|
if (!existsSync(dir)) continue;
|
|
21
41
|
try {
|
|
22
|
-
const watcher = watch(resolve(dir), { recursive: true }, () => {
|
|
42
|
+
const watcher = watch(resolve(dir), { recursive: true }, (_event, filename) => {
|
|
43
|
+
if (filename && CODE_EXTENSIONS.has(extname(filename))) {
|
|
44
|
+
codeChangePending = true;
|
|
45
|
+
}
|
|
23
46
|
debouncedOnChange();
|
|
24
47
|
});
|
|
25
48
|
watchers.push(watcher);
|
|
@@ -29,6 +29,12 @@ function _pluralRelKeys(): boolean {
|
|
|
29
29
|
return /^(true|1|yes)$/i.test(v);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Cross-model FK registry: maps referenced model name → list of has-many specs.
|
|
34
|
+
* Populated by BaseModel._processForeignKeys() when a model with foreignKey fields is used.
|
|
35
|
+
*/
|
|
36
|
+
const _fkRegistry = new Map<string, Array<{ foreignKey: string; declaringModel: string; hasManyKey: string }>>();
|
|
37
|
+
|
|
32
38
|
/**
|
|
33
39
|
* BaseModel provides instance methods for ORM models.
|
|
34
40
|
* Models extend this class and define static properties.
|
|
@@ -143,6 +149,49 @@ export class BaseModel {
|
|
|
143
149
|
return reverse;
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Process any foreignKey field definitions on this model, auto-wiring:
|
|
154
|
+
* - belongsTo entries on this model (strip _id from key → association name)
|
|
155
|
+
* - hasMany entries on the referenced model via the module-level _fkRegistry
|
|
156
|
+
*
|
|
157
|
+
* Idempotent — safe to call multiple times.
|
|
158
|
+
*/
|
|
159
|
+
static _processForeignKeys(): void {
|
|
160
|
+
const fields = this.fields ?? {};
|
|
161
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
162
|
+
if (def.type !== "foreignKey" || !def.references) continue;
|
|
163
|
+
|
|
164
|
+
// Auto-wire belongsTo on this model
|
|
165
|
+
const belongsName = key.endsWith("_id") ? key.slice(0, -3) : key;
|
|
166
|
+
this.belongsTo = this.belongsTo ?? [];
|
|
167
|
+
if (!this.belongsTo.find((r) => r.foreignKey === key)) {
|
|
168
|
+
this.belongsTo.push({ model: def.references, foreignKey: key });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Register hasMany on the referenced model via the module-level registry
|
|
172
|
+
const hasManyKey = def.relatedName ?? (this.tableName ?? this.name.toLowerCase());
|
|
173
|
+
const existing = _fkRegistry.get(def.references) ?? [];
|
|
174
|
+
if (!existing.find((r) => r.foreignKey === key && r.declaringModel === this.name)) {
|
|
175
|
+
existing.push({ foreignKey: key, declaringModel: this.name, hasManyKey });
|
|
176
|
+
_fkRegistry.set(def.references, existing);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Merge any FK-registry-registered hasMany entries for this model.
|
|
183
|
+
* Called before relationship resolution so the referenced model gets its has-many wired.
|
|
184
|
+
*/
|
|
185
|
+
static _applyFkRegistry(): void {
|
|
186
|
+
const entries = _fkRegistry.get(this.name) ?? [];
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
this.hasMany = this.hasMany ?? [];
|
|
189
|
+
if (!this.hasMany.find((r) => r.foreignKey === entry.foreignKey && r.model === entry.declaringModel)) {
|
|
190
|
+
this.hasMany.push({ model: entry.declaringModel, foreignKey: entry.foreignKey });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
146
195
|
/**
|
|
147
196
|
* Create a fluent QueryBuilder pre-configured for this model's table and database.
|
|
148
197
|
*
|
|
@@ -1029,6 +1078,10 @@ export class BaseModel {
|
|
|
1029
1078
|
private _lazyLoadRelationship(relName: string): unknown {
|
|
1030
1079
|
const ModelClass = this.constructor as typeof BaseModel;
|
|
1031
1080
|
|
|
1081
|
+
// Apply FK registry so foreignKey fields auto-wire relationships
|
|
1082
|
+
ModelClass._processForeignKeys();
|
|
1083
|
+
ModelClass._applyFkRegistry();
|
|
1084
|
+
|
|
1032
1085
|
// Check hasOne
|
|
1033
1086
|
if (ModelClass.hasOne) {
|
|
1034
1087
|
const rel = ModelClass.hasOne.find((r) => r.model.toLowerCase() === relName || r.model === relName);
|
|
@@ -1079,6 +1132,10 @@ export class BaseModel {
|
|
|
1079
1132
|
|
|
1080
1133
|
const ModelClass = instances[0].constructor as typeof BaseModel;
|
|
1081
1134
|
|
|
1135
|
+
// Apply FK registry so foreignKey fields auto-wire hasMany on referenced models
|
|
1136
|
+
ModelClass._processForeignKeys();
|
|
1137
|
+
ModelClass._applyFkRegistry();
|
|
1138
|
+
|
|
1082
1139
|
// Group includes: top-level and nested
|
|
1083
1140
|
const topLevel: Record<string, string[]> = {};
|
|
1084
1141
|
for (const inc of include) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type FieldType = "string" | "integer" | "number" | "numeric" | "boolean" | "datetime" | "text";
|
|
1
|
+
export type FieldType = "string" | "integer" | "number" | "numeric" | "boolean" | "datetime" | "text" | "foreignKey";
|
|
2
2
|
|
|
3
3
|
export interface FieldDefinition {
|
|
4
4
|
type: FieldType;
|
|
@@ -11,6 +11,10 @@ export interface FieldDefinition {
|
|
|
11
11
|
min?: number;
|
|
12
12
|
max?: number;
|
|
13
13
|
pattern?: string;
|
|
14
|
+
/** For type "foreignKey": the referenced model name (string) */
|
|
15
|
+
references?: string;
|
|
16
|
+
/** For type "foreignKey": override the has-many property name on the referenced model */
|
|
17
|
+
relatedName?: string;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export interface RelationshipDefinition {
|