tina4-nodejs 3.1.2 → 3.4.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.
- package/CLAUDE.md +1 -1
- package/README.md +30 -2
- package/package.json +1 -1
- package/packages/cli/src/bin.ts +13 -1
- package/packages/cli/src/commands/migrate.ts +19 -5
- package/packages/cli/src/commands/migrateCreate.ts +29 -28
- package/packages/cli/src/commands/migrateRollback.ts +59 -0
- package/packages/cli/src/commands/migrateStatus.ts +62 -0
- package/packages/core/public/js/tina4-dev-admin.min.js +1 -1
- package/packages/core/src/auth.ts +44 -10
- package/packages/core/src/devAdmin.ts +14 -16
- package/packages/core/src/errorOverlay.ts +17 -15
- package/packages/core/src/index.ts +9 -2
- package/packages/core/src/queue.ts +127 -25
- package/packages/core/src/queueBackends/mongoBackend.ts +223 -0
- package/packages/core/src/request.ts +3 -3
- package/packages/core/src/routeDiscovery.ts +2 -1
- package/packages/core/src/router.ts +90 -51
- package/packages/core/src/server.ts +62 -4
- package/packages/core/src/session.ts +17 -1
- package/packages/core/src/sessionHandlers/databaseHandler.ts +134 -0
- package/packages/core/src/sessionHandlers/redisHandler.ts +230 -0
- package/packages/core/src/types.ts +12 -6
- package/packages/core/src/websocket.ts +11 -2
- package/packages/core/src/websocketConnection.ts +4 -2
- package/packages/frond/src/engine.ts +66 -1
- package/packages/orm/src/autoCrud.ts +17 -12
- package/packages/orm/src/baseModel.ts +99 -21
- package/packages/orm/src/database.ts +197 -69
- package/packages/orm/src/databaseResult.ts +207 -0
- package/packages/orm/src/index.ts +6 -3
- package/packages/orm/src/migration.ts +296 -71
- package/packages/orm/src/model.ts +1 -0
- package/packages/orm/src/types.ts +1 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { DatabaseAdapter, ColumnInfo } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Column metadata returned by columnInfo(). */
|
|
4
|
+
export interface ColumnInfoResult {
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
size: number | null;
|
|
8
|
+
decimals: number | null;
|
|
9
|
+
nullable: boolean;
|
|
10
|
+
primary_key: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* DatabaseResult — wraps fetched rows with convenience methods.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors Python's `DatabaseResult` dataclass from tina4_python.database.adapter.
|
|
17
|
+
* Provides iteration, JSON/CSV export, pagination metadata, and array-like access.
|
|
18
|
+
*/
|
|
19
|
+
export class DatabaseResult implements Iterable<Record<string, unknown>> {
|
|
20
|
+
readonly records: Record<string, unknown>[];
|
|
21
|
+
readonly columns: string[];
|
|
22
|
+
readonly count: number;
|
|
23
|
+
readonly limit: number;
|
|
24
|
+
readonly offset: number;
|
|
25
|
+
private readonly _adapter?: DatabaseAdapter;
|
|
26
|
+
private readonly _sql?: string;
|
|
27
|
+
private _columnInfoCache?: ColumnInfoResult[];
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
records?: Record<string, unknown>[],
|
|
31
|
+
columns?: string[],
|
|
32
|
+
count?: number,
|
|
33
|
+
limit?: number,
|
|
34
|
+
offset?: number,
|
|
35
|
+
adapter?: DatabaseAdapter,
|
|
36
|
+
sql?: string,
|
|
37
|
+
) {
|
|
38
|
+
this.records = records ?? [];
|
|
39
|
+
this.columns =
|
|
40
|
+
columns ?? (this.records.length > 0 ? Object.keys(this.records[0]) : []);
|
|
41
|
+
this.count = count ?? this.records.length;
|
|
42
|
+
this.limit = limit ?? this.records.length;
|
|
43
|
+
this.offset = offset ?? 0;
|
|
44
|
+
this._adapter = adapter;
|
|
45
|
+
this._sql = sql;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** JSON string of records. */
|
|
49
|
+
toJson(): string {
|
|
50
|
+
return JSON.stringify(this.records);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** CSV with header row. */
|
|
54
|
+
toCsv(): string {
|
|
55
|
+
if (this.columns.length === 0) return "";
|
|
56
|
+
const escape = (val: unknown): string => {
|
|
57
|
+
if (val === null || val === undefined) return "";
|
|
58
|
+
const str = String(val);
|
|
59
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
60
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
61
|
+
}
|
|
62
|
+
return str;
|
|
63
|
+
};
|
|
64
|
+
const header = this.columns.map(escape).join(",");
|
|
65
|
+
const rows = this.records.map((row) =>
|
|
66
|
+
this.columns.map((col) => escape(row[col])).join(","),
|
|
67
|
+
);
|
|
68
|
+
return [header, ...rows].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Same as records — plain array of row objects. */
|
|
72
|
+
toArray(): Record<string, unknown>[] {
|
|
73
|
+
return this.records;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Pagination envelope. */
|
|
77
|
+
toPaginate(): {
|
|
78
|
+
records: Record<string, unknown>[];
|
|
79
|
+
count: number;
|
|
80
|
+
limit: number;
|
|
81
|
+
offset: number;
|
|
82
|
+
} {
|
|
83
|
+
return {
|
|
84
|
+
records: this.records,
|
|
85
|
+
count: this.count,
|
|
86
|
+
limit: this.limit,
|
|
87
|
+
offset: this.offset,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Iterable — for (const row of result) */
|
|
92
|
+
[Symbol.iterator](): Iterator<Record<string, unknown>> {
|
|
93
|
+
return this.records[Symbol.iterator]();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Number of records in this page. */
|
|
97
|
+
get length(): number {
|
|
98
|
+
return this.records.length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Array-like indexed access with negative index support. */
|
|
102
|
+
at(index: number): Record<string, unknown> | undefined {
|
|
103
|
+
if (index < 0) {
|
|
104
|
+
index = this.records.length + index;
|
|
105
|
+
}
|
|
106
|
+
return this.records[index];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** JSON.stringify support — serialises as the records array. */
|
|
110
|
+
toJSON(): Record<string, unknown>[] {
|
|
111
|
+
return this.records;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Return column metadata for the query's table.
|
|
116
|
+
*
|
|
117
|
+
* Lazy — only queries the database when explicitly called. Caches the
|
|
118
|
+
* result so subsequent calls return immediately without re-querying.
|
|
119
|
+
*/
|
|
120
|
+
columnInfo(): ColumnInfoResult[] {
|
|
121
|
+
if (this._columnInfoCache !== undefined) {
|
|
122
|
+
return this._columnInfoCache;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const table = this._extractTableFromSql();
|
|
126
|
+
|
|
127
|
+
if (this._adapter && table) {
|
|
128
|
+
try {
|
|
129
|
+
this._columnInfoCache = this._queryColumnMetadata(table);
|
|
130
|
+
return this._columnInfoCache;
|
|
131
|
+
} catch {
|
|
132
|
+
// Fall through to fallback
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this._columnInfoCache = this._fallbackColumnInfo();
|
|
137
|
+
return this._columnInfoCache;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Extract table name from a SQL query using simple regex. */
|
|
141
|
+
private _extractTableFromSql(): string | null {
|
|
142
|
+
if (!this._sql) return null;
|
|
143
|
+
|
|
144
|
+
let m = this._sql.match(/\bFROM\s+["']?(\w+)["']?/i);
|
|
145
|
+
if (m) return m[1];
|
|
146
|
+
|
|
147
|
+
m = this._sql.match(/\bINSERT\s+INTO\s+["']?(\w+)["']?/i);
|
|
148
|
+
if (m) return m[1];
|
|
149
|
+
|
|
150
|
+
m = this._sql.match(/\bUPDATE\s+["']?(\w+)["']?/i);
|
|
151
|
+
if (m) return m[1];
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Query the database adapter for column metadata. */
|
|
157
|
+
private _queryColumnMetadata(table: string): ColumnInfoResult[] {
|
|
158
|
+
if (!this._adapter) return this._fallbackColumnInfo();
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const rawCols: ColumnInfo[] = this._adapter.columns(table);
|
|
162
|
+
return this._normalizeColumns(rawCols);
|
|
163
|
+
} catch {
|
|
164
|
+
return this._fallbackColumnInfo();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Normalize adapter column info to standard format. */
|
|
169
|
+
private _normalizeColumns(rawCols: ColumnInfo[]): ColumnInfoResult[] {
|
|
170
|
+
return rawCols.map((col) => {
|
|
171
|
+
const colType = (col.type ?? "UNKNOWN").toUpperCase();
|
|
172
|
+
const [size, decimals] = this._parseTypeSize(colType);
|
|
173
|
+
return {
|
|
174
|
+
name: col.name,
|
|
175
|
+
type: colType.replace(/\(.*\)/, ""),
|
|
176
|
+
size,
|
|
177
|
+
decimals,
|
|
178
|
+
nullable: col.nullable ?? true,
|
|
179
|
+
primary_key: col.primaryKey ?? false,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Parse size and decimals from a type string like VARCHAR(255) or NUMERIC(10,2). */
|
|
185
|
+
private _parseTypeSize(typeStr: string): [number | null, number | null] {
|
|
186
|
+
const m = typeStr.match(/\((\d+)(?:\s*,\s*(\d+))?\)/);
|
|
187
|
+
if (m) {
|
|
188
|
+
const size = parseInt(m[1], 10);
|
|
189
|
+
const decimals = m[2] ? parseInt(m[2], 10) : null;
|
|
190
|
+
return [size, decimals];
|
|
191
|
+
}
|
|
192
|
+
return [null, null];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Derive basic column info from record keys when no adapter is available. */
|
|
196
|
+
private _fallbackColumnInfo(): ColumnInfoResult[] {
|
|
197
|
+
if (this.columns.length === 0) return [];
|
|
198
|
+
return this.columns.map((name) => ({
|
|
199
|
+
name,
|
|
200
|
+
type: "UNKNOWN",
|
|
201
|
+
size: null,
|
|
202
|
+
decimals: null,
|
|
203
|
+
nullable: true,
|
|
204
|
+
primary_key: false,
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -3,7 +3,7 @@ export type {
|
|
|
3
3
|
FieldDefinition,
|
|
4
4
|
ModelDefinition,
|
|
5
5
|
DatabaseAdapter,
|
|
6
|
-
DatabaseResult,
|
|
6
|
+
DatabaseResult as DatabaseWriteResult,
|
|
7
7
|
ColumnInfo,
|
|
8
8
|
QueryOptions,
|
|
9
9
|
RelationshipDefinition,
|
|
@@ -12,7 +12,9 @@ export type {
|
|
|
12
12
|
|
|
13
13
|
export { FetchResult } from "./types.js";
|
|
14
14
|
|
|
15
|
-
export {
|
|
15
|
+
export { DatabaseResult } from "./databaseResult.js";
|
|
16
|
+
export type { ColumnInfoResult } from "./databaseResult.js";
|
|
17
|
+
export { Database, initDatabase, getAdapter, setAdapter, closeDatabase, parseDatabaseUrl, setNamedAdapter, getNamedAdapter } from "./database.js";
|
|
16
18
|
export type { DatabaseConfig, ParsedDatabaseUrl } from "./database.js";
|
|
17
19
|
export { discoverModels } from "./model.js";
|
|
18
20
|
export type { DiscoveredModel } from "./model.js";
|
|
@@ -29,8 +31,9 @@ export {
|
|
|
29
31
|
removeMigrationRecord,
|
|
30
32
|
migrate,
|
|
31
33
|
createMigration,
|
|
34
|
+
status,
|
|
32
35
|
} from "./migration.js";
|
|
33
|
-
export type { MigrationResult } from "./migration.js";
|
|
36
|
+
export type { MigrationResult, MigrationStatus } from "./migration.js";
|
|
34
37
|
export { generateCrudRoutes } from "./autoCrud.js";
|
|
35
38
|
export { buildQuery, parseQueryString } from "./query.js";
|
|
36
39
|
export { validate } from "./validation.js";
|