tina4-nodejs 3.10.65 → 3.10.67
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 +28 -2
- package/package.json +1 -1
- package/packages/core/src/metrics.ts +16 -2
- package/packages/orm/src/baseModel.ts +14 -3
package/CLAUDE.md
CHANGED
|
@@ -153,13 +153,39 @@ Database layer with auto-CRUD generation, seeding, fake data, and SQL translatio
|
|
|
153
153
|
- `fakeData.ts` — ORM-aware fake data extending core (adds `forField()` with column-name heuristics)
|
|
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
|
-
- `
|
|
157
|
-
- `
|
|
156
|
+
- **Instance methods:** `save()`, `delete()`, `forceDelete()`, `restore()`, `load(sql, params?, include?): boolean` (selectOne into self), `validate(): string[]`, `toDict(include?): Record`, `toObject(): Record`, `toArray(): unknown[]`, `toList(): unknown[]`, `toJson(): string`, `hasOne()`, `hasMany()`, `belongsTo()`
|
|
157
|
+
- **Static methods:** `find(id, include?)`, `findById(id, include?)`, `findAll(where?, params?, include?)`, `findOrFail(id)`, `create(data)`, `select(sql, params?)`, `selectOne(sql, params?, include?)`, `count(conditions?, params?)`, `withTrashed(conditions?, params?)`, `scope(name, filterSql, params?)`, `createTable()`, `query(): QueryBuilder`
|
|
158
158
|
- QueryBuilder supports `toMongo()` for generating MongoDB query documents from the same fluent API
|
|
159
159
|
- `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
160
|
|
|
161
161
|
**`tina4_sequences` table** — Auto-created by `getNextId()` on first use for SQLite, MySQL, and MSSQL. Stores the current sequence value per table. Do not modify this table manually.
|
|
162
162
|
|
|
163
|
+
### File Uploads
|
|
164
|
+
|
|
165
|
+
Multipart file uploads are available via `req.files` (array of UploadedFile objects):
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// req.files[0] =>
|
|
169
|
+
{
|
|
170
|
+
fieldName: "avatar",
|
|
171
|
+
filename: "photo.png",
|
|
172
|
+
type: "image/png",
|
|
173
|
+
content: Buffer, // raw bytes — NOT base64
|
|
174
|
+
size: 102400
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
post("/api/upload", (req, res) => {
|
|
180
|
+
const file = req.files?.find(f => f.fieldName === "avatar");
|
|
181
|
+
if (!file) return res.json({ error: "No file" }, 400);
|
|
182
|
+
fs.writeFileSync(`src/public/uploads/${file.filename}`, file.content);
|
|
183
|
+
return res.json({ ok: true });
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Max upload size: `TINA4_MAX_UPLOAD_SIZE` env var (default 10MB).
|
|
188
|
+
|
|
163
189
|
### @tina4/swagger (`packages/swagger/`)
|
|
164
190
|
Auto-generates OpenAPI 3.0 docs.
|
|
165
191
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.67",
|
|
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"],
|
|
@@ -54,6 +54,9 @@ function relativePath(filePath: string, root: string = "."): string {
|
|
|
54
54
|
return path.relative(root, filePath);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Stores the resolved scan root so fileDetail() can locate framework files.
|
|
58
|
+
let _lastScanRoot = "";
|
|
59
|
+
|
|
57
60
|
// ── Test file detection ─────────────────────────────────────
|
|
58
61
|
|
|
59
62
|
function hasMatchingTest(relPath: string): boolean {
|
|
@@ -591,10 +594,13 @@ function detectViolations(
|
|
|
591
594
|
function resolveRoot(root: string = "src"): string {
|
|
592
595
|
const rootPath = path.resolve(root);
|
|
593
596
|
if (fs.existsSync(rootPath) && walkFiles(rootPath, [".ts", ".js"]).length > 0) {
|
|
597
|
+
_lastScanRoot = rootPath;
|
|
594
598
|
return root;
|
|
595
599
|
}
|
|
596
600
|
// Fallback: scan the framework package itself
|
|
597
|
-
|
|
601
|
+
const fwDir = path.resolve(path.dirname(new URL(import.meta.url).pathname));
|
|
602
|
+
_lastScanRoot = fwDir;
|
|
603
|
+
return fwDir;
|
|
598
604
|
}
|
|
599
605
|
|
|
600
606
|
// ── Quick Metrics ────────────────────────────────────────────
|
|
@@ -854,7 +860,15 @@ export function fullAnalysis(root: string = "src"): Record<string, any> {
|
|
|
854
860
|
// ── File Detail ──────────────────────────────────────────────
|
|
855
861
|
|
|
856
862
|
export function fileDetail(filePath: string): Record<string, any> {
|
|
857
|
-
|
|
863
|
+
let resolved = path.resolve(filePath);
|
|
864
|
+
if (!fs.existsSync(resolved) && _lastScanRoot) {
|
|
865
|
+
// Try resolving relative to the last scan root (framework mode)
|
|
866
|
+
const candidate = path.resolve(_lastScanRoot, filePath);
|
|
867
|
+
if (fs.existsSync(candidate)) {
|
|
868
|
+
resolved = candidate;
|
|
869
|
+
filePath = candidate;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
858
872
|
if (!fs.existsSync(resolved)) {
|
|
859
873
|
return { error: `File not found: ${filePath}` };
|
|
860
874
|
}
|
|
@@ -240,9 +240,20 @@ export class BaseModel {
|
|
|
240
240
|
return (this as unknown as typeof BaseModel).findById.call(this, id, include) as T | null;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
/**
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Load a record into this instance via selectOne.
|
|
245
|
+
* Returns true if found and loaded, false otherwise.
|
|
246
|
+
*/
|
|
247
|
+
load(sql: string, params?: unknown[], include?: string[]): boolean {
|
|
248
|
+
const ModelClass = this.constructor as typeof BaseModel & (new (data?: Record<string, unknown>) => BaseModel);
|
|
249
|
+
const result = ModelClass.selectOne(sql, params, include);
|
|
250
|
+
if (!result) return false;
|
|
251
|
+
const data = (result as any).toJSON ? (result as any).toJSON() : result;
|
|
252
|
+
for (const [key, value] of Object.entries(data)) {
|
|
253
|
+
(this as any)[key] = value;
|
|
254
|
+
}
|
|
255
|
+
(this as any)._exists = true;
|
|
256
|
+
return true;
|
|
246
257
|
}
|
|
247
258
|
|
|
248
259
|
/**
|