@anteros/core 0.0.18 โ 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.
- package/database/collection.ts +39 -11
- package/database/mongodbadapter.ts +49 -57
- package/package.json +1 -1
- package/server/boot.ts +48 -1
- package/types/collection.d.ts +2 -0
- package/types/config.d.ts +1 -0
- package/utils/func.ts +13 -2
package/database/collection.ts
CHANGED
|
@@ -84,23 +84,35 @@ async function initializeOnDatabase(collections: Collection[]) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
if (field?.index) {
|
|
87
|
-
let indexValue:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
indexValue =
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
111
|
+
[field.name]: indexValue as any
|
|
100
112
|
},
|
|
101
113
|
unique: field.unique ?? false,
|
|
102
|
-
sparse:
|
|
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.
|
|
91
|
-
validationResult = schema.validate(
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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(
|
|
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(
|
|
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/package.json
CHANGED
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
|
|
98
|
+
reusePort,
|
|
52
99
|
fetch: (req, server) => {
|
|
53
100
|
const url = new URL(req.url);
|
|
54
101
|
|
package/types/collection.d.ts
CHANGED
|
@@ -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
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
|
|
|
@@ -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
|
-
|
|
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;
|