@fuzionx/framework 0.1.73 → 0.1.75

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/cli/index.js CHANGED
@@ -103,9 +103,15 @@ function _knexColumnBuilder(t, col, def) {
103
103
  c.notNullable();
104
104
  }
105
105
 
106
- // 기본값
106
+ // 기본값 — SQL 함수 표현식(now() 등)은 knex.raw()로 전달
107
107
  if (def.default !== undefined) {
108
- c.defaultTo(def.default);
108
+ const val = def.default;
109
+ if (typeof val === 'string' && /^\w+\(.*\)$/.test(val)) {
110
+ // SQL 함수 표현식 (now(), uuid_generate_v4() 등)
111
+ c.defaultTo(t.client.raw(val));
112
+ } else {
113
+ c.defaultTo(val);
114
+ }
109
115
  } else if ((def.type === 'integer' || def.type === 'bigInteger') && !def.nullable) {
110
116
  c.defaultTo(0); // non-nullable integer만 0 기본값 (nullable FK는 NULL 유지)
111
117
  } else if (def.type === 'boolean') {
@@ -902,6 +908,197 @@ export async function run(args) {
902
908
  return;
903
909
  }
904
910
 
911
+ // ── fx db:fresh — 테이블 전체 삭제 → 재생성 → 시드 ──
912
+ // ── fx db:reset — db:fresh 별칭 ──
913
+ if (command === 'db:fresh' || command === 'db:reset') {
914
+ const confirm = rest.includes('--confirm');
915
+ const skipSeed = rest.includes('--no-seed');
916
+
917
+ if (!confirm) {
918
+ console.log(`
919
+ ⚠️ 이 명령은 모든 테이블을 삭제하고 재생성합니다.
920
+ 데이터가 모두 삭제됩니다!
921
+
922
+ 실행하려면 --confirm 플래그를 추가하세요:
923
+ fx ${command} --confirm
924
+
925
+ 시드를 건너뛰려면:
926
+ fx ${command} --confirm --no-seed
927
+ `);
928
+ process.exit(0);
929
+ }
930
+
931
+ try {
932
+ // ── 1. DB 설정 로드 ──
933
+ const { default: Config } = await import('../lib/core/Config.js');
934
+ const config = new Config({});
935
+ config.loadYaml(path.resolve('fuzionx.yaml'));
936
+ const dbConfig = config.get('database') || {};
937
+ const defaultConn = dbConfig.default || 'main';
938
+ const connConfig = dbConfig.connections?.[defaultConn] || {};
939
+ const driver = (connConfig.driver || 'sqlite').toLowerCase();
940
+
941
+ console.log(`\n🔥 DB Fresh Start (driver: ${driver})\n`);
942
+
943
+ if (driver === 'sqlite') {
944
+ // SQLite — DB 파일 삭제
945
+ const dbPath = path.resolve(connConfig.database || connConfig.path || './storage/database.sqlite');
946
+ try {
947
+ await fs.unlink(dbPath);
948
+ console.log(` 🗑️ Deleted: ${dbPath}`);
949
+ } catch (e) {
950
+ if (e.code !== 'ENOENT') throw e;
951
+ console.log(` ⚠️ SQLite file not found — clean state.`);
952
+ }
953
+
954
+ } else if (driver === 'postgres' || driver === 'postgresql') {
955
+ // PostgreSQL — 모든 테이블 CASCADE DROP
956
+ const knexModule = _cwdRequire('knex');
957
+ const knexInit = typeof knexModule === 'function' ? knexModule : knexModule.knex || knexModule.default;
958
+ const db = knexInit({
959
+ client: 'pg',
960
+ connection: {
961
+ host: connConfig.host || '127.0.0.1',
962
+ port: connConfig.port || 5432,
963
+ user: connConfig.user || 'postgres',
964
+ password: connConfig.password || '',
965
+ database: connConfig.database || '',
966
+ },
967
+ pool: { min: 1, max: 3 },
968
+ });
969
+
970
+ // public 스키마의 모든 테이블 조회
971
+ const result = await db.raw(`
972
+ SELECT tablename FROM pg_tables
973
+ WHERE schemaname = 'public'
974
+ ORDER BY tablename
975
+ `);
976
+ const tables = result.rows.map(r => r.tablename);
977
+
978
+ if (tables.length === 0) {
979
+ console.log(' ⚠️ 테이블이 없습니다 — clean state.');
980
+ } else {
981
+ console.log(` 🗑️ ${tables.length}개 테이블 삭제 중...\n`);
982
+ for (const table of tables) {
983
+ await db.raw(`DROP TABLE IF EXISTS "${table}" CASCADE`);
984
+ console.log(` ✗ ${table}`);
985
+ }
986
+ console.log(`\n ✅ ${tables.length}개 테이블 삭제 완료.`);
987
+ }
988
+
989
+ // 시퀀스도 정리
990
+ const seqResult = await db.raw(`
991
+ SELECT relname FROM pg_class
992
+ WHERE relkind = 'S' AND relnamespace = (
993
+ SELECT oid FROM pg_namespace WHERE nspname = 'public'
994
+ )
995
+ `);
996
+ const sequences = seqResult.rows.map(r => r.relname);
997
+ if (sequences.length > 0) {
998
+ for (const seq of sequences) {
999
+ await db.raw(`DROP SEQUENCE IF EXISTS "${seq}" CASCADE`);
1000
+ }
1001
+ console.log(` 🔗 ${sequences.length}개 시퀀스 삭제.`);
1002
+ }
1003
+
1004
+ await db.destroy();
1005
+
1006
+ } else if (driver === 'mariadb' || driver === 'mysql') {
1007
+ // MariaDB/MySQL — 모든 테이블 CASCADE DROP
1008
+ const knexModule = _cwdRequire('knex');
1009
+ const knexInit = typeof knexModule === 'function' ? knexModule : knexModule.knex || knexModule.default;
1010
+ const db = knexInit({
1011
+ client: 'mysql2',
1012
+ connection: {
1013
+ host: connConfig.host || '127.0.0.1',
1014
+ port: connConfig.port || 3306,
1015
+ user: connConfig.user || 'root',
1016
+ password: connConfig.password || '',
1017
+ database: connConfig.database || '',
1018
+ },
1019
+ pool: { min: 1, max: 3 },
1020
+ });
1021
+
1022
+ const [rows] = await db.raw(`SHOW TABLES`);
1023
+ const dbName = connConfig.database || '';
1024
+ const tables = rows.map(r => Object.values(r)[0]);
1025
+
1026
+ if (tables.length === 0) {
1027
+ console.log(' ⚠️ 테이블이 없습니다 — clean state.');
1028
+ } else {
1029
+ await db.raw('SET FOREIGN_KEY_CHECKS = 0');
1030
+ console.log(` 🗑️ ${tables.length}개 테이블 삭제 중...\n`);
1031
+ for (const table of tables) {
1032
+ await db.raw(`DROP TABLE IF EXISTS \`${table}\``);
1033
+ console.log(` ✗ ${table}`);
1034
+ }
1035
+ await db.raw('SET FOREIGN_KEY_CHECKS = 1');
1036
+ console.log(`\n ✅ ${tables.length}개 테이블 삭제 완료.`);
1037
+ }
1038
+
1039
+ await db.destroy();
1040
+ }
1041
+
1042
+ // ── 2. MongoDB 컬렉션 삭제 ──
1043
+ const mongoConfig = dbConfig.connections?.mongo || dbConfig.connections?.mongodb;
1044
+ if (mongoConfig) {
1045
+ try {
1046
+ const host = mongoConfig.host || '127.0.0.1';
1047
+ const port = mongoConfig.port || 27017;
1048
+ const dbName = mongoConfig.database || 'fuzionx';
1049
+ const authSource = mongoConfig.authSource || dbName;
1050
+ let mongoUri = mongoConfig.uri || mongoConfig.url;
1051
+ if (!mongoUri) {
1052
+ if (mongoConfig.user && mongoConfig.password) {
1053
+ mongoUri = `mongodb://${encodeURIComponent(String(mongoConfig.user))}:${encodeURIComponent(String(mongoConfig.password))}@${host}:${port}/${dbName}?authSource=${authSource}`;
1054
+ } else {
1055
+ mongoUri = `mongodb://${host}:${port}/${dbName}`;
1056
+ }
1057
+ }
1058
+
1059
+ const { MongoClient } = await import('mongodb').catch(() => ({ MongoClient: _cwdRequire('mongodb').MongoClient }));
1060
+ const mongoClient = new MongoClient(mongoUri, { serverSelectionTimeoutMS: 5000 });
1061
+ await mongoClient.connect();
1062
+ const mongoDB = mongoClient.db(dbName);
1063
+
1064
+ const collections = await mongoDB.listCollections().toArray();
1065
+ const userCollections = collections.filter(c => !c.name.startsWith('system.'));
1066
+
1067
+ if (userCollections.length > 0) {
1068
+ console.log(`\n 🗑️ MongoDB: ${userCollections.length}개 컬렉션 삭제 중...\n`);
1069
+ for (const coll of userCollections) {
1070
+ await mongoDB.dropCollection(coll.name).catch(() => {});
1071
+ console.log(` ✗ ${coll.name}`);
1072
+ }
1073
+ console.log(`\n ✅ MongoDB ${userCollections.length}개 컬렉션 삭제 완료.`);
1074
+ } else {
1075
+ console.log('\n ⚠️ MongoDB 컬렉션이 없습니다.');
1076
+ }
1077
+
1078
+ await mongoClient.close();
1079
+ } catch (mongoErr) {
1080
+ console.warn(` ⚠️ MongoDB 초기화 실패: ${mongoErr.message}`);
1081
+ }
1082
+ }
1083
+
1084
+ // ── 3. 재생성: db:sync --apply ──
1085
+ console.log('\n\n📊 테이블 재생성 (db:sync --apply)...\n');
1086
+ const { execSync } = await import('node:child_process');
1087
+ execSync('npx fx db:sync --apply', { stdio: 'inherit', cwd: process.cwd() });
1088
+
1089
+ // ── 4. 시드 투입 ──
1090
+ if (!skipSeed) {
1091
+ console.log('\n\n🌱 시드 데이터 투입 (db:seed)...\n');
1092
+ execSync('npx fx db:seed', { stdio: 'inherit', cwd: process.cwd() });
1093
+ }
1094
+
1095
+ console.log('\n✅ DB Fresh 완료!\n');
1096
+ } catch (err) {
1097
+ console.error('db:fresh error:', err.message, err.stack);
1098
+ }
1099
+ process.exit(0);
1100
+ }
1101
+
905
1102
  // ── fx db:seed — 시드 데이터 투입 ──
906
1103
  if (command === 'db:seed') {
907
1104
  try {
@@ -953,6 +1150,17 @@ export async function run(args) {
953
1150
  // CLI 자체 SqlModel에도 주입 (혹시 직접 참조하는 경우 대비)
954
1151
  const SqlModel = (await import('../lib/database/SqlModel.js')).default;
955
1152
  SqlModel.setConnectionManager(cm);
1153
+
1154
+ // 프로젝트의 node_modules/@fuzionx/framework 가 별도 복사본(non-symlink)일 수 있으므로
1155
+ // 프로젝트 경로의 SqlModel/PostgreModel 에도 직접 주입
1156
+ try {
1157
+ const projFwPath = path.resolve('node_modules/@fuzionx/framework/lib/database');
1158
+ const projSqlModel = (await import(pathToFileURL(path.join(projFwPath, 'SqlModel.js')).href)).default;
1159
+ if (projSqlModel && typeof projSqlModel.setConnectionManager === 'function') {
1160
+ projSqlModel.setConnectionManager(cm);
1161
+ }
1162
+ } catch {}
1163
+
956
1164
  }
957
1165
 
958
1166
  // 시드 실행
@@ -990,5 +1198,7 @@ export async function run(args) {
990
1198
  fx config Print fuzionx.yaml
991
1199
  fx db:sync Sync models → DB (--apply)
992
1200
  fx db:seed Run seed files (database/seeds)
1201
+ fx db:fresh --confirm Drop all tables → sync → seed
1202
+ fx db:reset --confirm Alias for db:fresh
993
1203
  `);
994
1204
  }
@@ -9,7 +9,7 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "@fuzionx/player": "^0.1.73",
12
+ "@fuzionx/player": "^0.1.75",
13
13
  "pinia": "^3.0.4",
14
14
  "vue": "^3.5.0",
15
15
  "vue-router": "^4.5.0"
@@ -978,6 +978,11 @@ export default class Application {
978
978
  inst.onDisconnect(socket);
979
979
  });
980
980
 
981
+ // 원격 메타데이터 변경 이벤트 (Hub METADATA_UPDATE → onRemoteMetadataUpdate)
982
+ wsNs.on('metadata_update', (socket, key, value) => {
983
+ inst.onRemoteMetadataUpdate(socket, key, value);
984
+ });
985
+
981
986
  this.logger.info(`[WS] 핸들러 등록: ${namespace} (events: ${[...eventMap.keys()].join(', ')})`);
982
987
  }
983
988
  }
@@ -104,4 +104,16 @@ export default class WsHandler extends Base {
104
104
  async onError(socket, error) {
105
105
  this.logger?.error(`WS error in ${this.constructor.namespace}:`, error);
106
106
  }
107
+
108
+ /**
109
+ * 원격 세션 메타데이터 변경 시 (서브클래스에서 오버라이드)
110
+ *
111
+ * Hub METADATA_UPDATE → 다른 워커에서 setMetadata 호출 시 발생.
112
+ * 접속자 관리 등 cross-worker 상태 동기화에 사용.
113
+ *
114
+ * @param {object} socket - 원격 세션 소켓 래퍼 (send → Hub DIRECT)
115
+ * @param {string} key - 변경된 메타데이터 키
116
+ * @param {string} value - 변경된 메타데이터 값
117
+ */
118
+ async onRemoteMetadataUpdate(socket, key, value) {}
107
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzionx/framework",
3
- "version": "0.1.73",
3
+ "version": "0.1.75",
4
4
  "type": "module",
5
5
  "description": "Full-stack MVC framework built on @fuzionx/core — Controller, Service, Model, Middleware, DI, EventBus",
6
6
  "main": "index.js",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@aws-sdk/client-s3": "^3.1028.0",
38
- "@fuzionx/core": "^0.1.73",
38
+ "@fuzionx/core": "^0.1.75",
39
39
  "better-sqlite3": "^12.8.0",
40
40
  "knex": "^3.2.5",
41
41
  "mongoose": "^9.3.2",