@restforgejs/platform 5.2.16 → 5.3.5

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.
Files changed (199) hide show
  1. package/build-info.json +2 -2
  2. package/cli/consumer-deploy.js +1 -1
  3. package/cli/consumer.js +1 -1
  4. package/generators/cli/endpoint/create.js +69 -6
  5. package/generators/cli/payload/sync.js +16 -6
  6. package/generators/cli/project/auth.js +2 -2
  7. package/generators/cli/project/sdk.js +112 -0
  8. package/generators/lib/arg-parser.js +6 -0
  9. package/generators/lib/auth/processor-generator.js +5 -3
  10. package/generators/lib/auth/templates/processor/google.js.tmpl +178 -0
  11. package/generators/lib/auth/templates/processor/login.js.tmpl +8 -8
  12. package/generators/lib/auth/templates/processor/logout.js.tmpl +2 -2
  13. package/generators/lib/auth/templates/processor/me.js.tmpl +2 -2
  14. package/generators/lib/auth/templates/processor/refresh.js.tmpl +6 -6
  15. package/generators/lib/auth/templates/processor/register.js.tmpl +4 -4
  16. package/generators/lib/auth/templates/processor/reset-password.js.tmpl +7 -7
  17. package/generators/lib/auth/templates/rfx_auth.js.tmpl +3 -0
  18. package/generators/lib/generators/model-generator.js +46 -59
  19. package/generators/lib/help-generator.js +41 -3
  20. package/generators/lib/payload/endpoint-schema-validator.js +8 -3
  21. package/generators/lib/payload/field-projections.js +116 -0
  22. package/generators/lib/payload/payload-runner.js +164 -48
  23. package/generators/lib/payload/schema-diff.js +108 -0
  24. package/generators/lib/sdk/generator.js +719 -0
  25. package/generators/lib/sdk/naming.js +48 -0
  26. package/generators/lib/sdk/runtime/README.md.tmpl +207 -0
  27. package/generators/lib/sdk/runtime/auth-client.js +186 -0
  28. package/generators/lib/sdk/runtime/deploy.mjs.tmpl +85 -0
  29. package/generators/lib/sdk/runtime/http-client.js +81 -0
  30. package/generators/lib/sdk/runtime/resource-client.js +59 -0
  31. package/generators/lib/sdk/runtime/storage.js +31 -0
  32. package/generators/lib/templates/dashboard-catalog.js +1 -1
  33. package/generators/lib/templates/db-connection-env.js +1 -1
  34. package/generators/lib/templates/dbschema-catalog.js +1 -1
  35. package/generators/lib/templates/field-validation-catalog.js +1 -1
  36. package/generators/lib/templates/mysql-template.js +1 -1
  37. package/generators/lib/templates/oracle-template.js +1 -1
  38. package/generators/lib/templates/postgres-template.js +1 -1
  39. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  40. package/generators/lib/templates/sqlite-template.js +1 -1
  41. package/generators/lib/utils/cli-output.js +40 -0
  42. package/generators/lib/utils/config-resolver.js +61 -0
  43. package/generators/lib/utils/database-introspector.js +28 -5
  44. package/integrity-manifest.json +18 -18
  45. package/package.json +1 -1
  46. package/scripts/verify-integrity.js +1 -1
  47. package/server.js +1 -1
  48. package/src/components/handlers/adjust_handler.js +1 -1
  49. package/src/components/handlers/audit_handler.js +1 -1
  50. package/src/components/handlers/delete_handler.js +1 -1
  51. package/src/components/handlers/export_handler.js +1 -1
  52. package/src/components/handlers/import_handler.js +1 -1
  53. package/src/components/handlers/insert_handler.js +1 -1
  54. package/src/components/handlers/update_handler.js +1 -1
  55. package/src/components/handlers/upload_handler.js +1 -1
  56. package/src/components/handlers/workflow_handler.js +1 -1
  57. package/src/components/integrations/webhook.js +1 -1
  58. package/src/consumers/baseConsumer.js +1 -1
  59. package/src/consumers/declarativeMapper.js +1 -1
  60. package/src/consumers/handlers/apiHandler.js +1 -1
  61. package/src/consumers/handlers/consoleHandler.js +1 -1
  62. package/src/consumers/handlers/databaseHandler.js +1 -1
  63. package/src/consumers/handlers/index.js +1 -1
  64. package/src/consumers/handlers/kafkaHandler.js +1 -1
  65. package/src/consumers/index.js +1 -1
  66. package/src/consumers/messageTransformer.js +1 -1
  67. package/src/consumers/validator.js +1 -1
  68. package/src/core/db/dialect/base-dialect.js +1 -1
  69. package/src/core/db/dialect/index.js +1 -1
  70. package/src/core/db/dialect/mysql-dialect.js +1 -1
  71. package/src/core/db/dialect/oracle-dialect.js +1 -1
  72. package/src/core/db/dialect/postgres-dialect.js +1 -1
  73. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  74. package/src/core/db/flatten-helper.js +1 -1
  75. package/src/core/db/query-builder-error.js +1 -1
  76. package/src/core/db/query-builder.js +1 -1
  77. package/src/core/db/relation-helper.js +1 -1
  78. package/src/core/handlers/delete_handler.js +1 -1
  79. package/src/core/handlers/insert_handler.js +1 -1
  80. package/src/core/handlers/update_handler.js +1 -1
  81. package/src/core/models/base-model.js +1 -1
  82. package/src/core/utils/cache-manager.js +1 -1
  83. package/src/core/utils/component-engine.js +1 -1
  84. package/src/core/utils/context-builder.js +1 -1
  85. package/src/core/utils/datetime-formatter.js +1 -1
  86. package/src/core/utils/datetime-parser.js +1 -1
  87. package/src/core/utils/db.js +1 -1
  88. package/src/core/utils/logger.js +1 -1
  89. package/src/core/utils/payload-loader.js +1 -1
  90. package/src/core/utils/security-checks.js +1 -1
  91. package/src/middleware/body-options.js +1 -1
  92. package/src/middleware/cors.js +1 -1
  93. package/src/middleware/idempotency.js +1 -1
  94. package/src/middleware/rate-limiter.js +1 -1
  95. package/src/middleware/request-logger.js +1 -1
  96. package/src/middleware/security-headers.js +1 -1
  97. package/src/models/base-model-mysql.js +1 -1
  98. package/src/models/base-model-oracle.js +1 -1
  99. package/src/models/base-model-sqlite.js +1 -1
  100. package/src/models/base-model.js +1 -1
  101. package/src/pro/caching/redis-client.js +1 -1
  102. package/src/pro/caching/redis-helper.js +1 -1
  103. package/src/pro/consumers/baseConsumer.js +1 -1
  104. package/src/pro/consumers/declarativeMapper.js +1 -1
  105. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  106. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  107. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  108. package/src/pro/consumers/handlers/index.js +1 -1
  109. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  110. package/src/pro/consumers/index.js +1 -1
  111. package/src/pro/consumers/messageTransformer.js +1 -1
  112. package/src/pro/consumers/validator.js +1 -1
  113. package/src/pro/database/base-model-mysql.js +1 -1
  114. package/src/pro/database/base-model-oracle.js +1 -1
  115. package/src/pro/database/base-model-sqlite.js +1 -1
  116. package/src/pro/database/db-mysql.js +1 -1
  117. package/src/pro/database/db-oracle.js +1 -1
  118. package/src/pro/database/db-sqlite.js +1 -1
  119. package/src/pro/excel/excel-generator.js +1 -1
  120. package/src/pro/excel/excel-parser.js +1 -1
  121. package/src/pro/excel/export-service.js +1 -1
  122. package/src/pro/excel/export_handler.js +1 -1
  123. package/src/pro/excel/import-service.js +1 -1
  124. package/src/pro/excel/import-validator.js +1 -1
  125. package/src/pro/excel/import_handler.js +1 -1
  126. package/src/pro/excel/upsert-builder.js +1 -1
  127. package/src/pro/idgen/idgen-routes.js +1 -1
  128. package/src/pro/integrations/lookup-resolver.js +1 -1
  129. package/src/pro/integrations/upload-handler-v2.js +1 -1
  130. package/src/pro/integrations/upload-handler.js +1 -1
  131. package/src/pro/integrations/webhook.js +1 -1
  132. package/src/pro/locking/lock-routes.js +1 -1
  133. package/src/pro/locking/resource-lock-manager.js +1 -1
  134. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  135. package/src/pro/messaging/kafkaService.js +1 -1
  136. package/src/pro/messaging/messagehubService.js +1 -1
  137. package/src/pro/messaging/rabbitmqService.js +1 -1
  138. package/src/pro/scheduler/job-manager.js +1 -1
  139. package/src/pro/scheduler/job-routes.js +1 -1
  140. package/src/pro/scheduler/job-validator.js +1 -1
  141. package/src/pro/storage/base-storage-provider.js +1 -1
  142. package/src/pro/storage/file-metadata-helper.js +1 -1
  143. package/src/pro/storage/index.js +1 -1
  144. package/src/pro/storage/local-storage-provider.js +1 -1
  145. package/src/pro/storage/s3-storage-provider.js +1 -1
  146. package/src/pro/storage/upload-cleanup-job.js +1 -1
  147. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  148. package/src/pro/storage/upload-pending-tracker.js +1 -1
  149. package/src/pro/websocket/broadcast-helper.js +1 -1
  150. package/src/pro/websocket/index.js +1 -1
  151. package/src/pro/websocket/livesync-server.js +1 -1
  152. package/src/pro/websocket/ws-broadcaster.js +1 -1
  153. package/src/services/export-service.js +1 -1
  154. package/src/services/import-service.js +1 -1
  155. package/src/services/kafkaConsumerService.js +1 -1
  156. package/src/services/kafkaService.js +1 -1
  157. package/src/services/messagehubService.js +1 -1
  158. package/src/services/rabbitmqService.js +1 -1
  159. package/src/utils/cache-invalidation-registry.js +1 -1
  160. package/src/utils/cache-manager.js +1 -1
  161. package/src/utils/component-engine.js +1 -1
  162. package/src/utils/config-extractor.js +1 -1
  163. package/src/utils/consumerLogger.js +1 -1
  164. package/src/utils/context-builder.js +1 -1
  165. package/src/utils/dashboard-helpers.js +1 -1
  166. package/src/utils/dateHelper.js +1 -1
  167. package/src/utils/datetime-formatter.js +1 -1
  168. package/src/utils/datetime-parser.js +1 -1
  169. package/src/utils/db-bootstrap.js +1 -1
  170. package/src/utils/db-mysql.js +1 -1
  171. package/src/utils/db-oracle.js +1 -1
  172. package/src/utils/db-sqlite.js +1 -1
  173. package/src/utils/db.js +1 -1
  174. package/src/utils/demo-generator.js +1 -1
  175. package/src/utils/excel-generator.js +1 -1
  176. package/src/utils/excel-parser.js +1 -1
  177. package/src/utils/file-watcher.js +1 -1
  178. package/src/utils/id-generator.js +1 -1
  179. package/src/utils/idempotency-manager.js +1 -1
  180. package/src/utils/import-validator.js +1 -1
  181. package/src/utils/license-client.js +1 -1
  182. package/src/utils/lock-manager.js +1 -1
  183. package/src/utils/logger.js +1 -1
  184. package/src/utils/lookup-resolver.js +1 -1
  185. package/src/utils/payload-loader.js +1 -1
  186. package/src/utils/processor-response.js +1 -1
  187. package/src/utils/rabbitmq.js +1 -1
  188. package/src/utils/redis-client.js +1 -1
  189. package/src/utils/redis-helper.js +1 -1
  190. package/src/utils/request-scope.js +1 -1
  191. package/src/utils/security-checks.js +1 -1
  192. package/src/utils/service-resolver.js +1 -1
  193. package/src/utils/shutdown-coordinator.js +1 -1
  194. package/src/utils/soft-delete-dashboard-guard.js +1 -1
  195. package/src/utils/sql-table-extractor.js +1 -1
  196. package/src/utils/trusted-keys.js +1 -1
  197. package/src/utils/upload-handler.js +1 -1
  198. package/src/utils/upsert-builder.js +1 -1
  199. package/src/utils/workflow-hook-executor.js +1 -1
