@anteros/core 0.0.17 โ†’ 0.0.19

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.
@@ -84,23 +84,35 @@ async function initializeOnDatabase(collections: Collection[]) {
84
84
  }
85
85
 
86
86
  if (field?.index) {
87
- let indexValue: Boolean | number = field.index;
88
- let includesStringType: Field['type'][] = ['string', 'email', 'enum', 'url', 'random', 'slug'];
89
- let includesDateType: Field['type'][] = ['date', 'datetime-local'];
90
- if (typeof indexValue == "boolean" && includesStringType.includes(field.type)) {
91
- indexValue = 1
87
+ let indexValue: number | string = 1;
88
+
89
+ // Honor explicit indexType first
90
+ if (field.indexType) {
91
+ indexValue = field.indexType;
92
+ } else if (typeof field.index === 'number') {
93
+ indexValue = field.index;
94
+ } else if (field.type === 'geojson.Point' || field.type === 'geojson.LineString' || field.type === 'geojson.Polygon') {
95
+ indexValue = '2dsphere';
96
+ } else {
97
+ let includesStringType: Field['type'][] = ['string', 'email', 'enum', 'url', 'random', 'slug', 'relationship', 'uuid', 'password'];
98
+ let includesDateType: Field['type'][] = ['date', 'datetime-local'];
99
+ if (includesStringType.includes(field.type)) {
100
+ indexValue = 1;
101
+ } else if (includesDateType.includes(field.type)) {
102
+ indexValue = -1;
103
+ }
92
104
  }
93
105
 
94
- if (typeof indexValue == "boolean" && includesDateType.includes(field.type)) {
95
- indexValue = -1
96
- }
106
+ let isSparse = field.type === 'random'
107
+ || (field.unique && field.required !== true);
108
+
97
109
  specsFieldsIndexes.push({
98
110
  key: {
99
- [field.name]: indexValue as number
111
+ [field.name]: indexValue as any
100
112
  },
101
113
  unique: field.unique ?? false,
102
- sparse: field.type == 'random' ? true : false,
103
- ...field.indexOptions,
114
+ sparse: isSparse,
115
+ ...field.indexOptions, // explicit overrides win
104
116
  })
105
117
 
106
118
  }
@@ -111,6 +123,22 @@ async function initializeOnDatabase(collections: Collection[]) {
111
123
  if (specsFieldsIndexes.length) {
112
124
  await db.collection(collection.slug).createIndexes(specsFieldsIndexes)
113
125
  }
126
+
127
+ // Purge orphan indexes (fields removed from schema)
128
+ if (collection.purgeOrphanIndexes) {
129
+ const expectedKeys = new Set(specsFieldsIndexes.map((idx) =>
130
+ JSON.stringify(Object.keys(idx.key!).sort())
131
+ ));
132
+ const existingIndexes = await db.collection(collection.slug).listIndexes().toArray();
133
+ for (const idx of existingIndexes) {
134
+ if (idx.name === '_id_') continue; // built-in
135
+ const idxKeys = JSON.stringify(Object.keys(idx.key!).sort());
136
+ if (!expectedKeys.has(idxKeys)) {
137
+ await db.collection(collection.slug).dropIndex(idx.name!);
138
+ console.log(`๐Ÿงน Dropped orphan index: ${collection.slug}.${idx.name}`.gray);
139
+ }
140
+ }
141
+ }
114
142
  }
115
143
 
116
144
  } catch (err: any) {
@@ -72,76 +72,61 @@ class MongoRest {
72
72
  })
73
73
 
74
74
 
