@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 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
- if (isDirectory) {
19
- // Charger les fichiers *.file.ts
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
- // Charger aussi les fichiers *.model.ts avec _isFileCollection_
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 crypto from "crypto";
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
- const id = crypto.randomUUID();
261
- const storage = getStorageForCollection(collection, tenant_id);
262
- const subpath = col.storage?.path || undefined;
263
- const { path: filepath, size } = await storage.save(tenant_id, collection, file, { id, mimetype, subpath });
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 = `${id}${ext}`;
267
-
268
- // Enregistrer les métadonnées en base si trackMetaData est activé
269
- if (col.trackMetaData !== false) {
270
- try {
271
- const rest = new useRest({ tenant_id, internal: false, useHook: false, useCustomApi: false });
272
- await rest.insertOne(collection, {
273
- _id: id,
274
- _file: {
275
- filename,
276
- name: file.name,
277
- mimetype,
278
- size,
279
- url: `/files/${tenant_id}/${collection}/${filename}`,
280
- },
281
- ...(data || {}),
282
- });
283
- } catch (err) {
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
- // Réplication vers les destinations configurées
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: id,
340
+ _id,
300
341
  _file: {
301
342
  filename,
302
343
  name: file.name,
303
344
  mimetype,
304
345
  size,
305
- url: `/files/${tenant_id}/${collection}/${filename}`,
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
- if (col?.trackMetaData !== false) {
362
- try {
363
- const rest = new useRest({ tenant_id, internal: true, useHook: false, useCustomApi: false });
364
- const doc = await rest.findOne<any>(collection, fileId);
365
- filename = doc?._file?.filename;
366
- } catch {}
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 metadata document from MongoDB
384
- if (col?.trackMetaData !== false) {
385
- try {
386
- const rest = new useRest({ tenant_id, internal: true, useHook: false, useCustomApi: false });
387
- await rest.deleteOne(collection, fileId);
388
- } catch (err: any) {
389
- console.error('Failed to delete file metadata:', err?.message || err);
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.2",
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.25",
23
+ "hono": "^4.12.27",
24
24
  "ioredis": "^5.11.1",
25
- "joi": "^18.2.1",
25
+ "joi": "^18.2.3",
26
26
  "jose": "^6.2.3",
27
27
  "mongodb": "6.21.0",
28
28
  "socket.io": "^4.8.3",