@@ -22,7 +22,7 @@ module.exports = {
22
22
  return {
23
23
  success: false,
24
24
  statusCode: 400,
25
- message: 'Field username dan password wajib diisi.',
25
+ message: 'Username and password are required.',
26
26
  timestamp: new Date().toISOString()
27
27
  };
28
28
  }
@@ -35,7 +35,7 @@ module.exports = {
35
35
  return {
36
36
  success: false,
37
37
  statusCode: 409,
38
- message: 'Username sudah terpakai.',
38
+ message: 'Username is already taken.',
39
39
  timestamp: new Date().toISOString()
40
40
  };
41
41
  }
@@ -55,7 +55,7 @@ module.exports = {
55
55
  return {
56
56
  success: true,
57
57
  statusCode: 201,
58
- message: 'Registrasi berhasil.',
58
+ message: 'Registration successful.',
59
59
  data: {
60
60
  user_id: userId,
61
61
  username,
@@ -69,7 +69,7 @@ module.exports = {
69
69
  return {
70
70
  success: false,
71
71
  statusCode: 500,
72
- message: 'Terjadi kesalahan internal pada server.',
72
+ message: 'An internal server error occurred.',
73
73
  timestamp: new Date().toISOString()
74
74
  };
75
75
  }
@@ -25,7 +25,7 @@ module.exports = {
25
25
  return {
26
26
  success: false,
27
27
  statusCode: 400,
28
- message: 'Email, password baru, dan konfirmasi wajib diisi.',
28
+ message: 'Email, new password, and confirmation are required.',
29
29
  timestamp: new Date().toISOString()
30
30
  };
31
31
  }
@@ -33,7 +33,7 @@ module.exports = {
33
33
  return {
34
34
  success: false,
35
35
  statusCode: 400,
36
- message: `Password minimal ${MIN_PASSWORD} karakter.`,
36
+ message: `Password must be at least ${MIN_PASSWORD} characters.`,
37
37
  timestamp: new Date().toISOString()
38
38
  };
39
39
  }
@@ -41,7 +41,7 @@ module.exports = {
41
41
  return {
42
42
  success: false,
43
43
  statusCode: 400,
44
- message: 'Konfirmasi password tidak cocok.',
44
+ message: 'Password confirmation does not match.',
45
45
  timestamp: new Date().toISOString()
46
46
  };
47
47
  }
@@ -55,7 +55,7 @@ module.exports = {
55
55
  return {
56
56
  success: false,
57
57
  statusCode: 404,
58
- message: 'Email tidak terdaftar.',
58
+ message: 'Email is not registered.',
59
59
  timestamp: new Date().toISOString()
60
60
  };
61
61
  }
@@ -64,7 +64,7 @@ module.exports = {
64
64
  return {
65
65
  success: false,
66
66
  statusCode: 409,
67
- message: 'Email terdaftar pada lebih dari satu akun. Hubungi administrator.',
67
+ message: 'Email is registered to multiple accounts. Contact administrator.',
68
68
  timestamp: new Date().toISOString()
69
69
  };
70
70
  }
@@ -90,7 +90,7 @@ module.exports = {
90
90
  return {
91
91
  success: true,
92
92
  statusCode: 200,
93
- message: 'Password berhasil diperbarui. Silakan login dengan password baru.',
93
+ message: 'Password updated successfully. Please sign in with your new password.',
94
94
  timestamp: new Date().toISOString()
95
95
  };
96
96
  } catch (error) {
@@ -98,7 +98,7 @@ module.exports = {
98
98
  return {
99
99
  success: false,
100
100
  statusCode: 500,
101
- message: 'Terjadi kesalahan internal pada server.',
101
+ message: 'An internal server error occurred.',
102
102
  timestamp: new Date().toISOString()
103
103
  };
104
104
  }
@@ -6,6 +6,7 @@
6
6
  *
7
7
  * POST /api/{{PROJECT_NAME}}/rfx_auth/register — buat user baru
8
8
  * POST /api/{{PROJECT_NAME}}/rfx_auth/login — login, terbitkan access + refresh token
9
+ * POST /api/{{PROJECT_NAME}}/rfx_auth/google — login via Google ID token (find-or-create)
9
10
  * POST /api/{{PROJECT_NAME}}/rfx_auth/refresh — tukar refresh token (rotation)
10
11
  * POST /api/{{PROJECT_NAME}}/rfx_auth/logout — revoke refresh token
11
12
  * POST /api/{{PROJECT_NAME}}/rfx_auth/reset-password — reset password via email (tanpa token)
@@ -21,6 +22,7 @@ const services = resolveServices();
21
22
 
22
23
  const register = require('./processor/auth/register');
23
24
  const login = require('./processor/auth/login');
25
+ const google = require('./processor/auth/google');
24
26
  const refresh = require('./processor/auth/refresh');
25
27
  const logout = require('./processor/auth/logout');
26
28
  const me = require('./processor/auth/me');
@@ -83,6 +85,7 @@ function handle(processor, requiredFields, actionLabel) {
83
85
 
84
86
  router.post('/register', handle(register, ['username', 'password'], 'register'));
85
87
  router.post('/login', handle(login, ['username', 'password'], 'login'));
88
+ router.post('/google', handle(google, ['credential'], 'google'));
86
89
  router.post('/refresh', handle(refresh, ['refresh_token'], 'refresh'));
87
90
  router.post('/logout', handle(logout, [], 'logout'));
88
91
  router.post('/reset-password', handle(resetPassword, ['email', 'new_password', 'confirm_password'], 'reset-password'));
@@ -61,38 +61,32 @@ class ModelGenerator {
61
61
  */
62
62
  static generateModelContent(moduleName, endpointName, payload, databaseType) {
63
63
  try {
64
- // Try Oracle template first if specified
65
- if (databaseType === 'sqlite') {
66
- const sqliteContent = this.generateSqliteModelContent(moduleName, endpointName, payload);
67
- if (sqliteContent) {
68
- console.log(`Using SQLite-specific template for ${endpointName} model`);
69
- return sqliteContent;
70
- } else {
71
- console.log(`SQLite template not available, falling back to PostgreSQL template`);
72
- }
73
- }
74
-
75
- if (databaseType === 'mysql') {
76
- const mysqlContent = this.generateMysqlModelContent(moduleName, endpointName, payload);
77
- if (mysqlContent) {
78
- console.log(`Using MySQL-specific template for ${endpointName} model`);
79
- return mysqlContent;
80
- } else {
81
- console.log(`MySQL template not available, falling back to PostgreSQL template`);
82
- }
83
- }
84
-
85
- if (databaseType === 'oracle') {
86
- const oracleContent = this.generateOracleModelContent(moduleName, endpointName, payload);
87
- if (oracleContent) {
88
- console.log(`Using Oracle-specific template for ${endpointName} model`);
89
- return oracleContent;
90
- } else {
91
- console.log(`Oracle template not available, falling back to PostgreSQL template`);
64
+ // Tipe database non-postgres yang diminta eksplisit WAJIB memakai template
65
+ // dialeknya. Bila template-nya tidak tersedia, kosong, atau melempar error,
66
+ // generator HARD-FAIL tidak boleh fallback senyap ke PostgreSQL karena akan
67
+ // menghasilkan model dialek salah (mis. sintaks pg untuk project Oracle).
68
+ const dialectGenerators = {
69
+ sqlite: { label: 'SQLite', fn: this.generateSqliteModelContent },
70
+ mysql: { label: 'MySQL', fn: this.generateMysqlModelContent },
71
+ oracle: { label: 'Oracle', fn: this.generateOracleModelContent }
72
+ };
73
+
74
+ const dialect = dialectGenerators[databaseType];
75
+ if (dialect) {
76
+ const content = dialect.fn.call(this, moduleName, endpointName, payload);
77
+ if (content) {
78
+ console.log(`Using ${dialect.label}-specific template for ${endpointName} model`);
79
+ return content;
92
80
  }
81
+ throw new Error(
82
+ `${dialect.label} model template unavailable or produced empty output for ` +
83
+ `'${moduleName}/${endpointName}'. Refusing to fall back to the PostgreSQL template ` +
84
+ `for an explicitly requested '${databaseType}' model. ` +
85
+ `Verify the ${dialect.label} template is installed and supports this payload.`
86
+ );
93
87
  }
94
88
 
95
- // Default PostgreSQL template
89
+ // Default PostgreSQL template (databaseType === 'postgres')
96
90
  return this.generatePostgresModelContent(moduleName, endpointName, payload);
97
91
 
98
92
  } catch (error) {
@@ -109,18 +103,17 @@ class ModelGenerator {
109
103
  * @returns {string|null} Oracle model content atau null
110
104
  */
111
105
  static generateOracleModelContent(moduleName, endpointName, payload) {
106
+ const oracleUtils = this.loadOracleUtils();
107
+ if (!oracleUtils || !oracleUtils.createOracleModelTemplate) {
108
+ return null; // modul template Oracle benar-benar tidak tersedia
109
+ }
110
+ // Error saat membangun template adalah bug nyata — jangan ditelan,
111
+ // bungkus dengan konteks lalu lempar agar terlihat oleh pemanggil.
112
112
  try {
113
- const oracleUtils = this.loadOracleUtils();
114
- if (oracleUtils && oracleUtils.createOracleModelTemplate) {
115
- const content = oracleUtils.createOracleModelTemplate(moduleName, endpointName, payload);
116
- if (content && content.trim().length > 0) {
117
- return content;
118
- }
119
- }
120
- return null;
113
+ const content = oracleUtils.createOracleModelTemplate(moduleName, endpointName, payload);
114
+ return content && content.trim().length > 0 ? content : null;
121
115
  } catch (error) {
122
- console.warn(`Oracle model template not available: ${error.message}`);
123
- return null;
116
+ throw new Error(`Oracle model template failed for '${moduleName}/${endpointName}': ${error.message}`);
124
117
  }
125
118
  }
126
119
 
@@ -144,18 +137,15 @@ class ModelGenerator {
144
137
  * @returns {string|null} SQLite model content atau null
145
138
  */
146
139
  static generateSqliteModelContent(moduleName, endpointName, payload) {
140
+ const sqliteUtils = this.loadSqliteUtils();
141
+ if (!sqliteUtils || !sqliteUtils.createSqliteModelTemplate) {
142
+ return null; // modul template SQLite benar-benar tidak tersedia
143
+ }
147
144
  try {
148
- const sqliteUtils = this.loadSqliteUtils();
149
- if (sqliteUtils && sqliteUtils.createSqliteModelTemplate) {
150
- const content = sqliteUtils.createSqliteModelTemplate(moduleName, endpointName, payload);
151
- if (content && content.trim().length > 0) {
152
- return content;
153
- }
154
- }
155
- return null;
145
+ const content = sqliteUtils.createSqliteModelTemplate(moduleName, endpointName, payload);
146
+ return content && content.trim().length > 0 ? content : null;
156
147
  } catch (error) {
157
- console.warn(`SQLite model template not available: ${error.message}`);
158
- return null;
148
+ throw new Error(`SQLite model template failed for '${moduleName}/${endpointName}': ${error.message}`);
159
149
  }
160
150
  }
161
151
 
@@ -179,18 +169,15 @@ class ModelGenerator {
179
169
  * @returns {string|null} MySQL model content atau null
180
170
  */
181
171
  static generateMysqlModelContent(moduleName, endpointName, payload) {
172
+ const mysqlUtils = this.loadMysqlUtils();
173
+ if (!mysqlUtils || !mysqlUtils.createMysqlModelTemplate) {
174
+ return null; // modul template MySQL benar-benar tidak tersedia
175
+ }
182
176
  try {
183
- const mysqlUtils = this.loadMysqlUtils();
184
- if (mysqlUtils && mysqlUtils.createMysqlModelTemplate) {
185
- const content = mysqlUtils.createMysqlModelTemplate(moduleName, endpointName, payload);
186
- if (content && content.trim().length > 0) {
187
- return content;
188
- }
189
- }
190
- return null;
177
+ const content = mysqlUtils.createMysqlModelTemplate(moduleName, endpointName, payload);
178
+ return content && content.trim().length > 0 ? content : null;
191
179
  } catch (error) {
192
- console.warn(`MySQL model template not available: ${error.message}`);
193
- return null;
180
+ throw new Error(`MySQL model template failed for '${moduleName}/${endpointName}': ${error.message}`);
194
181
  }
195
182
  }
196
183
 
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const HELP_WIDTH = 100;
4
+
3
5
  const CATEGORY_ORDER = ['generation', 'management', 'introspection', 'utility'];
4
6
  const CATEGORY_LABEL = {
5
7
  generation: 'Generation',
@@ -131,6 +133,27 @@ function flagSignature(flagDef) {
131
133
  return ` <${flagDef.type}>`;
132
134
  }
133
135
 
136
+ function wrapText(text, firstAvail, contIndent) {
137
+ const words = text.split(' ');
138
+ const lines = [];
139
+ let current = '';
140
+ let avail = firstAvail;
141
+
142
+ for (const word of words) {
143
+ if (!current) {
144
+ current = word;
145
+ } else if (current.length + 1 + word.length <= avail) {
146
+ current += ' ' + word;
147
+ } else {
148
+ lines.push(current);
149
+ current = word;
150
+ avail = HELP_WIDTH - contIndent;
151
+ }
152
+ }
153
+ if (current) lines.push(current);
154
+ return lines;
155
+ }
156
+
134
157
  function defaultRepr(def) {
135
158
  if (def === null) return 'null';
136
159
  if (def === undefined) return 'undefined';
@@ -201,7 +224,12 @@ function generateCommandHelp(contract) {
201
224
  for (const posDef of positional) {
202
225
  const sig = posDef.required ? `<${posDef.name}>` : `[${posDef.name}]`;
203
226
  const label = `${sig} <${posDef.type}>`;
204
- lines.push(` ${padRight(label, colWidth)} ${posDef.description}`);
227
+ const prefix = ` ${padRight(label, colWidth)} `;
228
+ const descLines = wrapText(posDef.description, HELP_WIDTH - prefix.length, prefix.length);
229
+ lines.push(`${prefix}${descLines[0]}`);
230
+ for (let i = 1; i < descLines.length; i++) {
231
+ lines.push(`${' '.repeat(prefix.length)}${descLines[i]}`);
232
+ }
205
233
  }
206
234
  lines.push('');
207
235
  }
@@ -211,7 +239,12 @@ function generateCommandHelp(contract) {
211
239
  for (const fname of requiredFlagNames) {
212
240
  const fdef = flags[fname];
213
241
  const label = `--${fname}${flagSignature(fdef)}`;
214
- lines.push(` ${padRight(label, colWidth)} ${fdef.description}`);
242
+ const prefix = ` ${padRight(label, colWidth)} `;
243
+ const descLines = wrapText(fdef.description, HELP_WIDTH - prefix.length, prefix.length);
244
+ lines.push(`${prefix}${descLines[0]}`);
245
+ for (let i = 1; i < descLines.length; i++) {
246
+ lines.push(`${' '.repeat(prefix.length)}${descLines[i]}`);
247
+ }
215
248
  }
216
249
  lines.push('');
217
250
  }
@@ -222,7 +255,12 @@ function generateCommandHelp(contract) {
222
255
  const fdef = flags[fname];
223
256
  const label = `--${fname}${flagSignature(fdef)}`;
224
257
  const suffix = ` (default: ${defaultRepr(fdef.default)})`;
225
- lines.push(` ${padRight(label, colWidth)} ${fdef.description}${suffix}`);
258
+ const prefix = ` ${padRight(label, colWidth)} `;
259
+ const descLines = wrapText(`${fdef.description}${suffix}`, HELP_WIDTH - prefix.length, prefix.length);
260
+ lines.push(`${prefix}${descLines[0]}`);
261
+ for (let i = 1; i < descLines.length; i++) {
262
+ lines.push(`${' '.repeat(prefix.length)}${descLines[i]}`);
263
+ }
226
264
  }
227
265
  lines.push('');
228
266
  }
@@ -23,7 +23,7 @@
23
23
  const path = require('path');
24
24
  const { DatabaseIntrospector } = require('../utils/database-introspector');
25
25
  const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-resolver');
26
- const { compareSchemaStrict, formatDriftReport } = require('./schema-diff');
26
+ const { compareSchemaStrict, formatDriftReport, resolveFieldProjectionInputs } = require('./schema-diff');
27
27
 
28
28
  /**
29
29
  * Buat Error object dengan property `exitCode` agar cli-entry dapat
@@ -57,7 +57,8 @@ function createExitError(message, exitCode) {
57
57
  * @returns {Promise<{
58
58
  * status: 'ok' | 'skipped',
59
59
  * reason?: string,
60
- * columnsChecked?: number
60
+ * columnsChecked?: number,
61
+ * projectionInputs?: {physicalColumns: string[], readSourceColumns: string[], datatablesColumns: string[]|null}
61
62
  * }>}
62
63
  * @throws {Error} Dengan property exitCode (1=drift, 2=usage, 3=connection)
63
64
  */
@@ -128,8 +129,11 @@ async function validateEndpointSchema(options) {
128
129
  : path.join(workingDir || process.cwd(), 'payload');
129
130
 
130
131
  let comparison;
132
+ let projectionInputs = null;
131
133
  try {
132
134
  comparison = await compareSchemaStrict(payload, db, { payloadDir });
135
+ // Reuse db handle (D2): compute per-source column inputs untuk deriveFieldProjections
136
+ projectionInputs = await resolveFieldProjectionInputs(payload, db, { payloadDir });
133
137
  } finally {
134
138
  try { await db.close(); } catch (_e) { /* ignore */ }
135
139
  }
@@ -158,7 +162,8 @@ async function validateEndpointSchema(options) {
158
162
 
159
163
  return {
160
164
  status: 'ok',
161
- columnsChecked: comparison.totalColumnsChecked || 0
165
+ columnsChecked: comparison.totalColumnsChecked || 0,
166
+ projectionInputs
162
167
  };
163
168
  }
164
169
 
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const { isSoftDeleteEnabled, SOFT_DELETE_COLUMNS } = require('../dbschema-kit/soft-delete-constants');
4
+
5
+ /**
6
+ * Derive field projection whitelists from fieldName and introspected source columns.
7
+ * Pure function — no DB, no side effects.
8
+ *
9
+ * Kebijakan case-matching: case-insensitive (lowercase normalization).
10
+ * DB introspection mengembalikan nama kolom dengan case yang bisa berbeda
11
+ * tergantung dialect (Oracle uppercase, PostgreSQL lowercase, dll.).
12
+ * Pencocokan dilakukan lowercase di kedua sisi agar konsisten.
13
+ *
14
+ * @param {Object} input
15
+ * @param {string[]} input.fieldName - Array fieldName dari payload
16
+ * @param {string[]} input.physicalColumns - Kolom fisik tabel dari introspeksi DB
17
+ * @param {string[]|null} input.readSourceColumns - Kolom output sumber-baca-resolusi
18
+ * (viewName → viewQuery → tableName). Null atau [] → sumber gagal diintrospeksi.
19
+ * @param {string[]|null} input.datatablesColumns - Kolom output datatablesQuery.
20
+ * null = tidak ada datatablesQuery (fallback ke schemaFields).
21
+ * [] = datatablesQuery ada tapi introspeksi gagal (hard-fail kecuali ada override).
22
+ * @param {Object} [input.overrides={}]
23
+ * @param {string[]} [input.overrides.readableFields] - Override eksplisit readableFields
24
+ * @param {string[]} [input.overrides.datatablesFields] - Override eksplisit datatablesFields
25
+ * @returns {{ schemaFields: string[], readableFields: string[], datatablesFields: string[] }}
26
+ * @throws {Error} Bila validasi override gagal atau hard-fail D3 terpenuhi
27
+ */
28
+ function deriveFieldProjections({
29
+ fieldName,
30
+ physicalColumns,
31
+ readSourceColumns,
32
+ datatablesColumns,
33
+ overrides = {}
34
+ }) {
35
+ // Validasi override: tiap elemen harus ⊆ fieldName
36
+ for (const key of ['readableFields', 'datatablesFields']) {
37
+ if (!overrides[key]) continue;
38
+ for (const f of overrides[key]) {
39
+ if (!fieldName.includes(f)) {
40
+ throw new Error(
41
+ `Override ${key} berisi "${f}" yang tidak ada di fieldName. ` +
42
+ `Nilai valid: [${fieldName.join(', ')}]`
43
+ );
44
+ }
45
+ }
46
+ }
47
+
48
+ const toSet = (arr) => new Set((arr || []).map(c => String(c).toLowerCase()));
49
+
50
+ // schemaFields = fieldName ∩ physicalColumns (pertahankan urutan fieldName)
51
+ const physicalSet = toSet(physicalColumns);
52
+ const schemaFields = fieldName.filter(f => physicalSet.has(f.toLowerCase()));
53
+
54
+ // readableFields
55
+ let readableFields;
56
+ if (overrides.readableFields) {
57
+ readableFields = overrides.readableFields;
58
+ } else {
59
+ // Hard-fail D3: sumber-baca tidak terintrospeksi dan tidak ada override
60
+ if (!readSourceColumns || readSourceColumns.length === 0) {
61
+ throw new Error(
62
+ 'Cannot derive readableFields: read-source resolution ' +
63
+ '(viewName → viewQuery → tableName) produced no columns. ' +
64
+ 'Provide payload.readableFields as an override to bypass.'
65
+ );
66
+ }
67
+ const readSet = toSet(readSourceColumns);
68
+ readableFields = fieldName.filter(f => readSet.has(f.toLowerCase()));
69
+ }
70
+
71
+ // datatablesFields
72
+ let datatablesFields;
73
+ if (overrides.datatablesFields) {
74
+ datatablesFields = overrides.datatablesFields;
75
+ } else if (datatablesColumns === null) {
76
+ // Tidak ada datatablesQuery → fallback ke schemaFields
77
+ datatablesFields = schemaFields;
78
+ } else if (datatablesColumns.length === 0) {
79
+ // Hard-fail D3: datatablesQuery ada tapi introspeksi gagal
80
+ throw new Error(
81
+ 'Cannot derive datatablesFields: datatablesQuery is set but ' +
82
+ 'introspection produced no columns. ' +
83
+ 'Provide payload.datatablesFields as an override to bypass.'
84
+ );
85
+ } else {
86
+ const dtSet = toSet(datatablesColumns);
87
+ datatablesFields = fieldName.filter(f => dtSet.has(f.toLowerCase()));
88
+ }
89
+
90
+ return { schemaFields, readableFields, datatablesFields };
91
+ }
92
+
93
+ /**
94
+ * Augmentasi proyeksi dengan kolom soft-delete bila soft-delete aktif (D9).
95
+ * Meniru buildValidFieldsString: kolom soft-delete disuntik middleware ke WHERE,
96
+ * sehingga WAJIB ada di ketiga proyeksi agar validasi whitelist lolos.
97
+ * Augmentasi tak-bersyarat — tidak digate interseksi kolom sumber.
98
+ * Mutasi in-place; mengembalikan objek yang sama.
99
+ *
100
+ * @param {{ schemaFields: string[], readableFields: string[], datatablesFields: string[] }} fieldProjections
101
+ * @param {Object} payload - IR/payload dengan blok softDelete opsional
102
+ * @returns {typeof fieldProjections}
103
+ */
104
+ function augmentProjectionsForSoftDelete(fieldProjections, payload) {
105
+ if (!isSoftDeleteEnabled(payload)) return fieldProjections;
106
+ for (const proj of ['schemaFields', 'readableFields', 'datatablesFields']) {
107
+ for (const col of SOFT_DELETE_COLUMNS) {
108
+ if (!fieldProjections[proj].includes(col)) {
109
+ fieldProjections[proj].push(col);
110
+ }
111
+ }
112
+ }
113
+ return fieldProjections;
114
+ }
115
+
116
+ module.exports = { deriveFieldProjections, augmentProjectionsForSoftDelete };