Skip to content

Commit bae97fd

Browse files
authored
Merge pull request #217 from enfyra/release/v1.4.0
chore: release v1.4.0 with WebSocket support and dependency updates
2 parents 87b281d + 5af286a commit bae97fd

File tree

105 files changed

+1725
-3820
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+1725
-3820
lines changed

data/snapshot.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,5 +460,42 @@
460460
{ "name": "description", "type": "text", "isNullable": true, "isSystem": true, "description": "Description of the AI configuration" }
461461
],
462462
"relations": []
463+
},
464+
"websocket_definition": {
465+
"name": "websocket_definition",
466+
"description": "Defines WebSocket gateway namespaces for real-time communication",
467+
"isSystem": true,
468+
"uniques": [["path"]],
469+
"columns": [
470+
{ "name": "id", "type": "int", "isPrimary": true, "isGenerated": true, "isNullable": false, "isSystem": true, "description": "Primary key identifier" },
471+
{ "name": "path", "type": "varchar", "isNullable": false, "isSystem": true, "defaultValue": null, "description": "WebSocket namespace path with pattern support (e.g., /chat/:roomId)" },
472+
{ "name": "isEnabled", "type": "boolean", "isNullable": false, "isSystem": true, "defaultValue": true, "description": "Whether the gateway is active" },
473+
{ "name": "isSystem", "type": "boolean", "isNullable": false, "isSystem": true, "defaultValue": false, "description": "Whether this is a system-defined gateway" },
474+
{ "name": "description", "type": "text", "isSystem": true, "description": "Description of the gateway's purpose" },
475+
{ "name": "requireAuth", "type": "boolean", "isNullable": false, "isSystem": true, "defaultValue": true, "description": "Whether authentication is required for connections" },
476+
{ "name": "connectionHandlerScript", "type": "code", "isNullable": true, "isSystem": true, "description": "JavaScript code to execute when client connects" },
477+
{ "name": "connectionHandlerTimeout", "type": "int", "isSystem": true, "isNullable": true, "description": "Timeout in milliseconds for connection handler execution. If not set, uses default system timeout." }
478+
],
479+
"relations": [
480+
{ "propertyName": "targetTables", "type": "many-to-many", "targetTable": "table_definition", "isSystem": true, "description": "Tables that this websocket gateway can access in handlers" }
481+
]
482+
},
483+
"websocket_event_definition": {
484+
"name": "websocket_event_definition",
485+
"description": "Defines event handlers for WebSocket gateways",
486+
"isSystem": true,
487+
"uniques": [["gateway", "eventName"]],
488+
"columns": [
489+
{ "name": "id", "type": "int", "isPrimary": true, "isGenerated": true, "isNullable": false, "isSystem": true, "description": "Primary key identifier" },
490+
{ "name": "eventName", "type": "varchar", "isNullable": false, "isSystem": true, "defaultValue": null, "description": "Event name to listen for (e.g., sendMessage, typing)" },
491+
{ "name": "isEnabled", "type": "boolean", "isNullable": false, "isSystem": true, "defaultValue": true, "description": "Whether the event handler is active" },
492+
{ "name": "isSystem", "type": "boolean", "isNullable": false, "isSystem": true, "defaultValue": false, "description": "Whether this is a system-defined event" },
493+
{ "name": "description", "type": "text", "isSystem": true, "description": "Description of what the event does" },
494+
{ "name": "handlerScript", "type": "code", "isSystem": true, "description": "JavaScript code to execute when event is received" },
495+
{ "name": "timeout", "type": "int", "isSystem": true, "isNullable": true, "description": "Timeout in milliseconds for event handler execution. If not set, uses default system timeout." }
496+
],
497+
"relations": [
498+
{ "propertyName": "gateway", "type": "many-to-one", "targetTable": "websocket_definition", "isSystem": true, "inversePropertyName": "events", "isNullable": false, "description": "Gateway this event belongs to" }
499+
]
463500
}
464501
}

