tina4-nodejs 3.2.1 → 3.5.0

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 (34) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/packages/cli/src/bin.ts +13 -1
  5. package/packages/cli/src/commands/migrate.ts +19 -5
  6. package/packages/cli/src/commands/migrateCreate.ts +29 -28
  7. package/packages/cli/src/commands/migrateRollback.ts +59 -0
  8. package/packages/cli/src/commands/migrateStatus.ts +62 -0
  9. package/packages/core/public/js/tina4-dev-admin.min.js +1 -1
  10. package/packages/core/public/js/tina4js.min.js +47 -0
  11. package/packages/core/src/auth.ts +44 -10
  12. package/packages/core/src/devAdmin.ts +14 -16
  13. package/packages/core/src/index.ts +10 -3
  14. package/packages/core/src/middleware.ts +232 -2
  15. package/packages/core/src/queue.ts +127 -25
  16. package/packages/core/src/queueBackends/mongoBackend.ts +223 -0
  17. package/packages/core/src/request.ts +3 -3
  18. package/packages/core/src/router.ts +115 -51
  19. package/packages/core/src/server.ts +47 -3
  20. package/packages/core/src/session.ts +29 -1
  21. package/packages/core/src/sessionHandlers/databaseHandler.ts +134 -0
  22. package/packages/core/src/sessionHandlers/redisHandler.ts +230 -0
  23. package/packages/core/src/types.ts +12 -6
  24. package/packages/core/src/websocket.ts +11 -2
  25. package/packages/core/src/websocketConnection.ts +4 -2
  26. package/packages/frond/src/engine.ts +66 -1
  27. package/packages/orm/src/autoCrud.ts +17 -12
  28. package/packages/orm/src/baseModel.ts +99 -21
  29. package/packages/orm/src/database.ts +197 -69
  30. package/packages/orm/src/databaseResult.ts +207 -0
  31. package/packages/orm/src/index.ts +6 -3
  32. package/packages/orm/src/migration.ts +296 -71
  33. package/packages/orm/src/model.ts +1 -0
  34. package/packages/orm/src/types.ts +1 -0
@@ -8,11 +8,16 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
8
8
  const routes: RouteDefinition[] = [];
9
9
 
10
10
  for (const { definition } of models) {
11
- const { tableName, fields, softDelete, tableFilter } = definition;
11
+ const { tableName, fields, softDelete, tableFilter, fieldMapping } = definition;
12
12
  const basePath = `/api/${tableName}`;
13
+ const mapping = fieldMapping ?? {};
13
14
 
14
- // Find primary key field
15
+ // Helper to get DB column name for a JS property name
16
+ const getDbCol = (prop: string): string => mapping[prop] ?? prop;
17
+
18
+ // Find primary key field (JS property name) and its DB column name
15
19
  const pkField = Object.entries(fields).find(([, def]) => def.primaryKey)?.[0] ?? "id";
20
+ const pkColumn = getDbCol(pkField);
16
21
 
17
22
  // Build extra WHERE conditions for soft delete and table filter
18
23
  const extraConditions: string[] = [];
@@ -65,7 +70,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
65
70
  },
