tina4-nodejs 3.13.16 → 3.13.18
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 +3 -3
- package/package.json +1 -1
- package/packages/orm/src/adapters/postgres.ts +29 -0
- package/packages/orm/src/baseModel.ts +65 -12
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.13.18)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.13.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.13.18 — The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
8
8
|
|
|
9
9
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
10
10
|
|
|
@@ -1062,7 +1062,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
|
|
|
1062
1062
|
## v3 Features Summary
|
|
1063
1063
|
|
|
1064
1064
|
- **45 built-in features**, zero third-party dependencies
|
|
1065
|
-
- **3,
|
|
1065
|
+
- **3,653 tests** passing across all modules
|
|
1066
1066
|
- **Race-safe `getNextId()`** with atomic sequence table (`tina4_sequences`) for SQLite/MySQL/MSSQL; PostgreSQL auto-creates sequences
|
|
1067
1067
|
- **Frond template engine optimizations**: pre-compiled regexes, lazy loop context (copy-on-write), filter chain caching, path split caching, inline common filters (11-15% speedup)
|
|
1068
1068
|
- **Production server auto-detect**: `npx tina4nodejs serve --production` auto-uses cluster mode
|
package/package.json
CHANGED
|
@@ -10,12 +10,14 @@ import { SQLTranslator } from "../sqlTranslation.js";
|
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
|
|
12
12
|
let pg: typeof import("pg") | null = null;
|
|
13
|
+
let typeParsersRegistered = false;
|
|
13
14
|
|
|
14
15
|
function requirePg(): typeof import("pg") {
|
|
15
16
|
if (pg) return pg;
|
|
16
17
|
try {
|
|
17
18
|
const req = createRequire(import.meta.url);
|
|
18
19
|
pg = req("pg");
|
|
20
|
+
registerTypeParsers(pg!);
|
|
19
21
|
return pg!;
|
|
20
22
|
} catch {
|
|
21
23
|
throw new Error(
|
|
@@ -28,6 +30,33 @@ function requirePg(): typeof import("pg") {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Register global pg type parsers so int8 and numeric/decimal columns decode to
|
|
35
|
+
* JS numbers instead of strings. node-postgres returns int8 (OID 20) and
|
|
36
|
+
* numeric (OID 1700) as strings by default to preserve full precision; Python,
|
|
37
|
+
* Ruby and PHP all return native numerics for aggregates (SUM, AVG, COUNT, …),
|
|
38
|
+
* so this brings Node to cross-framework parity. count() and getNextId() already
|
|
39
|
+
* coerce via Number(), so this is purely additive for them.
|
|
40
|
+
*
|
|
41
|
+
* PRECISION CAVEAT: values beyond Number.MAX_SAFE_INTEGER (2^53 - 1) lose
|
|
42
|
+
* precision when coerced to a JS double. That is the accepted trade-off for
|
|
43
|
+
* parity — Python and Ruby return native numerics (and lose precision the same
|
|
44
|
+
* way for floats) too. Applications that need exact 64-bit/arbitrary-precision
|
|
45
|
+
* values should select the column with an explicit ::text cast.
|
|
46
|
+
*
|
|
47
|
+
* Idempotent — registration runs once per process.
|
|
48
|
+
*/
|
|
49
|
+
function registerTypeParsers(pgModule: typeof import("pg")): void {
|
|
50
|
+
if (typeParsersRegistered) return;
|
|
51
|
+
const types = pgModule.types ?? (pgModule as any).default?.types;
|
|
52
|
+
if (!types?.setTypeParser) return;
|
|
53
|
+
// OID 20 = int8 (bigint) → Number (NULL passes through untouched by pg)
|
|
54
|
+
types.setTypeParser(20, (v: string) => Number(v));
|
|
55
|
+
// OID 1700 = numeric / decimal → parseFloat
|
|
56
|
+
types.setTypeParser(1700, (v: string) => parseFloat(v));
|
|
57
|
+
typeParsersRegistered = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
export interface PostgresConfig {
|
|
32
61
|
host?: string;
|
|
33
62
|
port?: number;
|
|
@@ -8,6 +8,7 @@ import { validate as validateFields } from "./validation.js";
|
|
|
8
8
|
import { QueryBuilder } from "./queryBuilder.js";
|
|
9
9
|
import { SQLiteAdapter } from "./adapters/sqlite.js";
|
|
10
10
|
import { QueryCache } from "./sqlTranslation.js";
|
|
11
|
+
import { Log } from "@tina4/core";
|
|
11
12
|
import type { DatabaseAdapter, FieldDefinition, RelationshipDefinition } from "./types.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -1149,6 +1150,28 @@ export class BaseModel {
|
|
|
1149
1150
|
|
|
1150
1151
|
static registerModel(name: string, modelClass: typeof BaseModel): void {
|
|
1151
1152
|
BaseModel._modelRegistry[name] = modelClass;
|
|
1153
|
+
// Eagerly process this model's foreignKey fields so the cross-model
|
|
1154
|
+
// _fkRegistry is populated as soon as the model is known — not only when
|
|
1155
|
+
// the *declaring* model happens to be touched first. This is what makes
|
|
1156
|
+
// eager loading work standalone (without server-boot auto-discovery):
|
|
1157
|
+
// a parent's hasMany is registered by the child's _processForeignKeys(),
|
|
1158
|
+
// so we must run it for every registered model proactively.
|
|
1159
|
+
modelClass._processForeignKeys();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Process foreignKey fields on every registered model so the cross-model
|
|
1164
|
+
* _fkRegistry (and each model's belongsTo/hasMany) is fully wired regardless
|
|
1165
|
+
* of which model was used first. Idempotent — _processForeignKeys() and
|
|
1166
|
+
* _applyFkRegistry() both guard against duplicates.
|
|
1167
|
+
*/
|
|
1168
|
+
private static _processAllForeignKeys(): void {
|
|
1169
|
+
for (const modelClass of Object.values(BaseModel._modelRegistry)) {
|
|
1170
|
+
modelClass._processForeignKeys();
|
|
1171
|
+
}
|
|
1172
|
+
for (const modelClass of Object.values(BaseModel._modelRegistry)) {
|
|
1173
|
+
modelClass._applyFkRegistry();
|
|
1174
|
+
}
|
|
1152
1175
|
}
|
|
1153
1176
|
|
|
1154
1177
|
/**
|
|
@@ -1168,9 +1191,13 @@ export class BaseModel {
|
|
|
1168
1191
|
|
|
1169
1192
|
const ModelClass = instances[0].constructor as typeof BaseModel;
|
|
1170
1193
|
|
|
1171
|
-
//
|
|
1172
|
-
|
|
1173
|
-
|
|
1194
|
+
// Wire FK relationships across ALL registered models, not just the parent.
|
|
1195
|
+
// A parent's hasMany is declared by the CHILD's foreignKey field, so we must
|
|
1196
|
+
// process every registered model's FKs before resolving includes — otherwise
|
|
1197
|
+
// a standalone Author.findById(id, ["posts"]) silently finds nothing because
|
|
1198
|
+
// Post._processForeignKeys() never ran. _applyFkRegistry() then merges the
|
|
1199
|
+
// registered hasMany entries onto each model.
|
|
1200
|
+
BaseModel._processAllForeignKeys();
|
|
1174
1201
|
|
|
1175
1202
|
// Group includes: top-level and nested
|
|
1176
1203
|
const topLevel: Record<string, string[]> = {};
|
|
@@ -1186,28 +1213,54 @@ export class BaseModel {
|
|
|
1186
1213
|
}
|
|
1187
1214
|
|
|
1188
1215
|
for (const [relName, nested] of Object.entries(topLevel)) {
|
|
1189
|
-
// Find the relationship definition
|
|
1216
|
+
// Find the relationship definition.
|
|
1217
|
+
//
|
|
1218
|
+
// Include names are resolved case-insensitively against each candidate
|
|
1219
|
+
// relation. Accepted forms (for a relation to model "Post" on table "posts"):
|
|
1220
|
+
// - the model name → "Post" / "post"
|
|
1221
|
+
// - the auto/related key → "post" (singular) or "posts" (when
|
|
1222
|
+
// TINA4_ORM_PLURAL_TABLE_NAMES is enabled)
|
|
1223
|
+
// - the table name → "posts"
|
|
1224
|
+
// All matching is lower-cased so "Post", "post" and "posts" all resolve.
|
|
1225
|
+
const want = relName.toLowerCase();
|
|
1190
1226
|
let relDef: RelationshipDefinition | undefined;
|
|
1191
1227
|
let relType: "hasOne" | "hasMany" | "belongsTo" | null = null;
|
|
1192
1228
|
|
|
1229
|
+
const matchesModel = (r: RelationshipDefinition): boolean => {
|
|
1230
|
+
const base = r.model.toLowerCase();
|
|
1231
|
+
const related = BaseModel._modelRegistry[r.model];
|
|
1232
|
+
const table = related?.tableName?.toLowerCase();
|
|
1233
|
+
return (
|
|
1234
|
+
base === want ||
|
|
1235
|
+
base + "s" === want ||
|
|
1236
|
+
(table !== undefined && table === want)
|
|
1237
|
+
);
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1193
1240
|
if (ModelClass.hasOne) {
|
|
1194
|
-
relDef = ModelClass.hasOne.find(
|
|
1241
|
+
relDef = ModelClass.hasOne.find(matchesModel);
|
|
1195
1242
|
if (relDef) relType = "hasOne";
|
|
1196
1243
|
}
|
|
1197
1244
|
if (!relDef && ModelClass.hasMany) {
|
|
1198
|
-
relDef = ModelClass.hasMany.find(
|
|
1199
|
-
const base = r.model.toLowerCase();
|
|
1200
|
-
const key = _pluralRelKeys() ? base + "s" : base;
|
|
1201
|
-
return key === relName || base === relName || r.model === relName;
|
|
1202
|
-
});
|
|
1245
|
+
relDef = ModelClass.hasMany.find(matchesModel);
|
|
1203
1246
|
if (relDef) relType = "hasMany";
|
|
1204
1247
|
}
|
|
1205
1248
|
if (!relDef && ModelClass.belongsTo) {
|
|
1206
|
-
relDef = ModelClass.belongsTo.find(
|
|
1249
|
+
relDef = ModelClass.belongsTo.find(matchesModel);
|
|
1207
1250
|
if (relDef) relType = "belongsTo";
|
|
1208
1251
|
}
|
|
1209
1252
|
|
|
1210
|
-
if (!relDef || !relType)
|
|
1253
|
+
if (!relDef || !relType) {
|
|
1254
|
+
// Don't silently skip — a typo'd or unknown include name is almost
|
|
1255
|
+
// always a developer mistake. Surface it so it's visible.
|
|
1256
|
+
Log.warn(
|
|
1257
|
+
`eager-load: include "${relName}" did not match any relationship on ` +
|
|
1258
|
+
`${ModelClass.name} (table "${ModelClass.tableName}"). ` +
|
|
1259
|
+
`Accepted forms are the related model name, its singular/plural ` +
|
|
1260
|
+
`key, or the related table name (case-insensitive).`,
|
|
1261
|
+
);
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1211
1264
|
|
|
1212
1265
|
const relatedClass = BaseModel._modelRegistry[relDef.model];
|
|
1213
1266
|
if (!relatedClass) continue;
|