package.json

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "enfyra-server",
3-
"version": "1.3.4",
3+
"version": "1.4.0",
44
"description": "Dynamic backend platform that auto-generates REST and GraphQL APIs from database schemas",
55
"author": "dothinh115 <dothinh115@gmail.com>",
66
"private": false,
@@ -35,23 +35,28 @@
3535
"@langchain/langgraph": "^1.0.2",
3636
"@langchain/openai": "^1.1.0",
3737
"@liaoliaots/nestjs-redis": "^10.0.0",
38+
"@nestjs/bullmq": "^11.0.4",
3839
"@nestjs/common": "^11.0.0",
3940
"@nestjs/config": "^4.0.2",
4041
"@nestjs/core": "^11.0.0",
4142
"@nestjs/jwt": "^11.0.0",
4243
"@nestjs/passport": "^11.0.5",
4344
"@nestjs/platform-express": "^11.0.0",
45+
"@nestjs/platform-socket.io": "^11.1.12",
4446
"@nestjs/schedule": "^6.0.1",
4547
"@nestjs/swagger": "^8.0.7",
48+
"@nestjs/websockets": "^11.1.12",
49+
"@types/ws": "^8.18.1",
4650
"bcryptjs": "^3.0.2",
51+
"bullmq": "^5.67.1",
4752
"class-transformer": "^0.5.1",
4853
"class-validator": "^0.14.2",
4954
"cors": "^2.8.5",
5055
"dotenv": "latest",
5156
"generic-pool": "^3.9.0",
5257
"graphql": "^16.11.0",
5358
"graphql-yoga": "^5.13.5",
54-
"ioredis": "^5.6.1",
59+
"ioredis": "^5.9.2",
5560
"jsonwebtoken": "^9.0.2",
5661
"knex": "^3.1.0",
5762
"lodash": "^4.17.21",
@@ -65,8 +70,11 @@
6570
"reflect-metadata": "^0.2.0",
6671
"rxjs": "^7.8.1",
6772
"sharp": "^0.34.3",
73+
"socket.io": "^4.8.3",
74+
"socket.io-redis": "^6.1.1",
6875
"sqlite3": "^5.1.7",
6976
"uuid": "^11.1.0",
77+
"ws": "^8.19.0",
7078
"zod": "^4.1.12"
7179
},
7280
"devDependencies": {
@@ -85,6 +93,7 @@
8593
"eslint-plugin-prettier": "^5.0.0",
8694
"jest": "^30.0.5",
8795
"prettier": "^3.0.0",
96+
"socket.io-client": "^4.8.3",
8897
"source-map-support": "^0.5.21",
8998
"supertest": "^7.0.0",
9099
"ts-jest": "^29.1.0",
@@ -93,28 +102,6 @@
93102
"tsconfig-paths": "^4.2.0",
94103
"typescript": "^5.1.3"
95104
},
96-
"jest": {
97-
"moduleFileExtensions": [
98-
"js",
99-
"json",
100-
"ts"
101-
],
102-
"rootDir": ".",
103-
"testMatch": [
104-
"<rootDir>/test/**/*.spec.ts"
105-
],
106-
"transform": {
107-
"^.+\\.(t|j)s$": "ts-jest"
108-
},
109-
"collectCoverageFrom": [
110-
"src/**/*.(t|j)s",
111-
"!src/**/*.spec.ts",
112-
"!src/**/*.interface.ts",
113-
"!src/**/*.dto.ts"
114-
],
115-
"coverageDirectory": "coverage",
116-
"testEnvironment": "node"
117-
},
118105
"resolutions": {
119106
"string-width": "4.2.3",
120107
"strip-ansi": "6.0.1",

scripts/init-db-mongo.ts

Lines changed: 2 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import {
99
RelationDef,
1010
TableDef,
1111
} from '../src/shared/types/database-init.types';
12-
1312
dotenv.config();
14-
1513
function getBsonType(columnDef: ColumnDef): string {
1614
const typeMap: Record<string, string> = {
1715
int: 'int',
@@ -32,19 +30,14 @@ function getBsonType(columnDef: ColumnDef): string {
3230
'array-select': 'array',
3331
enum: 'string',
3432
};
35-
3633
return typeMap[columnDef.type] || 'string';
3734
}
38-
3935
function createValidationSchema(tableDef: TableDef, allTables: Record<string, TableDef>): any {
4036
const properties: any = {};
4137
const required: string[] = [];
42-
4338
for (const col of tableDef.columns) {
4439
if (col.isPrimary && col.name === 'id') continue;
45-
4640
const bsonType = getBsonType(col);
47-
4841
if (col.isNullable !== false) {
4942
properties[col.name] = { bsonType: [bsonType, 'null'] };
5043
} else {
@@ -53,22 +46,18 @@ function createValidationSchema(tableDef: TableDef, allTables: Record<string, Ta
5346
required.push(col.name);
5447
}
5548
}
56-
5749
if (col.type === 'enum' && Array.isArray(col.options)) {
5850
properties[col.name].enum = col.options;
5951
}
60-
6152
if (col.description) {
6253
properties[col.name].description = col.description;
6354
}
6455
}
65-
6656
if (tableDef.relations) {
6757
for (const rel of tableDef.relations) {
6858
if (rel.type === 'one-to-many') {
6959
continue;
7060
}
71-
7261
if (rel.type === 'many-to-one' || rel.type === 'one-to-one') {
7362
properties[rel.propertyName] = {
7463
bsonType: ['objectId', 'null'],
@@ -83,41 +72,34 @@ function createValidationSchema(tableDef: TableDef, allTables: Record<string, Ta
8372
}
8473
}
8574
}
86-
8775
const schema: any = {
8876
$jsonSchema: {
8977
bsonType: 'object',
9078
properties,
9179
},
9280
};
93-
9481
if (required.length > 0) {
9582
schema.$jsonSchema.required = required;
9683
}
97-
9884
return schema;
9985
}
100-
10186
async function createIndexes(
10287
db: Db,
10388
collectionName: string,
10489
tableDef: TableDef,
10590
): Promise<void> {
10691
const collection = db.collection(collectionName);
107-
10892
if (tableDef.uniques && tableDef.uniques.length > 0) {
10993
for (const uniqueGroup of tableDef.uniques) {
11094
if (Array.isArray(uniqueGroup) && uniqueGroup.length > 0) {
11195
const indexSpec: any = {};
11296
for (const fieldName of uniqueGroup) {
11397
indexSpec[fieldName] = 1;
11498
}
115-
11699
const partialFilter: any = {};
117100
for (const fieldName of uniqueGroup) {
118101
partialFilter[fieldName] = { $type: 'string' };
119102
}
120-
121103
await collection.createIndex(indexSpec, {
122104
unique: true,
123105
partialFilterExpression: partialFilter,
@@ -126,7 +108,6 @@ async function createIndexes(
126108
}
127109
}
128110
}
129-
130111
if (tableDef.indexes && tableDef.indexes.length > 0) {
131112
for (const indexGroup of tableDef.indexes) {
132113
if (Array.isArray(indexGroup) && indexGroup.length > 0) {
@@ -139,18 +120,14 @@ async function createIndexes(
139120
}
140121
}
141122
}
142-
143-
// Create indexes for datetime/timestamp fields (for sorting/filtering)
144123
const dateTimeFields = tableDef.columns.filter(col =>
145124
col.type === 'datetime' || col.type === 'timestamp' || col.type === 'date'
146125
);
147-
148126
for (const field of dateTimeFields) {
149127
const indexName = `${collectionName}_${field.name}_idx`;
150-
151128
try {
152129
await collection.createIndex(
153-
{ [field.name]: -1 }, // -1 for descending (most recent first)
130+
{ [field.name]: -1 },
154131
{ name: indexName }
155132
);
156133
console.log(` Created index on datetime field: ${field.name}`);
@@ -162,14 +139,10 @@ async function createIndexes(
162139
}
163140
}
164141
}
165-
166-
// Create compound index for createdAt + updatedAt (common pattern)
167142
const hasCreatedAt = tableDef.columns.some(col => col.name === 'createdAt');
168143
const hasUpdatedAt = tableDef.columns.some(col => col.name === 'updatedAt');
169-
170144
if (hasCreatedAt && hasUpdatedAt) {
171145
const indexName = `${collectionName}_timestamps_idx`;
172-
173146
try {
174147
await collection.createIndex(
175148
{ createdAt: -1, updatedAt: -1 },
@@ -184,13 +157,11 @@ async function createIndexes(
184157
}
185158
}
186159
}
187-
188160
if (tableDef.relations) {
189161
for (const relation of tableDef.relations) {
190162
if (relation.type === 'many-to-one' || relation.type === 'one-to-one') {
191163
const fieldName = relation.propertyName;
192164
const indexName = `${collectionName}_${fieldName}_fk_idx`;
193-
194165
try {
195166
await collection.createIndex(
196167
{ [fieldName]: 1 },
@@ -205,11 +176,9 @@ async function createIndexes(
205176
}
206177
}
207178
}
208-
209179
if (relation.type === 'many-to-many' && !relation.mappedBy) {
210180
const fieldName = relation.propertyName;
211181
const indexName = `${collectionName}_${fieldName}_fk_idx`;
212-
213182
try {
214183
await collection.createIndex(
215184
{ [fieldName]: 1 },
@@ -227,76 +196,55 @@ async function createIndexes(
227196
}
228197
}
229198
}
230-
231-
232199
async function createCollection(db: Db, tableDef: TableDef, allTables: Record<string, TableDef>): Promise<void> {
233200
const collectionName = tableDef.name;
234-
235201
console.log(`📝 Creating collection: ${collectionName}`);
236-
237202
const collections = await db.listCollections({ name: collectionName }).toArray();
238203
if (collections.length > 0) {
239204
console.log(`⏩ Collection already exists: ${collectionName}`);
240205
return;
241206
}
242-
243207
const METADATA_TABLES = ['table_definition', 'column_definition', 'relation_definition'];
244-
245208
if (METADATA_TABLES.includes(collectionName)) {
246209
await db.createCollection(collectionName);
247210
console.log(`✅ Created collection (no validation): ${collectionName}`);
248211
} else {
249212
const validationSchema = createValidationSchema(tableDef, allTables);
250-
251213
await db.createCollection(collectionName, {
252214
validator: validationSchema,
253215
validationLevel: 'moderate',
254216
validationAction: 'error',
255217
});
256218
console.log(`✅ Created collection (with validation): ${collectionName}`);
257219
}
258-
259220
await createIndexes(db, collectionName, tableDef);
260221
}
261-
262222
export async function initializeDatabaseMongo(): Promise<void> {
263223
const MONGO_URI = process.env.MONGO_URI;
264-
265224
if (!MONGO_URI) {
266225
throw new Error('MONGO_URI is not defined in environment variables');
267226
}
268-
269227
console.log('🚀 Initializing MongoDB database...');
270-
271228
const client = new MongoClient(MONGO_URI);
272-
273229
try {
274230
await client.connect();
275231
console.log('✅ Connected to MongoDB');
276-
277232
const dbName = MONGO_URI.match(/\/([^/?]+)(\?|$)/)?.[1] || 'enfyra';
278233
const db = client.db(dbName);
279-
280234
const settingCollection = db.collection('setting_definition');
281235
const existingSettings = await settingCollection.findOne({ isInit: true });
282-
283236
if (existingSettings) {
284237
console.log('⚠️ Database already initialized, skipping init.');
285238
return;
286239
}
287-
288240
const snapshotPath = path.resolve(process.cwd(), 'data/snapshot.json');
289241
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8'));
290-
291242
console.log('📖 Loaded snapshot.json');
292-
293243
const tables = Object.values(snapshot) as TableDef[];
294244
console.log(`📊 Found ${tables.length} collections to create`);
295-
296245
for (const tableDef of tables) {
297246
await createCollection(db, tableDef, snapshot);
298247
}
299-
300248
console.log('🎉 MongoDB database initialization completed!');
301249
} catch (error) {
302250
console.error('❌ Error during MongoDB initialization:', error);
@@ -305,7 +253,6 @@ export async function initializeDatabaseMongo(): Promise<void> {
305253
await client.close();
306254
}
307255
}
308-
309256
if (require.main === module) {
310257
initializeDatabaseMongo()
311258
.then(() => {
@@ -316,6 +263,4 @@ if (require.main === module) {
316263
console.error('❌ Failed:', error);
317264
process.exit(1);
318265
});
319-
}
320-
321-
266+
}

0 commit comments

Comments
 (0)