66
71
  handler: async (req: Tina4Request, res: Tina4Response) => {
67
72
  const adapter = getAdapter();
68
- const conditions = [`"${pkField}" = ?`, ...extraConditions];
73
+ const conditions = [`"${pkColumn}" = ?`, ...extraConditions];
69
74
  const items = adapter.query(
70
75
  `SELECT * FROM "${tableName}" WHERE ${conditions.join(" AND ")}`,
71
76
  [req.params.id],
@@ -112,7 +117,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
112
117
  insertFields.push(["is_deleted", 0]);
113
118
  }
114
119
 
115
- const columns = insertFields.map(([k]) => `"${k}"`).join(", ");
120
+ const columns = insertFields.map(([k]) => `"${getDbCol(k)}"`).join(", ");
116
121
  const placeholders = insertFields.map(() => "?").join(", ");
117
122
  const values = insertFields.map(([, v]) => v);
118
123
 
@@ -123,7 +128,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
123
128
 
124
129
  const id = result.lastInsertRowid;
125
130
  const created = adapter.query(
126
- `SELECT * FROM "${tableName}" WHERE "${pkField}" = ?`,
131
+ `SELECT * FROM "${tableName}" WHERE "${pkColumn}" = ?`,
127
132
  [id],
128
133
  );
129
134
 
@@ -155,7 +160,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
155
160
  const adapter = getAdapter();
156
161
 
157
162
  // Check exists (respect soft delete)
158
- const conditions = [`"${pkField}" = ?`, ...extraConditions];
163
+ const conditions = [`"${pkColumn}" = ?`, ...extraConditions];
159
164
  const existing = adapter.query(
160
165
  `SELECT * FROM "${tableName}" WHERE ${conditions.join(" AND ")}`,
161
166
  [req.params.id],
@@ -172,16 +177,16 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
172
177
  return;
173
178
  }
174
179
 
175
- const setClause = updateFields.map(([k]) => `"${k}" = ?`).join(", ");
180
+ const setClause = updateFields.map(([k]) => `"${getDbCol(k)}" = ?`).join(", ");
176
181
  const values = [...updateFields.map(([, v]) => v), req.params.id];
177
182
 
178
183
  adapter.execute(
179
- `UPDATE "${tableName}" SET ${setClause} WHERE "${pkField}" = ?`,
184
+ `UPDATE "${tableName}" SET ${setClause} WHERE "${pkColumn}" = ?`,
180
185
  values,
181
186
  );
182
187
 
183
188
  const updated = adapter.query(
184
- `SELECT * FROM "${tableName}" WHERE "${pkField}" = ?`,
189
+ `SELECT * FROM "${tableName}" WHERE "${pkColumn}" = ?`,
185
190
  [req.params.id],
186
191
  );
187
192
 
@@ -200,7 +205,7 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
200
205
  handler: async (req: Tina4Request, res: Tina4Response) => {
201
206
  const adapter = getAdapter();
202
207
 
203
- const conditions = [`"${pkField}" = ?`, ...extraConditions];
208
+ const conditions = [`"${pkColumn}" = ?`, ...extraConditions];
204
209
  const existing = adapter.query(
205
210
  `SELECT * FROM "${tableName}" WHERE ${conditions.join(" AND ")}`,
206
211
  [req.params.id],
@@ -212,13 +217,13 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
212
217
 
213
218
  if (softDelete) {
214
219
  adapter.execute(
215
- `UPDATE "${tableName}" SET is_deleted = 1 WHERE "${pkField}" = ?`,
220
+ `UPDATE "${tableName}" SET is_deleted = 1 WHERE "${pkColumn}" = ?`,
216
221
  [req.params.id],
217
222
  );
218
223
  res.json({ message: "Deleted (soft)", data: existing[0] });
219
224
  } else {
220
225
  adapter.execute(
221
- `DELETE FROM "${tableName}" WHERE "${pkField}" = ?`,
226
+ `DELETE FROM "${tableName}" WHERE "${pkColumn}" = ?`,
222
227
  [req.params.id],
223
228
  );
224
229
  res.json({ message: "Deleted", data: existing[0] });
@@ -15,6 +15,7 @@ import type { DatabaseAdapter, FieldDefinition, RelationshipDefinition } from ".
15
15
  * static hasOne = [{ model: "Profile", foreignKey: "user_id" }];
16
16
  * static hasMany = [{ model: "Post", foreignKey: "author_id" }];
17
17
  * static _db = "secondary";
18
+ * static fieldMapping = { firstName: "first_name", lastName: "last_name" };
18
19
  * }
19
20
  */
20
21
  export class BaseModel {
@@ -27,6 +28,14 @@ export class BaseModel {
27
28
  static belongsTo?: RelationshipDefinition[];
28
29
  static _db?: string;
29
30
 
31
+ /**
32
+ * Maps JS property names to database column names.
33
+ * Example: { firstName: "first_name" } means the JS property `firstName`
34
+ * corresponds to the database column `first_name`.
35
+ * Properties not listed here use the property name as-is.
36
+ */
37
+ static fieldMapping: Record<string, string> = {};
38
+
30
39
  /** Instance data */
31
40
  [key: string]: unknown;
32
41
 
@@ -35,12 +44,52 @@ export class BaseModel {
35
44
 
36
45
  constructor(data?: Record<string, unknown>) {
37
46
  if (data) {
47
+ const ModelClass = this.constructor as typeof BaseModel;
48
+ const reverseMapping = ModelClass.getReverseMapping();
38
49
  for (const [key, value] of Object.entries(data)) {
39
- this[key] = value;
50
+ // If this DB column has a mapping, use the JS property name instead
51
+ const jsProp = reverseMapping[key] ?? key;
52
+ this[jsProp] = value;
40
53
  }
41
54
  }
42
55
  }
43
56
 
57
+ /**
58
+ * Get the database column name for a JS property.
59
+ * Returns the mapped column name, or the property name if no mapping exists.
60
+ */
61
+ static getDbColumn(prop: string): string {
62
+ return this.fieldMapping[prop] ?? prop;
63
+ }
64
+
65
+ /**
66
+ * Get all instance data converted to database column names.
67
+ * Uses fieldMapping to translate JS property names to DB column names.
68
+ */
69
+ getDbData(): Record<string, unknown> {
70
+ const ModelClass = this.constructor as typeof BaseModel;
71
+ const result: Record<string, unknown> = {};
72
+ for (const key of Object.keys(ModelClass.fields)) {
73
+ if (this[key] !== undefined) {
74
+ const dbCol = ModelClass.getDbColumn(key);
75
+ result[dbCol] = this[key];
76
+ }
77
+ }
78
+ return result;
79
+ }
80
+
81
+ /**
82
+ * Get the reverse mapping (DB column → JS property).
83
+ * Flips fieldMapping so that { firstName: "first_name" } becomes { first_name: "firstName" }.
84
+ */
85
+ static getReverseMapping(): Record<string, string> {
86
+ const reverse: Record<string, string> = {};
87
+ for (const [jsProp, dbCol] of Object.entries(this.fieldMapping)) {
88
+ reverse[dbCol] = jsProp;
89
+ }
90
+ return reverse;
91
+ }
92
+
44
93
  /**
45
94
  * Get the database adapter for this model.
46
95
  */
@@ -52,12 +101,19 @@ export class BaseModel {
52
101
  }
53
102
 
54
103
  /**
55
- * Get the primary key field name.
104
+ * Get the primary key field name (JS property name).
56
105
  */
57
106
  protected static getPkField(): string {
58
107
  return Object.entries(this.fields).find(([, def]) => def.primaryKey)?.[0] ?? "id";
59
108
  }
60
109
 
110
+ /**
111
+ * Get the primary key database column name (applies fieldMapping).
112
+ */
113
+ protected static getPkColumn(): string {
114
+ return this.getDbColumn(this.getPkField());
115
+ }
116
+
61
117
  /**
62
118
  * Find a record by primary key.
63
119
  * @param id Primary key value.
@@ -67,7 +123,8 @@ export class BaseModel {
67
123
  const ModelClass = this as unknown as typeof BaseModel & (new (data?: Record<string, unknown>) => T);
68
124
  const db = ModelClass.getDb();
69
125
  const pk = ModelClass.getPkField();
70
- let sql = `SELECT * FROM "${ModelClass.tableName}" WHERE "${pk}" = ?`;
126
+ const pkCol = ModelClass.getPkColumn();
127
+ let sql = `SELECT * FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`;
71
128
 
72
129
  if (ModelClass.softDelete) {
73
130
  sql += ` AND is_deleted = 0`;
@@ -129,6 +186,7 @@ export class BaseModel {
129
186
  const ModelClass = this.constructor as typeof BaseModel;
130
187
  const db = ModelClass.getDb();
131
188
  const pk = ModelClass.getPkField();
189
+ const pkCol = ModelClass.getPkColumn();
132
190
  const pkValue = this[pk];
133
191
  this._relCache = {}; // Clear relationship cache on save
134
192
 
@@ -139,17 +197,17 @@ export class BaseModel {
139
197
  );
140
198
  if (updateFields.length === 0) return;
141
199
 
142
- const setClause = updateFields.map(([k]) => `"${k}" = ?`).join(", ");
200
+ const setClause = updateFields.map(([k]) => `"${ModelClass.getDbColumn(k)}" = ?`).join(", ");
143
201
  const values = [...updateFields.map(([k]) => this[k]), pkValue];
144
202
 
145
- db.execute(`UPDATE "${ModelClass.tableName}" SET ${setClause} WHERE "${pk}" = ?`, values);
203
+ db.execute(`UPDATE "${ModelClass.tableName}" SET ${setClause} WHERE "${pkCol}" = ?`, values);
146
204
  } else {
147
205
  // Insert
148
206
  const insertFields = Object.entries(ModelClass.fields).filter(
149
207
  ([name, def]) => !(def.primaryKey && def.autoIncrement) && this[name] !== undefined,
150
208
  );
151
209
 
152
- const columns = insertFields.map(([k]) => `"${k}"`).join(", ");
210
+ const columns = insertFields.map(([k]) => `"${ModelClass.getDbColumn(k)}"`).join(", ");
153
211
  const placeholders = insertFields.map(() => "?").join(", ");
154
212
  const values = insertFields.map(([k]) => this[k]);
155
213
 
@@ -171,6 +229,7 @@ export class BaseModel {
171
229
  const ModelClass = this.constructor as typeof BaseModel;
172
230
  const db = ModelClass.getDb();
173
231
  const pk = ModelClass.getPkField();
232
+ const pkCol = ModelClass.getPkColumn();
174
233
  const pkValue = this[pk];
175
234
 
176
235
  if (pkValue === undefined || pkValue === null) {
@@ -179,13 +238,13 @@ export class BaseModel {
179
238
 
180
239
  if (ModelClass.softDelete) {
181
240
  db.execute(
182
- `UPDATE "${ModelClass.tableName}" SET is_deleted = 1 WHERE "${pk}" = ?`,
241
+ `UPDATE "${ModelClass.tableName}" SET is_deleted = 1 WHERE "${pkCol}" = ?`,
183
242
  [pkValue],
184
243
  );
185
244
  this.is_deleted = 1;
186
245
  } else {
187
246
  db.execute(
188
- `DELETE FROM "${ModelClass.tableName}" WHERE "${pk}" = ?`,
247
+ `DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
189
248
  [pkValue],
190
249
  );
191
250
  }
@@ -317,7 +376,13 @@ export class BaseModel {
317
376
  if (db.tableExists(this.tableName)) return;
318
377
 
319
378
  if (typeof db.createTable === "function") {
320
- db.createTable(this.tableName, this.fields);
379
+ // Remap field keys to DB column names if fieldMapping is defined
380
+ const mappedFields: Record<string, FieldDefinition> = {};
381
+ for (const [fieldName, def] of Object.entries(this.fields)) {
382
+ const dbCol = this.getDbColumn(fieldName);
383
+ mappedFields[dbCol] = def;
384
+ }
385
+ db.createTable(this.tableName, mappedFields);
321
386
  } else {
322
387
  // Fallback: build SQL manually
323
388
  const typeMap: Record<string, string> = {
@@ -331,9 +396,10 @@ export class BaseModel {
331
396
  };
332
397
 
333
398
  const colDefs: string[] = [];
334
- for (const [colName, def] of Object.entries(this.fields)) {
399
+ for (const [fieldName, def] of Object.entries(this.fields)) {
400
+ const dbCol = this.getDbColumn(fieldName);
335
401
  const sqlType = typeMap[def.type] || "TEXT";
336
- const parts = [`"${colName}" ${sqlType}`];
402
+ const parts = [`"${dbCol}" ${sqlType}`];
337
403
  if (def.primaryKey) parts.push("PRIMARY KEY");
338
404
  if (def.autoIncrement) parts.push("AUTOINCREMENT");
339
405
  if (def.required && !def.primaryKey) parts.push("NOT NULL");
@@ -382,6 +448,7 @@ export class BaseModel {
382
448
  const ModelClass = this.constructor as typeof BaseModel;
383
449
  const db = ModelClass.getDb();
384
450
  const pk = ModelClass.getPkField();
451
+ const pkCol = ModelClass.getPkColumn();
385
452
  const pkValue = this[pk];
386
453
 
387
454
  if (pkValue === undefined || pkValue === null) {
@@ -389,7 +456,7 @@ export class BaseModel {
389
456
  }
390
457
 
391
458
  db.execute(
392
- `DELETE FROM "${ModelClass.tableName}" WHERE "${pk}" = ?`,
459
+ `DELETE FROM "${ModelClass.tableName}" WHERE "${pkCol}" = ?`,
393
460
  [pkValue],
394
461
  );
395
462
  }
@@ -405,6 +472,7 @@ export class BaseModel {
405
472
 
406
473
  const db = ModelClass.getDb();
407
474
  const pk = ModelClass.getPkField();
475
+ const pkCol = ModelClass.getPkColumn();
408
476
  const pkValue = this[pk];
409
477
 
410
478
  if (pkValue === undefined || pkValue === null) {
@@ -412,7 +480,7 @@ export class BaseModel {
412
480
  }
413
481
 
414
482
  db.execute(
415
- `UPDATE "${ModelClass.tableName}" SET is_deleted = 0 WHERE "${pk}" = ?`,
483
+ `UPDATE "${ModelClass.tableName}" SET is_deleted = 0 WHERE "${pkCol}" = ?`,
416
484
  [pkValue],
417
485
  );
418
486
  this.is_deleted = 0;
@@ -570,15 +638,19 @@ export class BaseModel {
570
638
  relatedClass: typeof BaseModel & (new (data?: Record<string, unknown>) => R),
571
639
  foreignKey: string,
572
640
  ): R | null {
573
- const fkValue = this[foreignKey];
641
+ // foreignKey is a DB column name — resolve to JS property name on this model
642
+ const ModelClass = this.constructor as typeof BaseModel;
643
+ const reverseMap = ModelClass.getReverseMapping();
644
+ const fkProp = reverseMap[foreignKey] ?? foreignKey;
645
+ const fkValue = this[fkProp];
574
646
 
575
647
  if (fkValue === undefined || fkValue === null) {
576
648
  return null;
577
649
  }
578
650
 
579
651
  const db = relatedClass.getDb();
580
- const relatedPk = relatedClass.getPkField();
581
- let sql = `SELECT * FROM "${relatedClass.tableName}" WHERE "${relatedPk}" = ?`;
652
+ const relatedPkCol = relatedClass.getPkColumn();
653
+ let sql = `SELECT * FROM "${relatedClass.tableName}" WHERE "${relatedPkCol}" = ?`;
582
654
  if (relatedClass.softDelete) {
583
655
  sql += ` AND is_deleted = 0`;
584
656
  }
@@ -727,10 +799,12 @@ export class BaseModel {
727
799
  relatedClass._eagerLoad(related, nested);
728
800
  }
729
801
 
730
- // Group by FK
802
+ // Group by FK — fk is a DB column name, resolve to JS property name on the related model
803
+ const relatedReverseMap = relatedClass.getReverseMapping();
804
+ const fkProp = relatedReverseMap[fk] ?? fk;
731
805
  const grouped: Record<string, BaseModel[]> = {};
732
806
  for (const record of related) {
733
- const fkVal = String(record[fk]);
807
+ const fkVal = String(record[fkProp]);
734
808
  if (!grouped[fkVal]) grouped[fkVal] = [];
735
809
  grouped[fkVal].push(record);
736
810
  }
@@ -745,16 +819,20 @@ export class BaseModel {
745
819
  }
746
820
  }
747
821
  } else if (relType === "belongsTo") {
822
+ // fk is a DB column name on the current model — resolve to JS property name
823
+ const ownerReverseMap = ModelClass.getReverseMapping();
824
+ const fkProp = ownerReverseMap[fk] ?? fk;
748
825
  const fkValues = [...new Set(
749
826
  instances
750
- .map((inst) => inst[fk])
827
+ .map((inst) => inst[fkProp])
751
828
  .filter((v) => v !== undefined && v !== null),
752
829
  )];
753
830
  if (fkValues.length === 0) continue;
754
831
 
755
832
  const relatedPk = relatedClass.getPkField();
833
+ const relatedPkCol = relatedClass.getPkColumn();
756
834
  const placeholders = fkValues.map(() => "?").join(",");
757
- let sql = `SELECT * FROM "${relatedClass.tableName}" WHERE "${relatedPk}" IN (${placeholders})`;
835
+ let sql = `SELECT * FROM "${relatedClass.tableName}" WHERE "${relatedPkCol}" IN (${placeholders})`;
758
836
  if (relatedClass.softDelete) {
759
837
  sql += ` AND is_deleted = 0`;
760
838
  }
@@ -772,7 +850,7 @@ export class BaseModel {
772
850
  }
773
851
 
774
852
  for (const inst of instances) {
775
- const fkVal = inst[fk];
853
+ const fkVal = inst[fkProp];
776
854
  inst._relCache[relName] = fkVal !== undefined && fkVal !== null
777
855
  ? lookup[String(fkVal)] ?? null
778
856
  : null;