75
- //let unauthorizedKeys = func.unauthorizedKeys(options.data, getCollectionKeys(col))
76
- /* if (unauthorizedKeys.length > 0) {
77
- throw new AppError(`Unauthorized keys in "${options.collection}" : ${unauthorizedKeys.join(', ')}`, {
78
- code: 'UNAUTHORIZED_KEYS',
79
- status: 400
80
- })
81
- } */
82
-
83
-
84
-
85
75
  if (schema && partialSchema) {
86
76
 
87
77
 
88
78
  if (options?.data) {
89
79
  if (options.action == 'insertMany') {
90
- options.data.map(d => {
91
- validationResult = schema.validate(d, {
92
- allowUnknown: false,
93
- })
94
- if (validationResult && validationResult.error) {
80
+ for (let i = 0; i < options.data.length; i++) {
81
+ validationResult = schema.validate(options.data[i], { allowUnknown: false })
82
+ if (validationResult?.error) {
95
83
  throw new AppError(validationResult.error.message, {
96
84
  code: 'VALIDATION_ERROR',
97
85
  status: 400
98
86
  })
99
87
  }
100
- })
88
+ // Apply Joi defaults (e.g., .default() values) back to data
89
+ options.data[i] = validationResult.value
90
+ }
101
91
  }
102
92
 
103
93
  if (options.action == 'insertOne') {
104
- validationResult = schema.validate(options.data, {
105
- allowUnknown: false,
106
- })
94
+ validationResult = schema.validate(options.data, { allowUnknown: false })
95
+ if (validationResult?.error) {
96
+ throw new AppError(validationResult.error.message, {
97
+ code: 'VALIDATION_ERROR',
98
+ status: 400
99
+ })
100
+ }
101
+ // Persist Joi defaults back to data
102
+ if (validationResult.value) options.data = validationResult.value
107
103
  }
108
104
  }
109
105
 
110
106
 
111
107
  if (options?.update) {
112
- if (options.action == 'insertOne' && options.update?.$set) {
113
- validationResult = partialSchema.validate(options.update?.$set, {
114
- allowUnknown: false,
115
- })
116
- }
117
-
118
- if (options.action == 'insertMany' && options.update?.$set) {
119
- validationResult = partialSchema.validate(options.update?.$set, {
120
- allowUnknown: false,
121
- })
108
+ if ((options.action == 'insertOne' || options.action == 'insertMany') && options.update?.$set) {
109
+ validationResult = partialSchema.validate(options.update.$set, { allowUnknown: false })
122
110
  }
123
111
 
124
112
  if (options.action == "findOneAndUpdate" && options.update.$set) {
125
- validationResult = partialSchema.validate(options.update.$set, {
126
- allowUnknown: false,
127
- })
113
+ validationResult = partialSchema.validate(options.update.$set, { allowUnknown: false })
128
114
  }
129
115
 
130
116
  if (options.action == "findOneAndUpdate" && options.update.$setOnInsert) {
131
- validationResult = partialSchema.validate(options.update.$setOnInsert, {
132
- allowUnknown: false,
133
- })
117
+ validationResult = partialSchema.validate(options.update.$setOnInsert, { allowUnknown: false })
134
118
  }
135
- }
136
119
 
137
- if (validationResult?.error) {
138
- throw new AppError(validationResult.error.message, {
139
- code: 'VALIDATION_ERROR',
140
- status: 400
141
- })
120
+ if (validationResult?.error) {
121
+ throw new AppError(validationResult.error.message, {
122
+ code: 'VALIDATION_ERROR',
123
+ status: 400
124
+ })
125
+ }
142
126
  }
143
127
 
144
128
  }
129
+ }
145
130
 
146
131
 
147
132
 
@@ -371,18 +356,19 @@ class MongoRest {
371
356
  data = await func.buildInput(data as T, {
372
357
  action: action, col: col, rest: this
373
358
  })
374
- this._validate({ collection, action: action, data: data })
375
359
 
376
360
  const meta: any = { action, collection, data }
377
361
  if (col.hooks?.beforeOperation) {
378
362
  await col.hooks.beforeOperation({ rest: this, io, action, meta })
379
363
  }
364
+ // Validate AFTER hooks (they may fill required fields or apply Joi defaults)
365
+ this._validate({ collection, action: action, data: meta.data })
380
366
 
381
- await this.db.collection(collection).insertOne(func.toBson(data, { col }) as any, {
367
+ await this.db.collection(collection).insertOne(func.toBson(meta.data, { col }) as any, {
382
368
  session: this.session,
383
369
  })
384
370
 
385
- const result: T & { _id: string } = func.toJson(data as T & { _id: string })
371
+ const result: T & { _id: string } = func.toJson(meta.data as T & { _id: string })
386
372
  meta.result = result
387
373
  if (col?.hooks?.afterOperation) {
388
374
  await col.hooks.afterOperation({ rest: this, io, action, meta })
@@ -409,18 +395,18 @@ class MongoRest {
409
395
  dataInput.push(d)
410
396
  }
411
397
 
412
- this._validate({ collection, action: action, data: dataInput })
413
-
414
398
  const meta: any = { action, collection, data: dataInput }
415
399
  if (col.hooks?.beforeOperation) {
416
400
  await col.hooks.beforeOperation({ rest: this, io, action, meta })
417
401
  }
402
+ // Validate AFTER hooks (they may fill required fields or apply Joi defaults)
403
+ this._validate({ collection, action: action, data: meta.data })
418
404
 
419
- await this.db.collection(collection).insertMany(func.toBson(dataInput, { col }) as any, {
405
+ await this.db.collection(collection).insertMany(func.toBson(meta.data, { col }) as any, {
420
406
  session: this.session,
421
407
  })
422
408
 
423
- const result: (T & { _id: string })[] = func.toJson(dataInput as (T & { _id: string })[])
409
+ const result: (T & { _id: string })[] = func.toJson(meta.data as (T & { _id: string })[])
424
410
  meta.result = result
425
411
 
426
412
  if (col.hooks?.afterOperation) {
@@ -443,13 +429,15 @@ class MongoRest {
443
429
  }
444
430
 
445
431
  update = await func.buildInput(update)
446
- this._validate({ collection, action: action, update: update })
447
- update = func.toBson(update, { col })
448
432
 
449
433
  const meta: any = { action, collection, update, id: _id }
450
434
  if (col.hooks?.beforeOperation) {
451
435
  await col.hooks.beforeOperation({ rest: this, io, action, meta })
452
436
  }
437
+ // Validate and toBson AFTER hooks
438
+ update = meta.update;
439
+ this._validate({ collection, action: action, update: update })
440
+ update = func.toBson(update, { col })
453
441
 
454
442
  const data = await this.db.collection(collection).findOneAndUpdate(
455
443
  { _id: new ObjectId(_id) },
@@ -505,15 +493,17 @@ class MongoRest {
505
493
  }
506
494
 
507
495
 
508
- this._validate({ collection, action: action, update: update.$set })
509
- this._validate({ collection, action: action, update: update.$setOnInsert })
510
- update = func.toBson(update, { col })
511
- filter = func.toBson(filter, { col })
512
-
513
496
  const meta: any = { action, collection, filter, update }
514
497
  if (col.hooks?.beforeOperation) {
515
498
  await col.hooks.beforeOperation({ rest: this, io, action, meta })
516
499
  }
500
+ // Validate and toBson AFTER hooks
501
+ update = meta.update;
502
+ filter = meta.filter;
503
+ this._validate({ collection, action: action, update: update.$set })
504
+ this._validate({ collection, action: action, update: update.$setOnInsert })
505
+ update = func.toBson(update, { col })
506
+ filter = func.toBson(filter, { col })
517
507
 
518
508
  const data = await this.db.collection(collection).findOneAndUpdate(
519
509
  filter,
@@ -546,13 +536,15 @@ class MongoRest {
546
536
  update.$set = await func.buildInput(update.$set, { action: action, col: col })
547
537
  }
548
538
  update = await func.buildInput(update)
549
- this._validate({ collection, action: action, update: update })
550
- update = func.toBson(update, { col })
551
539
 
552
540
  const meta: any = { action, collection, update, ids: _ids }
553
541
  if (col.hooks?.beforeOperation) {
554
542
  await col.hooks.beforeOperation({ rest: this, io, action, meta })
555
543
  }
544
+ // Validate and toBson AFTER hooks
545
+ update = meta.update;
546
+ this._validate({ collection, action: action, update: update })
547
+ update = func.toBson(update, { col })
556
548
 
557
549
  const result = await this.db.collection(collection).updateMany(
558
550
  { _id: { $in: _ids.map(id => new ObjectId(id)) } },
package/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { define } from "./lib/define";
4
4
  import { bootApp } from "./server/boot";
5
5
  import { useRest } from "./database/rest";
6
+ import { AppError } from "./lib/error";
6
7
  import * as v from "joi";
7
8
  import utils from "./utils";
8
9
  import * as crypto from "./utils/crypto";
@@ -25,6 +26,7 @@ export {
25
26
  define,
26
27
  app,
27
28
  useRest,
29
+ AppError,
28
30
  v,
29
31
  utils,
30
32
  cache,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@anteros/core",
3
3
  "module": "index.ts",
4
- "version": "0.0.17",
4
+ "version": "0.0.19",
5
5
  "type": "module",
6
6
  "description": "Anteros Core",
7
7
  "private": false,
package/server/boot.ts CHANGED
@@ -17,6 +17,19 @@ import { loadServices } from '../lib/services'
17
17
  import { loadSockets } from '../lib/sockets'
18
18
  import { syncWorkflows } from '../lib/workflow'
19
19
  import { loadTenantsMiddlewares } from '../lib/middleware'
20
+
21
+ /** Check if a TCP port is already bound */
22
+ async function isPortInUse(port: number): Promise<boolean> {
23
+ try {
24
+ const proc = Bun.spawn(['lsof', '-ti', `:${port}`], { stdout: 'pipe' });
25
+ const output = await new Response(proc.stdout).text();
26
+ await proc.exited;
27
+ return output.trim().length > 0;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
20
33
  type BootAppOptions = ServerConfig & {
21
34
 
22
35
  }
@@ -38,17 +51,51 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
38
51
  //******************************* */
39
52
 
40
53
 
54
+ // JWT_SECRET check: auto-generate if missing and auth is enabled
55
+ const hasAuth = cfg.collections?.some((c: any) => c.api?.auth?.enabled);
56
+ let jwtSecret = cfg.server.jwt?.secret || Bun.env.JWT_SECRET;
57
+ if (hasAuth && !jwtSecret) {
58
+ jwtSecret = Array.from({ length: 64 }, () =>
59
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'[Math.floor(Math.random() * 64)]
60
+ ).join('');
61
+ try {
62
+ const envPath = '.env';
63
+ const exists = await Bun.file(envPath).exists();
64
+ const content = exists ? await Bun.file(envPath).text() : '';
65
+ if (!content.includes('JWT_SECRET=')) {
66
+ const suffix = content ? '\n' : '';
67
+ await Bun.write(envPath, `${content}${suffix}# Auto-generated on boot\nJWT_SECRET=${jwtSecret}\n`);
68
+ }
69
+ Bun.env.JWT_SECRET = jwtSecret;
70
+ console.log('๐Ÿ” JWT_SECRET auto-generated and saved to .env'.gray);
71
+ } catch {
72
+ Bun.env.JWT_SECRET = jwtSecret;
73
+ console.log('โš  JWT_SECRET auto-generated (in-memory only, could not write .env)'.yellow);
74
+ }
75
+ }
76
+
41
77
  const PORT = (cfg.server.port || 4000);
42
78
  const NAME = cfg.server.name || process.env.APP_NAME || 'SERVER';
43
79
  const useClusterMode = cfg.clusterMode || false;
44
80
  const env = process.env.NODE_ENV || Bun.env.NODE_ENV || 'dev';
81
+ // reusePort: cluster mode ON by default, dev mode OFF to detect zombies
82
+ const reusePort = cfg.server.reusePort ?? (useClusterMode && env !== 'dev');
83
+
84
+ // Detect zombie: check if port is already in use in dev mode
85
+ if (!reusePort && env === 'dev') {
86
+ const occupied = await isPortInUse(PORT);
87
+ if (occupied) {
88
+ console.error(`\nโš  Port ${PORT} is already in use. Possible zombie server.\n โ†’ Kill it: lsof -ti :${PORT} | xargs kill -9\n โ†’ Or set server.reusePort: true in config\n`.red.bold);
89
+ process.exit(1);
90
+ }
91
+ }
45
92
 
46
93
 
47
94
  const app = createApp();
48
95
 
49
96
  const server = Bun.serve({
50
97
  port: PORT,
51
- reusePort: useClusterMode,
98
+ reusePort,
52
99
  fetch: (req, server) => {
53
100
  const url = new URL(req.url);
54
101
 
@@ -70,6 +70,8 @@ export type Collection = {
70
70
  */
71
71
  _tenant_?: string;
72
72
  _isTimeSerie_?: boolean;
73
+ /** Drop MongoDB indexes for fields no longer in the schema (default: false) */
74
+ purgeOrphanIndexes?: boolean;
73
75
  _isCollection_?: boolean;
74
76
  _schema_?: Joi.Schema;
75
77
  _schemaPartial_?: Joi.Schema;
package/types/config.d.ts CHANGED
@@ -13,6 +13,7 @@ export type ServerConfig = {
13
13
  server: {
14
14
  name?: string;
15
15
  port: number;
16
+ reusePort?: boolean; // override Bun's reusePort (default: clusterMode && not dev)
16
17
  body?: {
17
18
  maxSize?: number;
18
19
  };
package/utils/func.ts CHANGED
@@ -372,7 +372,10 @@ function formatToObjectId<T>(value: T): FormatToObjectIdResult<T> {
372
372
  return value as FormatToObjectIdResult<T>;
373
373
  }
374
374
 
375
-
375
+ // Pass through primitives and Date instances untouched
376
+ if (value instanceof Date) {
377
+ return value as FormatToObjectIdResult<T>;
378
+ }
376
379
 
377
380
  if (typeof value === 'string') {
378
381
 
@@ -530,7 +533,7 @@ async function buildInput<T>(data: any | any[], options?: FormatInputOptions): P
530
533
 
531
534
  /* Set Default Value For Insert */
532
535
  if (options?.action?.match(/(insert)/)) {
533
- if (Object.hasOwn(f, 'defaultValue') && !dataValue) {
536
+ if (Object.hasOwn(f, 'defaultValue') && (dataValue === undefined || dataValue === null)) {
534
537
  data[fieldName] = f.defaultValue
535
538
  }
536
539
  }
@@ -608,7 +611,15 @@ function collectDateFieldPathsFromCollection(col: Collection): string[] {
608
611
 
609
612
  function toBson<T>(data: any | any[], _options?: FormatInputOptions): T {
610
613
  let data_ = formatToObjectId(data);
611
- data_ = formatToDate(data_);
614
+
615
+ // Build omitKeys for fields that should NOT be date-converted (random, number, integer)
616
+ const omitKeys: string[] = [];
617
+ for (const f of _options?.col?.fields ?? []) {
618
+ if (f.type === 'random' || f.type === 'number' || f.type === 'integer') {
619
+ omitKeys.push(f.name);
620
+ }
621
+ }
622
+ data_ = formatToDate(data_, { omitKeys: omitKeys.length > 0 ? omitKeys : undefined });
612
623
 
613
624
  if (Array.isArray(data)) {
614
625
  data.length = 0;