@restforgejs/platform 4.3.4 → 4.3.8

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 (178) 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/payload/migrate.js +96 -0
  5. package/generators/lib/migrate/backend-payload-migrator.js +221 -0
  6. package/generators/lib/migrate/field-type-resolver.js +319 -0
  7. package/generators/lib/migrate/label-generator.js +38 -0
  8. package/generators/lib/migrate/migrate-runner.js +187 -0
  9. package/generators/lib/migrate/naming.js +43 -0
  10. package/generators/lib/migrate/sql-parser.js +124 -0
  11. package/generators/lib/payload/endpoint-schema-validator.js +181 -181
  12. package/generators/lib/payload/payload-runner.js +1313 -1218
  13. package/generators/lib/payload/schema-diff.js +460 -460
  14. package/generators/lib/templates/dashboard-catalog.js +1 -1
  15. package/generators/lib/templates/db-connection-env.js +1 -1
  16. package/generators/lib/templates/dbschema-catalog.js +1 -1
  17. package/generators/lib/templates/field-validation-catalog.js +1 -1
  18. package/generators/lib/templates/mysql-template.js +1 -1
  19. package/generators/lib/templates/oracle-template.js +1 -1
  20. package/generators/lib/templates/postgres-template.js +1 -1
  21. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  22. package/generators/lib/templates/sqlite-template.js +1 -1
  23. package/integrity-manifest.json +18 -18
  24. package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +1 -1
  25. package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
  26. package/package.json +1 -1
  27. package/scripts/verify-integrity.js +1 -1
  28. package/server.js +1 -1
  29. package/src/components/handlers/adjust_handler.js +1 -1
  30. package/src/components/handlers/audit_handler.js +1 -1
  31. package/src/components/handlers/delete_handler.js +1 -1
  32. package/src/components/handlers/export_handler.js +1 -1
  33. package/src/components/handlers/import_handler.js +1 -1
  34. package/src/components/handlers/insert_handler.js +1 -1
  35. package/src/components/handlers/update_handler.js +1 -1
  36. package/src/components/handlers/upload_handler.js +1 -1
  37. package/src/components/handlers/workflow_handler.js +1 -1
  38. package/src/components/integrations/webhook.js +1 -1
  39. package/src/consumers/baseConsumer.js +1 -1
  40. package/src/consumers/declarativeMapper.js +1 -1
  41. package/src/consumers/handlers/apiHandler.js +1 -1
  42. package/src/consumers/handlers/consoleHandler.js +1 -1
  43. package/src/consumers/handlers/databaseHandler.js +1 -1
  44. package/src/consumers/handlers/index.js +1 -1
  45. package/src/consumers/handlers/kafkaHandler.js +1 -1
  46. package/src/consumers/index.js +1 -1
  47. package/src/consumers/messageTransformer.js +1 -1
  48. package/src/consumers/validator.js +1 -1
  49. package/src/core/db/dialect/base-dialect.js +1 -1
  50. package/src/core/db/dialect/index.js +1 -1
  51. package/src/core/db/dialect/mysql-dialect.js +1 -1
  52. package/src/core/db/dialect/oracle-dialect.js +1 -1
  53. package/src/core/db/dialect/postgres-dialect.js +1 -1
  54. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  55. package/src/core/db/flatten-helper.js +1 -1
  56. package/src/core/db/query-builder-error.js +1 -1
  57. package/src/core/db/query-builder.js +1 -1
  58. package/src/core/db/relation-helper.js +1 -1
  59. package/src/core/handlers/delete_handler.js +1 -1
  60. package/src/core/handlers/insert_handler.js +1 -1
  61. package/src/core/handlers/update_handler.js +1 -1
  62. package/src/core/models/base-model.js +1 -1
  63. package/src/core/utils/cache-manager.js +1 -1
  64. package/src/core/utils/component-engine.js +1 -1
  65. package/src/core/utils/context-builder.js +1 -1
  66. package/src/core/utils/datetime-formatter.js +1 -1
  67. package/src/core/utils/datetime-parser.js +1 -1
  68. package/src/core/utils/db.js +1 -1
  69. package/src/core/utils/logger.js +1 -1
  70. package/src/core/utils/payload-loader.js +1 -1
  71. package/src/core/utils/security-checks.js +1 -1
  72. package/src/middleware/body-options.js +1 -1
  73. package/src/middleware/cors.js +1 -1
  74. package/src/middleware/idempotency.js +1 -1
  75. package/src/middleware/rate-limiter.js +1 -1
  76. package/src/middleware/request-logger.js +1 -1
  77. package/src/middleware/security-headers.js +1 -1
  78. package/src/models/base-model-mysql.js +1 -1
  79. package/src/models/base-model-oracle.js +1 -1
  80. package/src/models/base-model-sqlite.js +1 -1
  81. package/src/models/base-model.js +1 -1
  82. package/src/pro/caching/redis-client.js +1 -1
  83. package/src/pro/caching/redis-helper.js +1 -1
  84. package/src/pro/consumers/baseConsumer.js +1 -1
  85. package/src/pro/consumers/declarativeMapper.js +1 -1
  86. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  87. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  88. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  89. package/src/pro/consumers/handlers/index.js +1 -1
  90. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  91. package/src/pro/consumers/index.js +1 -1
  92. package/src/pro/consumers/messageTransformer.js +1 -1
  93. package/src/pro/consumers/validator.js +1 -1
  94. package/src/pro/database/base-model-mysql.js +1 -1
  95. package/src/pro/database/base-model-oracle.js +1 -1
  96. package/src/pro/database/base-model-sqlite.js +1 -1
  97. package/src/pro/database/db-mysql.js +1 -1
  98. package/src/pro/database/db-oracle.js +1 -1
  99. package/src/pro/database/db-sqlite.js +1 -1
  100. package/src/pro/excel/excel-generator.js +1 -1
  101. package/src/pro/excel/excel-parser.js +1 -1
  102. package/src/pro/excel/export-service.js +1 -1
  103. package/src/pro/excel/export_handler.js +1 -1
  104. package/src/pro/excel/import-service.js +1 -1
  105. package/src/pro/excel/import-validator.js +1 -1
  106. package/src/pro/excel/import_handler.js +1 -1
  107. package/src/pro/excel/upsert-builder.js +1 -1
  108. package/src/pro/idgen/idgen-routes.js +1 -1
  109. package/src/pro/integrations/lookup-resolver.js +1 -1
  110. package/src/pro/integrations/upload-handler-v2.js +1 -1
  111. package/src/pro/integrations/upload-handler.js +1 -1
  112. package/src/pro/integrations/webhook.js +1 -1
  113. package/src/pro/locking/lock-routes.js +1 -1
  114. package/src/pro/locking/resource-lock-manager.js +1 -1
  115. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  116. package/src/pro/messaging/kafkaService.js +1 -1
  117. package/src/pro/messaging/messagehubService.js +1 -1
  118. package/src/pro/messaging/rabbitmqService.js +1 -1
  119. package/src/pro/scheduler/job-manager.js +1 -1
  120. package/src/pro/scheduler/job-routes.js +1 -1
  121. package/src/pro/scheduler/job-validator.js +1 -1
  122. package/src/pro/storage/base-storage-provider.js +1 -1
  123. package/src/pro/storage/file-metadata-helper.js +1 -1
  124. package/src/pro/storage/index.js +1 -1
  125. package/src/pro/storage/local-storage-provider.js +1 -1
  126. package/src/pro/storage/s3-storage-provider.js +1 -1
  127. package/src/pro/storage/upload-cleanup-job.js +1 -1
  128. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  129. package/src/pro/storage/upload-pending-tracker.js +1 -1
  130. package/src/pro/websocket/broadcast-helper.js +1 -1
  131. package/src/pro/websocket/index.js +1 -1
  132. package/src/pro/websocket/livesync-server.js +1 -1
  133. package/src/pro/websocket/ws-broadcaster.js +1 -1
  134. package/src/services/export-service.js +1 -1
  135. package/src/services/import-service.js +1 -1
  136. package/src/services/kafkaConsumerService.js +1 -1
  137. package/src/services/kafkaService.js +1 -1
  138. package/src/services/messagehubService.js +1 -1
  139. package/src/services/rabbitmqService.js +1 -1
  140. package/src/utils/cache-invalidation-registry.js +1 -1
  141. package/src/utils/cache-manager.js +1 -1
  142. package/src/utils/component-engine.js +1 -1
  143. package/src/utils/config-extractor.js +1 -1
  144. package/src/utils/consumerLogger.js +1 -1
  145. package/src/utils/context-builder.js +1 -1
  146. package/src/utils/dashboard-helpers.js +1 -1
  147. package/src/utils/dateHelper.js +1 -1
  148. package/src/utils/datetime-formatter.js +1 -1
  149. package/src/utils/datetime-parser.js +1 -1
  150. package/src/utils/db-bootstrap.js +1 -1
  151. package/src/utils/db-mysql.js +1 -1
  152. package/src/utils/db-oracle.js +1 -1
  153. package/src/utils/db-sqlite.js +1 -1
  154. package/src/utils/db.js +1 -1
  155. package/src/utils/demo-generator.js +1 -1
  156. package/src/utils/excel-generator.js +1 -1
  157. package/src/utils/excel-parser.js +1 -1
  158. package/src/utils/file-watcher.js +1 -1
  159. package/src/utils/id-generator.js +1 -1
  160. package/src/utils/idempotency-manager.js +1 -1
  161. package/src/utils/import-validator.js +1 -1
  162. package/src/utils/license-client.js +1 -1
  163. package/src/utils/lock-manager.js +1 -1
  164. package/src/utils/logger.js +1 -1
  165. package/src/utils/lookup-resolver.js +1 -1
  166. package/src/utils/payload-loader.js +1 -1
  167. package/src/utils/processor-response.js +1 -1
  168. package/src/utils/rabbitmq.js +1 -1
  169. package/src/utils/redis-client.js +1 -1
  170. package/src/utils/redis-helper.js +1 -1
  171. package/src/utils/request-scope.js +1 -1
  172. package/src/utils/security-checks.js +1 -1
  173. package/src/utils/service-resolver.js +1 -1
  174. package/src/utils/shutdown-coordinator.js +1 -1
  175. package/src/utils/trusted-keys.js +1 -1
  176. package/src/utils/upload-handler.js +1 -1
  177. package/src/utils/upsert-builder.js +1 -1
  178. package/src/utils/workflow-hook-executor.js +1 -1
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Contract: payload migrate
5
+ *
6
+ * Konversi file payload backend (RDF / RESTForge consumer) menjadi file payload
7
+ * frontend (UDF / designer). Port dari `rfd migrate` di packages/designer/.
8
+ *
9
+ * Perbedaan dengan `rfd migrate`:
10
+ * - `apiBaseUrl` di-construct dari SERVER_ADDRESS + SERVER_PORT (db-connection.env)
11
+ * + --project sehingga sesuai dengan runtime server actual
12
+ * - Port di-baca dari db-connection.env (SERVER_PORT), bukan hard-coded 3000
13
+ * - Primary key (constraints.primaryKey=true) di-skip dari fields[] apapun
14
+ * tipenya (uuid/string/integer), karena PK adalah identifier teknis dan
15
+ * tidak perlu di-render di UI form
16
+ */
17
+
18
+ const runner = require('../../lib/migrate/migrate-runner');
19
+
20
+ module.exports = {
21
+ resource: 'payload',
22
+ verb: 'migrate',
23
+ description: 'Convert backend payload (RDF) file into frontend payload (UDF) for the designer',
24
+ category: 'generation',
25
+ flags: {
26
+ name: {
27
+ type: 'string',
28
+ required: true,
29
+ description: 'Backend payload file name (e.g. visitors.json). Relative to cwd or cwd/payload/.'
30
+ },
31
+ output: {
32
+ type: 'string',
33
+ required: false,
34
+ default: null,
35
+ description: 'Output directory (the file will be written with the same name as --name inside it). If ending with `.json`, treated as an explicit file path. Default: frontend/payload/'
36
+ },
37
+ config: {
38
+ type: 'string',
39
+ required: false,
40
+ default: null,
41
+ description: 'Database config file (.env). Used to read SERVER_ADDRESS and SERVER_PORT. Falls back to `.restforge/defaults.json`.'
42
+ },
43
+ project: {
44
+ type: 'string',
45
+ required: true,
46
+ description: 'Project name (kebab-case code) used as the path segment in apiBaseUrl: http://{host}:{port}/api/{project}'
47
+ },
48
+ 'app-name': {
49
+ type: 'string',
50
+ required: false,
51
+ default: null,
52
+ description: 'Application name (default: "My Application")'
53
+ },
54
+ 'app-code': {
55
+ type: 'string',
56
+ required: false,
57
+ default: null,
58
+ description: 'Application code in kebab-case (default: follows --project)'
59
+ },
60
+ plugin: {
61
+ type: 'string',
62
+ required: false,
63
+ default: null,
64
+ description: 'Designer plugin ID (default: "vanilla-js-basic")'
65
+ },
66
+ port: {
67
+ type: 'number',
68
+ required: false,
69
+ default: null,
70
+ description: 'Frontend application port written to appConfig.port (default: 8000). Independent from the backend port used in apiBaseUrl.'
71
+ },
72
+ overwrite: {
73
+ type: 'boolean',
74
+ required: false,
75
+ default: false,
76
+ description: 'Overwrite the output file if it already exists'
77
+ }
78
+ },
79
+ examples: [
80
+ 'npx restforge payload migrate --name=visitors.json --output=..\\sandbox\\frontend\\payload --config=db.env --project=myapp',
81
+ 'npx restforge payload migrate --name=visitors.json --project=myapp --overwrite'
82
+ ],
83
+ async handler(args) {
84
+ await runner.run({
85
+ name: args.name,
86
+ output: args.output || null,
87
+ config: args.config || null,
88
+ project: args.project,
89
+ appName: args['app-name'] || null,
90
+ appCode: args['app-code'] || null,
91
+ plugin: args.plugin || null,
92
+ port: typeof args.port === 'number' ? args.port : null,
93
+ overwrite: args.overwrite === true
94
+ });
95
+ }
96
+ };
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Port dari packages/designer/src/migrators/backend_payload_migrator.rs.
5
+ *
6
+ * Orchestrator utama konversi payload backend (RESTForge / RDF) menjadi
7
+ * frontend payload designer (UDF). Menggabungkan output sql-parser +
8
+ * field-type-resolver + label-generator untuk menghasilkan frontend payload
9
+ * deterministic.
10
+ */
11
+
12
+ const { parseDatatablesQuery } = require('./sql-parser');
13
+ const { FieldTypeResolver } = require('./field-type-resolver');
14
+ const { snakeToKebab, snakeToTitle } = require('./naming');
15
+
16
+ const DEFAULT_ICON = 'file-text';
17
+ const ICON_MAP = {
18
+ contact: 'users',
19
+ customer: 'users',
20
+ user: 'users',
21
+ supplier: 'truck',
22
+ vendor: 'truck',
23
+ category: 'tag',
24
+ item: 'package',
25
+ product: 'package',
26
+ stock: 'clipboard',
27
+ inventory: 'clipboard',
28
+ warehouse: 'building',
29
+ order: 'shopping-cart',
30
+ invoice: 'file-text',
31
+ city: 'map-pin',
32
+ country: 'globe',
33
+ department: 'briefcase',
34
+ employee: 'user'
35
+ };
36
+
37
+ function detectIcon(tableName) {
38
+ const lower = String(tableName || '').toLowerCase();
39
+ for (const keyword of Object.keys(ICON_MAP)) {
40
+ if (lower.includes(keyword)) return ICON_MAP[keyword];
41
+ }
42
+ return DEFAULT_ICON;
43
+ }
44
+
45
+ function detectDisplayField(resolvedFields, primaryKey) {
46
+ for (const rf of resolvedFields) {
47
+ if (rf.name.endsWith('_name') || rf.name === 'name') return rf.name;
48
+ }
49
+ for (const rf of resolvedFields) {
50
+ if (rf.name.endsWith('_code') || rf.name === 'code') return rf.name;
51
+ }
52
+ for (const rf of resolvedFields) {
53
+ if ((rf.fieldType === 'text' || rf.fieldType === 'textarea') && rf.name !== primaryKey) {
54
+ return rf.name;
55
+ }
56
+ }
57
+ if (resolvedFields.length > 0) return resolvedFields[0].name;
58
+ return '';
59
+ }
60
+
61
+ function convertSinglePage(backend) {
62
+ const warnings = [];
63
+ const tableName = String(backend.tableName || '');
64
+ const primaryKey = String(backend.primaryKey || '');
65
+ const fieldNames = Array.isArray(backend.fieldName) ? backend.fieldName.filter(n => typeof n === 'string') : [];
66
+ const datatablesQuery = String(backend.datatablesQuery || '');
67
+ const datatablesWhere = Array.isArray(backend.datatablesWhere) ? backend.datatablesWhere : [];
68
+ const fieldValidations = Array.isArray(backend.fieldValidation) ? backend.fieldValidation : [];
69
+
70
+ const cleanTable = tableName.split('.').pop() || tableName;
71
+ const parsedQuery = parseDatatablesQuery(datatablesQuery);
72
+ const resolver = new FieldTypeResolver(fieldValidations, parsedQuery, primaryKey);
73
+
74
+ const resolvedFields = fieldNames
75
+ .map(name => resolver.resolve(name))
76
+ .filter(rf => !rf.skip);
77
+
78
+ const displayField = detectDisplayField(resolvedFields, primaryKey);
79
+
80
+ const hasSearch = datatablesWhere.length > 0
81
+ && datatablesWhere.some(w => typeof w === 'string' && w !== 'all');
82
+ const hasStatusFilter = resolvedFields.some(rf => rf.name === 'is_active' || rf.name === 'status');
83
+
84
+ const features = {
85
+ enableSearch: hasSearch,
86
+ fieldLayout: 'vertical'
87
+ };
88
+
89
+ if (hasStatusFilter) {
90
+ features.enableStatusFilter = true;
91
+ for (const rf of resolvedFields) {
92
+ if ((rf.name === 'is_active' || rf.name === 'status') && rf.fieldType === 'checkbox') {
93
+ const cbt = (rf.extra && rf.extra.checkboxText) || {};
94
+ const checked = typeof cbt.checked === 'string' ? cbt.checked : 'Active';
95
+ const unchecked = typeof cbt.unchecked === 'string' ? cbt.unchecked : 'Inactive';
96
+ features.statusFilter = {
97
+ field: rf.name,
98
+ label: rf.label,
99
+ options: [
100
+ { value: 'true', text: checked },
101
+ { value: 'false', text: unchecked }
102
+ ]
103
+ };
104
+ break;
105
+ }
106
+ }
107
+ }
108
+
109
+ const fieldsArray = [];
110
+ for (const rf of resolvedFields) {
111
+ const fieldObj = { name: rf.name, label: rf.label, type: rf.fieldType };
112
+ if (rf.required) fieldObj.required = true;
113
+ if (rf.inTable) {
114
+ fieldObj.inTable = true;
115
+ if (rf.tableOrder !== null && rf.tableOrder !== undefined) {
116
+ fieldObj.tableOrder = rf.tableOrder;
117
+ }
118
+ }
119
+ if (rf.tableField) fieldObj.tableField = rf.tableField;
120
+ if (rf.defaultValue !== undefined) fieldObj.defaultValue = rf.defaultValue;
121
+ if (rf.extra && typeof rf.extra === 'object') {
122
+ for (const [key, val] of Object.entries(rf.extra)) {
123
+ fieldObj[key] = val;
124
+ }
125
+ }
126
+ fieldsArray.push(fieldObj);
127
+ }
128
+
129
+ if (backend.masterDetail !== undefined && backend.masterDetail !== null) {
130
+ warnings.push(`${cleanTable}: masterDetail is detected — detail tables are not yet supported, only header fields will be converted`);
131
+ }
132
+
133
+ const pageId = snakeToKebab(cleanTable);
134
+ const pageTitle = snakeToTitle(cleanTable);
135
+
136
+ const page = {
137
+ pageId,
138
+ pageTitle,
139
+ pageSubtitle: `Manage ${pageTitle.toLowerCase()} data`,
140
+ pageIcon: detectIcon(cleanTable),
141
+ apiPath: pageId,
142
+ primaryKey,
143
+ displayField,
144
+ features,
145
+ fields: fieldsArray
146
+ };
147
+
148
+ return { page, warnings };
149
+ }
150
+
151
+ function migrate(backendPayloads, appName, appCode, plugin, apiBaseUrl, port) {
152
+ if (!Array.isArray(backendPayloads) || backendPayloads.length === 0) {
153
+ return {
154
+ success: false,
155
+ payload: null,
156
+ pageResults: [],
157
+ errors: ['No backend payload was provided'],
158
+ warnings: []
159
+ };
160
+ }
161
+
162
+ const pages = [];
163
+ const pageResults = [];
164
+ const allWarnings = [];
165
+
166
+ for (let i = 0; i < backendPayloads.length; i++) {
167
+ const backend = backendPayloads[i];
168
+ const tableName = (backend && typeof backend.tableName === 'string') ? backend.tableName : '';
169
+ if (!tableName) {
170
+ allWarnings.push(`Payload #${i + 1}: tableName is not defined, skipped`);
171
+ continue;
172
+ }
173
+
174
+ const { page, warnings } = convertSinglePage(backend);
175
+
176
+ const fieldCount = Array.isArray(page.fields) ? page.fields.length : 0;
177
+ const tableColCount = Array.isArray(page.fields)
178
+ ? page.fields.filter(f => f.inTable === true).length
179
+ : 0;
180
+
181
+ pages.push(page);
182
+ pageResults.push({
183
+ pageId: page.pageId,
184
+ fieldCount,
185
+ tableColCount,
186
+ warnings: warnings.slice()
187
+ });
188
+ for (const w of warnings) allWarnings.push(w);
189
+ }
190
+
191
+ if (pages.length === 0) {
192
+ return {
193
+ success: false,
194
+ payload: null,
195
+ pageResults: [],
196
+ errors: ['No page was successfully converted'],
197
+ warnings: allWarnings
198
+ };
199
+ }
200
+
201
+ const payload = {
202
+ appConfig: {
203
+ appName,
204
+ appCode,
205
+ plugin,
206
+ apiBaseUrl,
207
+ port
208
+ },
209
+ pages
210
+ };
211
+
212
+ return {
213
+ success: true,
214
+ payload,
215
+ pageResults,
216
+ errors: [],
217
+ warnings: allWarnings
218
+ };
219
+ }
220
+
221
+ module.exports = { migrate, convertSinglePage, detectIcon, detectDisplayField };
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Port dari packages/designer/src/migrators/field_type_resolver.rs.
5
+ *
6
+ * Rules engine untuk resolve tipe field frontend dari fieldValidations backend +
7
+ * struktur JOIN datatablesQuery. Output berupa ResolvedField yang mendeskripsikan
8
+ * tipe form input (text/number/date/select/checkbox/textarea/dll.), label,
9
+ * posisi tabel, dan extra metadata tambahan per tipe.
10
+ *
11
+ * Perbedaan dengan Rust source:
12
+ * - Rule 1a (PK skip) diperluas: SEMUA field dengan constraints.primaryKey === true
13
+ * akan di-skip dari output fields[] apapun tipenya (uuid, string, integer, ...).
14
+ * Rust source hanya skip jika type=uuid + autoGenerate. Hasil di Rust: tabel
15
+ * dengan PK type=string + autoGenerate (mis. visitors.visitor_id) tetap masuk
16
+ * ke fields[] sebagai text field. PK adalah identifier teknis, tidak perlu
17
+ * ditampilkan di UI form.
18
+ */
19
+
20
+ const { generateLabel } = require('./label-generator');
21
+ const { snakeToTitle } = require('./naming');
22
+
23
+ const AUDIT_FIELDS = ['created_at', 'created_by', 'updated_at', 'updated_by'];
24
+ const TEXTAREA_FIELDS = ['address', 'description', 'notes'];
25
+ const TEXTAREA_PREFIXES = ['remark'];
26
+
27
+ const CHECKBOX_TEXT_DEFAULT = { checked: 'Yes', unchecked: 'No' };
28
+ const CHECKBOX_TEXT_MAP = {
29
+ is_active: { checked: 'Active', unchecked: 'Inactive' },
30
+ status: { checked: 'Active', unchecked: 'Inactive' }
31
+ };
32
+
33
+ const MAXLENGTH_DEFAULTS = {
34
+ code: 20,
35
+ name: 100,
36
+ email: 255,
37
+ phone: 20,
38
+ textarea: 500,
39
+ text: 255
40
+ };
41
+
42
+ function defaultMaxlength(fieldName) {
43
+ if (fieldName.endsWith('_code') || fieldName === 'code') return MAXLENGTH_DEFAULTS.code;
44
+ if (fieldName.endsWith('_name') || fieldName === 'name') return MAXLENGTH_DEFAULTS.name;
45
+ if (fieldName.includes('email')) return MAXLENGTH_DEFAULTS.email;
46
+ if (fieldName.includes('phone')) return MAXLENGTH_DEFAULTS.phone;
47
+ return MAXLENGTH_DEFAULTS.text;
48
+ }
49
+
50
+ function generatePlaceholder(fieldName, label) {
51
+ if (fieldName.includes('email')) return 'email@example.com';
52
+ if (fieldName.includes('phone')) return '+62xxx';
53
+ if (fieldName.endsWith('_code') || fieldName === 'code') {
54
+ return `Enter ${label.toLowerCase()}`;
55
+ }
56
+ return '';
57
+ }
58
+
59
+ function guessDisplayCol(join) {
60
+ return `${join.tableName}_name`;
61
+ }
62
+
63
+ function makeSkipped(name) {
64
+ return { name, label: '', fieldType: '', skip: true, required: false, inTable: false, tableOrder: null, tableField: null, defaultValue: undefined, extra: {} };
65
+ }
66
+
67
+ class FieldTypeResolver {
68
+ constructor(fieldValidations, parsedQuery, primaryKey) {
69
+ this.parsedQuery = parsedQuery || { selectColumns: [], joins: [], mainTable: '', mainAlias: '' };
70
+ this.primaryKey = String(primaryKey || '');
71
+
72
+ this.validationMap = new Map();
73
+ for (const fv of (fieldValidations || [])) {
74
+ if (fv && typeof fv.name === 'string') {
75
+ this.validationMap.set(fv.name, fv);
76
+ }
77
+ }
78
+
79
+ this.joinMap = new Map();
80
+ for (const join of this.parsedQuery.joins) {
81
+ this.joinMap.set(join.localColumn, join);
82
+ }
83
+
84
+ const joinAliasSet = new Set(this.parsedQuery.joins.map(j => j.tableAlias));
85
+
86
+ this.selectPositions = new Map();
87
+ this.joinDisplayFields = new Map();
88
+ let pkSkipped = false;
89
+ let adjustedPos = 0;
90
+
91
+ for (const col of this.parsedQuery.selectColumns) {
92
+ if (joinAliasSet.has(col.tableAlias)) {
93
+ for (const join of this.parsedQuery.joins) {
94
+ if (join.tableAlias === col.tableAlias) {
95
+ this.joinDisplayFields.set(join.localColumn, col.name);
96
+ adjustedPos += 1;
97
+ this.selectPositions.set(join.localColumn, adjustedPos);
98
+ break;
99
+ }
100
+ }
101
+ } else if (col.name === this.primaryKey && !pkSkipped) {
102
+ pkSkipped = true;
103
+ continue;
104
+ } else {
105
+ adjustedPos += 1;
106
+ this.selectPositions.set(col.name, adjustedPos);
107
+ }
108
+ }
109
+ }
110
+
111
+ resolve(fieldName) {
112
+ const validation = this.validationMap.get(fieldName) || null;
113
+ const valType = (validation && typeof validation.type === 'string') ? validation.type : '';
114
+ const constraints = (validation && validation.constraints && typeof validation.constraints === 'object')
115
+ ? validation.constraints
116
+ : {};
117
+
118
+ // Rule 0a: Audit fields → skip
119
+ if (AUDIT_FIELDS.includes(fieldName)) {
120
+ return makeSkipped(fieldName);
121
+ }
122
+
123
+ // Rule 0b: Display-only field dari JOIN (column milik table_alias join) → skip
124
+ for (const join of this.parsedQuery.joins) {
125
+ if (!join.tableAlias) continue;
126
+ const belongsToJoin = this.parsedQuery.selectColumns.some(
127
+ col => col.tableAlias === join.tableAlias && col.name === fieldName
128
+ );
129
+ if (belongsToJoin) {
130
+ return makeSkipped(fieldName);
131
+ }
132
+ }
133
+
134
+ // Rule 1a (FIX): SEMUA field dengan constraints.primaryKey=true → skip
135
+ // (extended dari Rust source yang hanya skip type=uuid + autoGenerate)
136
+ if (constraints.primaryKey === true) {
137
+ return makeSkipped(fieldName);
138
+ }
139
+
140
+ // Rule 1b: PK by name tanpa validation → skip
141
+ if (fieldName === this.primaryKey && validation === null) {
142
+ return makeSkipped(fieldName);
143
+ }
144
+
145
+ // FK + label + posisi tabel
146
+ const isFk = this.joinMap.has(fieldName);
147
+ const label = generateLabel(fieldName, isFk);
148
+ const inTable = this.selectPositions.has(fieldName);
149
+ const tableOrder = this.selectPositions.get(fieldName) || null;
150
+ const tableField = this.joinDisplayFields.get(fieldName) || null;
151
+ const required = constraints.required === true;
152
+
153
+ // Rule 2: Boolean → checkbox
154
+ if (valType === 'boolean') {
155
+ const textMap = CHECKBOX_TEXT_MAP[fieldName] || CHECKBOX_TEXT_DEFAULT;
156
+ const defaultVal = Object.prototype.hasOwnProperty.call(constraints, 'default')
157
+ ? constraints.default
158
+ : true;
159
+ return {
160
+ name: fieldName,
161
+ label,
162
+ fieldType: 'checkbox',
163
+ skip: false,
164
+ required: false,
165
+ inTable,
166
+ tableOrder,
167
+ tableField: null,
168
+ defaultValue: defaultVal,
169
+ extra: {
170
+ checkboxText: { checked: textMap.checked, unchecked: textMap.unchecked }
171
+ }
172
+ };
173
+ }
174
+
175
+ // Rule 3: FK with JOIN → select (API)
176
+ if (isFk) {
177
+ const join = this.joinMap.get(fieldName);
178
+ const displayCol = guessDisplayCol(join);
179
+ return {
180
+ name: fieldName,
181
+ label,
182
+ fieldType: 'select',
183
+ skip: false,
184
+ required,
185
+ inTable,
186
+ tableOrder,
187
+ tableField,
188
+ defaultValue: undefined,
189
+ extra: {
190
+ dataSource: {
191
+ type: 'api',
192
+ resource: join.tableName,
193
+ select: [join.remoteColumn, displayCol]
194
+ }
195
+ }
196
+ };
197
+ }
198
+
199
+ // Rule 4: Number
200
+ if (valType === 'number') {
201
+ const extra = {};
202
+ for (const key of ['min', 'max', 'step']) {
203
+ if (Object.prototype.hasOwnProperty.call(constraints, key)) {
204
+ extra[key] = constraints[key];
205
+ }
206
+ }
207
+ return {
208
+ name: fieldName,
209
+ label,
210
+ fieldType: 'number',
211
+ skip: false,
212
+ required,
213
+ inTable,
214
+ tableOrder,
215
+ tableField: null,
216
+ defaultValue: undefined,
217
+ extra
218
+ };
219
+ }
220
+
221
+ // Rule 5: String with enum → select (static)
222
+ if (valType === 'string' && Array.isArray(constraints.enum) && constraints.enum.length > 0) {
223
+ const options = constraints.enum
224
+ .filter(v => typeof v === 'string')
225
+ .map(s => ({ value: s, text: snakeToTitle(s) }));
226
+ return {
227
+ name: fieldName,
228
+ label,
229
+ fieldType: 'select',
230
+ skip: false,
231
+ required,
232
+ inTable,
233
+ tableOrder,
234
+ tableField: null,
235
+ defaultValue: undefined,
236
+ extra: {
237
+ dataSource: { type: 'static', options }
238
+ }
239
+ };
240
+ }
241
+
242
+ // Rule 6: Date
243
+ if (fieldName.endsWith('_date') || fieldName === 'date') {
244
+ return {
245
+ name: fieldName,
246
+ label,
247
+ fieldType: 'date',
248
+ skip: false,
249
+ required,
250
+ inTable,
251
+ tableOrder,
252
+ tableField: null,
253
+ defaultValue: undefined,
254
+ extra: {}
255
+ };
256
+ }
257
+
258
+ // Rule 7: Time
259
+ if (fieldName.endsWith('_time') || fieldName === 'time') {
260
+ return {
261
+ name: fieldName,
262
+ label,
263
+ fieldType: 'time',
264
+ skip: false,
265
+ required,
266
+ inTable,
267
+ tableOrder,
268
+ tableField: null,
269
+ defaultValue: undefined,
270
+ extra: {}
271
+ };
272
+ }
273
+
274
+ // Rule 8: Textarea
275
+ const isTextarea = TEXTAREA_FIELDS.includes(fieldName)
276
+ || TEXTAREA_PREFIXES.some(p => fieldName.startsWith(p));
277
+ if (isTextarea) {
278
+ const maxlen = (typeof constraints.maxLength === 'number')
279
+ ? constraints.maxLength
280
+ : MAXLENGTH_DEFAULTS.textarea;
281
+ return {
282
+ name: fieldName,
283
+ label,
284
+ fieldType: 'textarea',
285
+ skip: false,
286
+ required,
287
+ inTable,
288
+ tableOrder,
289
+ tableField: null,
290
+ defaultValue: undefined,
291
+ extra: { rows: 3, maxlength: maxlen }
292
+ };
293
+ }
294
+
295
+ // Rule 9: Default → text
296
+ const maxlen = (typeof constraints.maxLength === 'number')
297
+ ? constraints.maxLength
298
+ : defaultMaxlength(fieldName);
299
+ const extra = { maxlength: maxlen };
300
+ const placeholder = generatePlaceholder(fieldName, label);
301
+ if (placeholder) {
302
+ extra.placeholder = placeholder;
303
+ }
304
+ return {
305
+ name: fieldName,
306
+ label,
307
+ fieldType: 'text',
308
+ skip: false,
309
+ required,
310
+ inTable,
311
+ tableOrder,
312
+ tableField: null,
313
+ defaultValue: undefined,
314
+ extra
315
+ };
316
+ }
317
+ }
318
+
319
+ module.exports = { FieldTypeResolver };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Port dari packages/designer/src/migrators/label_generator.rs.
5
+ * Generator label field dari snake_case ke human-readable format.
6
+ */
7
+
8
+ const { snakeToTitle } = require('./naming');
9
+
10
+ const LABEL_OVERRIDES = {
11
+ is_active: 'Status',
12
+ uom: 'UOM',
13
+ sku: 'SKU',
14
+ email: 'Email',
15
+ phone: 'Phone',
16
+ url: 'URL',
17
+ ip: 'IP',
18
+ id: 'ID'
19
+ };
20
+
21
+ function generateLabel(fieldName, isForeignKey) {
22
+ const name = String(fieldName || '');
23
+
24
+ if (Object.prototype.hasOwnProperty.call(LABEL_OVERRIDES, name)) {
25
+ return LABEL_OVERRIDES[name];
26
+ }
27
+
28
+ if (isForeignKey && name.endsWith('_id')) {
29
+ const stripped = name.slice(0, -3);
30
+ if (stripped.length > 0) {
31
+ return snakeToTitle(stripped);
32
+ }
33
+ }
34
+
35
+ return snakeToTitle(name);
36
+ }
37
+
38
+ module.exports = { generateLabel };