@anteros/core 0.0.2 → 0.0.3
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/database/file.ts +21 -7
- package/lib/files.ts +91 -49
- package/package.json +3 -3
- package/server/api.ts +327 -519
- package/server/config.ts +1 -1
- package/tests/api.test.ts +0 -8
- package/types/api.d.ts +0 -1
- package/types/collection.d.ts +13 -18
package/database/file.ts
CHANGED
|
@@ -10,13 +10,11 @@ async function syncFileCollections() {
|
|
|
10
10
|
const fileCollections: FileCollection[] = []
|
|
11
11
|
for (let tenant of cfg.tenants ?? []) {
|
|
12
12
|
const TENANT_PATH = path.join(process.cwd(), tenant.dir)
|
|
13
|
-
const FILES_PATH = path.join(TENANT_PATH, 'files')
|
|
14
|
-
let exist = await fs.exists(FILES_PATH)
|
|
15
|
-
if (!exist) continue;
|
|
16
|
-
const isDirectory = await (await fs.stat(FILES_PATH)).isDirectory()
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
// Scan files/ directory
|
|
15
|
+
const FILES_PATH = path.join(TENANT_PATH, 'files')
|
|
16
|
+
const filesExist = await fs.exists(FILES_PATH).catch(() => false)
|
|
17
|
+
if (filesExist && (await fs.stat(FILES_PATH)).isDirectory()) {
|
|
20
18
|
const globFiles = new Glob(path.join(FILES_PATH, '**/*.file.ts'))
|
|
21
19
|
for await (let file of globFiles.scan('.')) {
|
|
22
20
|
let fileModule = await import(file)
|
|
@@ -28,7 +26,7 @@ async function syncFileCollections() {
|
|
|
28
26
|
}
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
//
|
|
29
|
+
// Also scan *.model.ts in files/ with _isFileCollection_
|
|
32
30
|
const globModels = new Glob(path.join(FILES_PATH, '**/*.model.ts'))
|
|
33
31
|
for await (let file of globModels.scan('.')) {
|
|
34
32
|
let fileModule = await import(file)
|
|
@@ -40,6 +38,22 @@ async function syncFileCollections() {
|
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
40
|
}
|
|
41
|
+
|
|
42
|
+
// Also scan collections/ directory for *.file.ts
|
|
43
|
+
const COLLECTIONS_PATH = path.join(TENANT_PATH, 'collections')
|
|
44
|
+
const collectionsExist = await fs.exists(COLLECTIONS_PATH).catch(() => false)
|
|
45
|
+
if (collectionsExist && (await fs.stat(COLLECTIONS_PATH)).isDirectory()) {
|
|
46
|
+
const globFiles = new Glob(path.join(COLLECTIONS_PATH, '**/*.file.ts'))
|
|
47
|
+
for await (let file of globFiles.scan('.')) {
|
|
48
|
+
let fileModule = await import(file)
|
|
49
|
+
if (fileModule?.default?._isFileCollection_) {
|
|
50
|
+
fileCollections.push({
|
|
51
|
+
...fileModule?.default,
|
|
52
|
+
_tenant_: tenant.id
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
cfg.fileCollections = fileCollections
|
package/lib/files.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { FileCollection } from "../types/file";
|
|
|
6
6
|
import path from "path";
|
|
7
7
|
import fs from "fs/promises";
|
|
8
8
|
import { createReadStream, existsSync, mkdirSync } from "fs";
|
|
9
|
-
import
|
|
9
|
+
import { ObjectId } from "mongodb";
|
|
10
10
|
|
|
11
11
|
// ─── Storage Interface ──────────────────────────────────────────────────────
|
|
12
12
|
|
|
@@ -257,38 +257,79 @@ export async function handleUpload(options: UploadOptions): Promise<FileResult>
|
|
|
257
257
|
});
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
260
|
+
// Insert metadata first to get the MongoDB auto-generated ObjectId
|
|
261
|
+
let _id: string;
|
|
262
|
+
try {
|
|
263
|
+
// Coerce relation field IDs from string to ObjectId before insert.
|
|
264
|
+
// Form-data / JSON always sends IDs as strings, but MongoDB expects
|
|
265
|
+
// ObjectId for fields that reference other collections.
|
|
266
|
+
const insertDoc: Record<string, any> = {
|
|
267
|
+
_file: {
|
|
268
|
+
filename: '',
|
|
269
|
+
name: file.name,
|
|
270
|
+
mimetype,
|
|
271
|
+
size: 0,
|
|
272
|
+
url: '',
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
if (data) {
|
|
276
|
+
const relationFields =
|
|
277
|
+
col.fields?.filter(
|
|
278
|
+
(f) => f.type === 'relationship' && f.relation?.to && data[f.name] !== undefined && data[f.name] !== null,
|
|
279
|
+
) ?? []
|
|
280
|
+
for (const [key, value] of Object.entries(data)) {
|
|
281
|
+
const field = relationFields.find((f) => f.name === key)
|
|
282
|
+
if (field) {
|
|
283
|
+
const hasMany = field.relation?.hasMany ?? false
|
|
284
|
+
if (hasMany && Array.isArray(value)) {
|
|
285
|
+
insertDoc[key] = value.map((id) =>
|
|
286
|
+
ObjectId.isValid(id) ? new ObjectId(id) : id,
|
|
287
|
+
)
|
|
288
|
+
} else if (!hasMany) {
|
|
289
|
+
insertDoc[key] =
|
|
290
|
+
typeof value === 'string' && ObjectId.isValid(value)
|
|
291
|
+
? new ObjectId(value)
|
|
292
|
+
: value
|
|
293
|
+
} else {
|
|
294
|
+
insertDoc[key] = value
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
insertDoc[key] = value
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const rest = new useRest({ tenant_id, internal: false, useHook: false, useCustomApi: false });
|
|
302
|
+
const result = await rest.db.collection(collection).insertOne(insertDoc);
|
|
303
|
+
_id = String(result.insertedId);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
throw new AppError('Failed to save file metadata', {
|
|
306
|
+
code: 'FILE_METADATA_ERROR', status: 500,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
264
309
|
|
|
310
|
+
// Save the physical file to storage
|
|
265
311
|
const ext = path.extname(file.name);
|
|
266
|
-
const filename = `${
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
await storage.delete(tenant_id, collection, id, filename).catch(() => {});
|
|
285
|
-
throw new AppError('Failed to save file metadata', {
|
|
286
|
-
code: 'FILE_METADATA_ERROR', status: 500,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
312
|
+
const filename = `${_id}${ext}`;
|
|
313
|
+
const storage = getStorageForCollection(collection, tenant_id);
|
|
314
|
+
const subpath = col.storage?.path || undefined;
|
|
315
|
+
const { path: filepath, size } = await storage.save(tenant_id, collection, file, { id: _id, mimetype, subpath });
|
|
316
|
+
|
|
317
|
+
// Update MongoDB with actual file info
|
|
318
|
+
const url = `/files/${tenant_id}/${collection}/${filename}`;
|
|
319
|
+
try {
|
|
320
|
+
const rest = new useRest({ tenant_id, internal: true, useHook: false, useCustomApi: false });
|
|
321
|
+
await rest.db.collection(collection).updateOne(
|
|
322
|
+
{ _id: new ObjectId(_id) },
|
|
323
|
+
{ $set: { '_file.filename': filename, '_file.size': size, '_file.url': url } },
|
|
324
|
+
);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
await storage.delete(tenant_id, collection, _id, filename, subpath).catch(() => {});
|
|
327
|
+
throw new AppError('Failed to update file metadata', {
|
|
328
|
+
code: 'FILE_METADATA_ERROR', status: 500,
|
|
329
|
+
});
|
|
289
330
|
}
|
|
290
331
|
|
|
291
|
-
//
|
|
332
|
+
// Replicate to configured destinations
|
|
292
333
|
if (col.replicate?.length) {
|
|
293
334
|
replicateFile(filepath, filename, col.replicate).catch(err => {
|
|
294
335
|
console.error('Replication failed:', err?.message || err);
|
|
@@ -296,13 +337,13 @@ export async function handleUpload(options: UploadOptions): Promise<FileResult>
|
|
|
296
337
|
}
|
|
297
338
|
|
|
298
339
|
return {
|
|
299
|
-
_id
|
|
340
|
+
_id,
|
|
300
341
|
_file: {
|
|
301
342
|
filename,
|
|
302
343
|
name: file.name,
|
|
303
344
|
mimetype,
|
|
304
345
|
size,
|
|
305
|
-
url
|
|
346
|
+
url,
|
|
306
347
|
},
|
|
307
348
|
...(data || {}),
|
|
308
349
|
};
|
|
@@ -355,23 +396,24 @@ export async function handleDelete(
|
|
|
355
396
|
): Promise<void> {
|
|
356
397
|
const storage = getStorageForCollection(collection, tenant_id);
|
|
357
398
|
const col = getFileCollection(collection, tenant_id);
|
|
399
|
+
const subpath = col?.storage?.path || undefined;
|
|
358
400
|
|
|
359
|
-
// Look up the document to get the stored filename
|
|
401
|
+
// Look up the document in MongoDB to get the stored filename
|
|
360
402
|
let filename: string | undefined;
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
403
|
+
try {
|
|
404
|
+
const rest = new useRest({ tenant_id, internal: true, useHook: false, useCustomApi: false });
|
|
405
|
+
const doc = await rest.db.collection(collection).findOne(
|
|
406
|
+
{ _id: ObjectId.isValid(fileId) ? new ObjectId(fileId) : fileId },
|
|
407
|
+
{ projection: { '_file.filename': 1 } },
|
|
408
|
+
);
|
|
409
|
+
filename = doc?._file?.filename;
|
|
410
|
+
} catch {}
|
|
368
411
|
|
|
369
412
|
// Delete the physical file from storage
|
|
370
|
-
const subpath = col?.storage?.path || undefined;
|
|
371
413
|
if (filename) {
|
|
372
414
|
await storage.delete(tenant_id, collection, fileId, filename, subpath);
|
|
373
415
|
} else {
|
|
374
|
-
// Fallback: try common extensions
|
|
416
|
+
// Fallback: try common extensions using the fileId as base name
|
|
375
417
|
for (const ext of ['.jpg', '.jpeg', '.png', '.webp', '.avif', '.gif', '.svg', '.pdf', '.mp4', '.webm', '.mp3', '.wav', '.json', '.csv', '.zip', '']) {
|
|
376
418
|
try {
|
|
377
419
|
await storage.delete(tenant_id, collection, fileId, fileId + ext, subpath);
|
|
@@ -380,14 +422,14 @@ export async function handleDelete(
|
|
|
380
422
|
}
|
|
381
423
|
}
|
|
382
424
|
|
|
383
|
-
// Delete the
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
425
|
+
// Delete the document from MongoDB
|
|
426
|
+
try {
|
|
427
|
+
const rest = new useRest({ tenant_id, internal: true, useHook: false, useCustomApi: false });
|
|
428
|
+
await rest.db.collection(collection).deleteOne({
|
|
429
|
+
_id: ObjectId.isValid(fileId) ? new ObjectId(fileId) : fileId,
|
|
430
|
+
});
|
|
431
|
+
} catch (err: any) {
|
|
432
|
+
console.error('Failed to delete file metadata:', err?.message || err);
|
|
391
433
|
}
|
|
392
434
|
}
|
|
393
435
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anteros/core",
|
|
3
3
|
"module": "index.ts",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Anteros Core",
|
|
7
7
|
"private": false,
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"bentocache": "^1.6.1",
|
|
21
21
|
"boxen": "^8.0.1",
|
|
22
22
|
"dayjs": "^1.11.21",
|
|
23
|
-
"hono": "^4.12.
|
|
23
|
+
"hono": "^4.12.27",
|
|
24
24
|
"ioredis": "^5.11.1",
|
|
25
|
-
"joi": "^18.2.
|
|
25
|
+
"joi": "^18.2.3",
|
|
26
26
|
"jose": "^6.2.3",
|
|
27
27
|
"mongodb": "6.21.0",
|
|
28
28
|
"socket.io": "^4.8.3",
|