@restforgejs/platform 4.2.8 → 4.3.1

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 (324) hide show
  1. package/SECURITY.md +83 -4
  2. package/bin/sdf-tools.exe +0 -0
  3. package/build-info.json +2 -2
  4. package/cli/consumer-deploy.js +1 -1
  5. package/cli/consumer.js +1 -1
  6. package/generators/cli/dashboard/create.js +4 -1
  7. package/generators/cli/endpoint/create.js +1 -1
  8. package/generators/cli/key/generate.js +2 -1
  9. package/generators/cli/key/revoke.js +2 -1
  10. package/generators/cli/payload/diff.js +3 -2
  11. package/generators/cli/payload/generate.js +3 -2
  12. package/generators/cli/payload/sync.js +3 -2
  13. package/generators/cli/payload/validate.js +3 -2
  14. package/generators/cli/processor/create.js +14 -3
  15. package/generators/cli/project/delete.js +2 -1
  16. package/generators/cli/query/validate.js +3 -2
  17. package/generators/cli/schema/apply.js +3 -2
  18. package/generators/cli/schema/describe.js +3 -2
  19. package/generators/cli/schema/diff.js +3 -2
  20. package/generators/cli/schema/introspect.js +3 -2
  21. package/generators/cli/schema/list.js +3 -2
  22. package/generators/cli/schema/migrate.js +3 -2
  23. package/generators/lib/migration/audit-table-runner.js +213 -215
  24. package/generators/lib/templates/dashboard-catalog.js +1 -437
  25. package/generators/lib/templates/db-connection-env.js +1 -212
  26. package/generators/lib/templates/dbschema-catalog.js +1 -489
  27. package/generators/lib/templates/field-validation-catalog.js +1 -531
  28. package/generators/lib/templates/mysql-template.js +1 -3863
  29. package/generators/lib/templates/oracle-template.js +1 -3915
  30. package/generators/lib/templates/postgres-template.js +1 -5838
  31. package/generators/lib/templates/query-declarative-catalog.js +1 -199
  32. package/generators/lib/templates/sqlite-template.js +1 -3440
  33. package/generators/lib/utils/env-manager.js +6 -0
  34. package/generators/lib/utils/path-validator.js +71 -0
  35. package/generators/lib/validators/payload-validator.js +1 -2
  36. package/integrity-manifest.json +28 -10
  37. package/package.json +11 -3
  38. package/scripts/verify-integrity.js +1 -1
  39. package/server.js +1 -1
  40. package/src/components/handlers/adjust_handler.js +1 -1
  41. package/src/components/handlers/audit_handler.js +1 -1
  42. package/src/components/handlers/delete_handler.js +1 -1
  43. package/src/components/handlers/export_handler.js +1 -1
  44. package/src/components/handlers/import_handler.js +1 -1
  45. package/src/components/handlers/insert_handler.js +1 -1
  46. package/src/components/handlers/update_handler.js +1 -1
  47. package/src/components/handlers/upload_handler.js +1 -1
  48. package/src/components/handlers/workflow_handler.js +1 -1
  49. package/src/components/integrations/webhook.js +1 -1
  50. package/src/consumers/baseConsumer.js +1 -1
  51. package/src/consumers/declarativeMapper.js +1 -1
  52. package/src/consumers/handlers/apiHandler.js +1 -1
  53. package/src/consumers/handlers/consoleHandler.js +1 -1
  54. package/src/consumers/handlers/databaseHandler.js +1 -1
  55. package/src/consumers/handlers/index.js +1 -1
  56. package/src/consumers/handlers/kafkaHandler.js +1 -1
  57. package/src/consumers/index.js +1 -1
  58. package/src/consumers/messageTransformer.js +1 -1
  59. package/src/consumers/validator.js +1 -1
  60. package/src/core/db/dialect/base-dialect.js +1 -1
  61. package/src/core/db/dialect/index.js +1 -1
  62. package/src/core/db/dialect/mysql-dialect.js +1 -1
  63. package/src/core/db/dialect/oracle-dialect.js +1 -1
  64. package/src/core/db/dialect/postgres-dialect.js +1 -1
  65. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  66. package/src/core/db/flatten-helper.js +1 -1
  67. package/src/core/db/query-builder-error.js +1 -1
  68. package/src/core/db/query-builder.js +1 -1
  69. package/src/core/db/relation-helper.js +1 -1
  70. package/src/core/handlers/delete_handler.js +1 -1
  71. package/src/core/handlers/insert_handler.js +1 -1
  72. package/src/core/handlers/update_handler.js +1 -1
  73. package/src/core/models/base-model.js +1 -1
  74. package/src/core/utils/cache-manager.js +1 -1
  75. package/src/core/utils/component-engine.js +1 -1
  76. package/src/core/utils/context-builder.js +1 -1
  77. package/src/core/utils/datetime-formatter.js +1 -1
  78. package/src/core/utils/datetime-parser.js +1 -1
  79. package/src/core/utils/db.js +1 -1
  80. package/src/core/utils/logger.js +1 -1
  81. package/src/core/utils/payload-loader.js +1 -1
  82. package/src/core/utils/security-checks.js +1 -1
  83. package/src/middleware/body-options.js +1 -1
  84. package/src/middleware/cors.js +1 -1
  85. package/src/middleware/idempotency.js +1 -1
  86. package/src/middleware/rate-limiter.js +1 -1
  87. package/src/middleware/request-logger.js +1 -1
  88. package/src/middleware/security-headers.js +1 -1
  89. package/src/models/base-model-mysql.js +1 -1
  90. package/src/models/base-model-oracle.js +1 -1
  91. package/src/models/base-model-sqlite.js +1 -1
  92. package/src/models/base-model.js +1 -1
  93. package/src/pro/caching/redis-client.js +1 -1
  94. package/src/pro/caching/redis-helper.js +1 -1
  95. package/src/pro/consumers/baseConsumer.js +1 -1
  96. package/src/pro/consumers/declarativeMapper.js +1 -1
  97. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  98. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  99. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  100. package/src/pro/consumers/handlers/index.js +1 -1
  101. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  102. package/src/pro/consumers/index.js +1 -1
  103. package/src/pro/consumers/messageTransformer.js +1 -1
  104. package/src/pro/consumers/validator.js +1 -1
  105. package/src/pro/database/base-model-mysql.js +1 -1
  106. package/src/pro/database/base-model-oracle.js +1 -1
  107. package/src/pro/database/base-model-sqlite.js +1 -1
  108. package/src/pro/database/db-mysql.js +1 -1
  109. package/src/pro/database/db-oracle.js +1 -1
  110. package/src/pro/database/db-sqlite.js +1 -1
  111. package/src/pro/excel/excel-generator.js +1 -1
  112. package/src/pro/excel/excel-parser.js +1 -1
  113. package/src/pro/excel/export-service.js +1 -1
  114. package/src/pro/excel/export_handler.js +1 -1
  115. package/src/pro/excel/import-service.js +1 -1
  116. package/src/pro/excel/import-validator.js +1 -1
  117. package/src/pro/excel/import_handler.js +1 -1
  118. package/src/pro/excel/upsert-builder.js +1 -1
  119. package/src/pro/idgen/idgen-routes.js +1 -1
  120. package/src/pro/integrations/lookup-resolver.js +1 -1
  121. package/src/pro/integrations/upload-handler-v2.js +1 -1
  122. package/src/pro/integrations/upload-handler.js +1 -1
  123. package/src/pro/integrations/webhook.js +1 -1
  124. package/src/pro/locking/lock-routes.js +1 -1
  125. package/src/pro/locking/resource-lock-manager.js +1 -1
  126. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  127. package/src/pro/messaging/kafkaService.js +1 -1
  128. package/src/pro/messaging/messagehubService.js +1 -1
  129. package/src/pro/messaging/rabbitmqService.js +1 -1
  130. package/src/pro/scheduler/job-manager.js +1 -1
  131. package/src/pro/scheduler/job-routes.js +1 -1
  132. package/src/pro/scheduler/job-validator.js +1 -1
  133. package/src/pro/storage/base-storage-provider.js +1 -1
  134. package/src/pro/storage/file-metadata-helper.js +1 -1
  135. package/src/pro/storage/index.js +1 -1
  136. package/src/pro/storage/local-storage-provider.js +1 -1
  137. package/src/pro/storage/s3-storage-provider.js +1 -1
  138. package/src/pro/storage/upload-cleanup-job.js +1 -1
  139. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  140. package/src/pro/storage/upload-pending-tracker.js +1 -1
  141. package/src/pro/websocket/broadcast-helper.js +1 -1
  142. package/src/pro/websocket/index.js +1 -1
  143. package/src/pro/websocket/livesync-server.js +1 -1
  144. package/src/pro/websocket/ws-broadcaster.js +1 -1
  145. package/src/services/export-service.js +1 -1
  146. package/src/services/import-service.js +1 -1
  147. package/src/services/kafkaConsumerService.js +1 -1
  148. package/src/services/kafkaService.js +1 -1
  149. package/src/services/messagehubService.js +1 -1
  150. package/src/services/rabbitmqService.js +1 -1
  151. package/src/utils/cache-invalidation-registry.js +1 -1
  152. package/src/utils/cache-manager.js +1 -1
  153. package/src/utils/component-engine.js +1 -1
  154. package/src/utils/config-extractor.js +1 -1
  155. package/src/utils/consumerLogger.js +1 -1
  156. package/src/utils/context-builder.js +1 -1
  157. package/src/utils/dashboard-helpers.js +1 -1
  158. package/src/utils/dateHelper.js +1 -1
  159. package/src/utils/datetime-formatter.js +1 -1
  160. package/src/utils/datetime-parser.js +1 -1
  161. package/src/utils/db-bootstrap.js +1 -1
  162. package/src/utils/db-mysql.js +1 -1
  163. package/src/utils/db-oracle.js +1 -1
  164. package/src/utils/db-sqlite.js +1 -1
  165. package/src/utils/db.js +1 -1
  166. package/src/utils/demo-generator.js +1 -1
  167. package/src/utils/excel-generator.js +1 -1
  168. package/src/utils/excel-parser.js +1 -1
  169. package/src/utils/file-watcher.js +1 -1
  170. package/src/utils/id-generator.js +1 -1
  171. package/src/utils/idempotency-manager.js +1 -1
  172. package/src/utils/import-validator.js +1 -1
  173. package/src/utils/license-client.js +1 -1
  174. package/src/utils/lock-manager.js +1 -1
  175. package/src/utils/logger.js +1 -1
  176. package/src/utils/lookup-resolver.js +1 -1
  177. package/src/utils/payload-loader.js +1 -1
  178. package/src/utils/processor-response.js +1 -1
  179. package/src/utils/rabbitmq.js +1 -1
  180. package/src/utils/redis-client.js +1 -1
  181. package/src/utils/redis-helper.js +1 -1
  182. package/src/utils/request-scope.js +1 -1
  183. package/src/utils/security-checks.js +1 -1
  184. package/src/utils/service-resolver.js +1 -1
  185. package/src/utils/shutdown-coordinator.js +1 -1
  186. package/src/utils/trusted-keys.js +1 -1
  187. package/src/utils/upload-handler.js +1 -1
  188. package/src/utils/upsert-builder.js +1 -1
  189. package/src/utils/workflow-hook-executor.js +1 -1
  190. package/generators/metadata/global.json +0 -58
  191. package/generators/metadata/test-mysql-workbench.json +0 -118
  192. package/generators/metadata/test-mysql.json +0 -56
  193. package/generators/metadata/test-oracle-workbench.json +0 -118
  194. package/generators/metadata/test-oracle.json +0 -56
  195. package/generators/metadata/test-pg-workbench.json +0 -118
  196. package/generators/metadata/test-pg.json +0 -56
  197. package/generators/scripts/obfuscate-source.js +0 -356
  198. package/generators/scripts/validate-catalog.js +0 -430
  199. package/generators/scripts/validate-dbschema-catalog.js +0 -708
  200. package/generators/tests/baseline/mysql/mini_inventory_item/src/models/mini-inventory/item.js +0 -944
  201. package/generators/tests/baseline/mysql/mini_inventory_item/src/modules/mini-inventory/item.js +0 -740
  202. package/generators/tests/baseline/mysql/mini_inventory_item/src/modules/mini-inventory.js +0 -336
  203. package/generators/tests/baseline/oracle/mini_inventory_item/src/models/mini-inventory/item.js +0 -1002
  204. package/generators/tests/baseline/oracle/mini_inventory_item/src/modules/mini-inventory/item.js +0 -740
  205. package/generators/tests/baseline/oracle/mini_inventory_item/src/modules/mini-inventory.js +0 -336
  206. package/generators/tests/baseline/postgres/mini_inventory_item/src/models/mini-inventory/item.js +0 -1333
  207. package/generators/tests/baseline/postgres/mini_inventory_item/src/modules/mini-inventory/item.js +0 -1173
  208. package/generators/tests/baseline/postgres/mini_inventory_item/src/modules/mini-inventory.js +0 -496
  209. package/generators/tests/fixtures/payloads/custom-sensitive.json +0 -27
  210. package/generators/tests/fixtures/payloads/dynamic-search-optout.json +0 -23
  211. package/generators/tests/fixtures/payloads/login-with-password.json +0 -22
  212. package/generators/tests/fixtures/payloads/order-process.json +0 -52
  213. package/generators/tests/fixtures/payloads/with-inline-sql.json +0 -26
  214. package/generators/tests/integration-tahap4b/README.md +0 -145
  215. package/generators/tests/integration-tahap4b/run-concurrent.js +0 -77
  216. package/generators/tests/integration-tahap4b/seed.sql +0 -53
  217. package/generators/tests/integration-tahap4b/verify.sql +0 -110
  218. package/generators/tests/unit/cli/create-dashboard.test.js +0 -505
  219. package/generators/tests/unit/cli/create-processor.test.js +0 -319
  220. package/generators/tests/unit/cli/dispatch-dashboard.test.js +0 -149
  221. package/generators/tests/unit/lib/dashboard-generator.test.js +0 -895
  222. package/generators/tests/unit/lib/dashboard-validator.test.js +0 -354
  223. package/generators/tests/unit/lib/dbschema-kit/apply-executor.test.js +0 -437
  224. package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-introspect.test.js +0 -393
  225. package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-kit-generate-ddl.test.js +0 -104
  226. package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-kit-init.test.js +0 -119
  227. package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-kit-list.test.js +0 -48
  228. package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-kit-migrate.test.js +0 -175
  229. package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-kit-validate.test.js +0 -102
  230. package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-models.test.js +0 -43
  231. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/all-schemas-listing.js +0 -84
  232. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/connection-error.js +0 -13
  233. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/empty.js +0 -12
  234. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/multi-schema.js +0 -124
  235. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/single-schema-inventory.js +0 -64
  236. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/two-tables.js +0 -66
  237. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/migrate-stubs/connection-error.js +0 -9
  238. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/migrate-stubs/partial.js +0 -29
  239. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/migrate-stubs/rollback.js +0 -26
  240. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/migrate-stubs/success.js +0 -43
  241. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/multi-schema/audit/events.js +0 -18
  242. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/multi-schema/inventory/products.js +0 -9
  243. package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/multi-schema/users.js +0 -8
  244. package/generators/tests/unit/lib/dbschema-kit/connection.test.js +0 -112
  245. package/generators/tests/unit/lib/dbschema-kit/ddl-generator.test.js +0 -205
  246. package/generators/tests/unit/lib/dbschema-kit/define-model.test.js +0 -56
  247. package/generators/tests/unit/lib/dbschema-kit/dialect/index.test.js +0 -46
  248. package/generators/tests/unit/lib/dbschema-kit/dialect/mysql.test.js +0 -126
  249. package/generators/tests/unit/lib/dbschema-kit/dialect/oracle.test.js +0 -126
  250. package/generators/tests/unit/lib/dbschema-kit/dialect/postgres.test.js +0 -131
  251. package/generators/tests/unit/lib/dbschema-kit/dialect/sqlite.test.js +0 -126
  252. package/generators/tests/unit/lib/dbschema-kit/driver-loader.test.js +0 -93
  253. package/generators/tests/unit/lib/dbschema-kit/emitters/create-index.test.js +0 -173
  254. package/generators/tests/unit/lib/dbschema-kit/emitters/create-table.test.js +0 -376
  255. package/generators/tests/unit/lib/dbschema-kit/emitters/drop-table.test.js +0 -78
  256. package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/invalid-dialect.env +0 -6
  257. package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/missing-dialect.env +0 -5
  258. package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/missing-host.env +0 -5
  259. package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/oracle-valid.env +0 -6
  260. package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/postgres-valid.env +0 -7
  261. package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/sqlite-valid.env +0 -2
  262. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/category.js +0 -11
  263. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/item_product.js +0 -11
  264. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/stock_inbound.js +0 -24
  265. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/stock_inbound_item.js +0 -28
  266. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/supplier.js +0 -9
  267. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/warehouse.js +0 -9
  268. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-invalid/orphan.js +0 -17
  269. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/master/category.js +0 -11
  270. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/master/item_product.js +0 -11
  271. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/master/supplier.js +0 -9
  272. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/master/warehouse.js +0 -9
  273. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/transactions/stock_inbound.js +0 -24
  274. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/transactions/stock_inbound_item.js +0 -28
  275. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/multi-schema/audit/events.js +0 -18
  276. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/multi-schema/inventory/products.js +0 -9
  277. package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/multi-schema/public/users.js +0 -9
  278. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/duplicate-subfolder/extra/category.js +0 -8
  279. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/duplicate-subfolder/master/category.js +0 -8
  280. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/duplicate-tablename/bar.js +0 -8
  281. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/duplicate-tablename/foo.js +0 -8
  282. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/empty-folder/README.md +0 -1
  283. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/invalid-export/plain.js +0 -3
  284. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/invalid-schema/bad.js +0 -6
  285. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/legacy-pattern/legacy.js +0 -12
  286. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/multi-schema-distinct/audit/products.js +0 -9
  287. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/multi-schema-distinct/inventory/products.js +0 -9
  288. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/multi-schema-duplicate/a/products.js +0 -8
  289. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/multi-schema-duplicate/b/products.js +0 -8
  290. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/nested-deep/a/b/c/deep_table.js +0 -8
  291. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/recursive-multi-folder/.hidden/ignored.js +0 -7
  292. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/recursive-multi-folder/master/category.js +0 -8
  293. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/recursive-multi-folder/master/supplier.js +0 -8
  294. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/recursive-multi-folder/transactions/stock_inbound.js +0 -8
  295. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/recursive-multi-folder/transactions/stock_inbound_item.js +0 -8
  296. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/valid-multiple/category.js +0 -8
  297. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/valid-multiple/item_product.js +0 -9
  298. package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/valid-single/category.js +0 -8
  299. package/generators/tests/unit/lib/dbschema-kit/integration.test.js +0 -217
  300. package/generators/tests/unit/lib/dbschema-kit/introspect-mapper.test.js +0 -403
  301. package/generators/tests/unit/lib/dbschema-kit/ir-builder.test.js +0 -390
  302. package/generators/tests/unit/lib/dbschema-kit/loader.test.js +0 -128
  303. package/generators/tests/unit/lib/dbschema-kit/naming.test.js +0 -170
  304. package/generators/tests/unit/lib/dbschema-kit/parser/shorthand-parser.test.js +0 -237
  305. package/generators/tests/unit/lib/dbschema-kit/schema-printer.test.js +0 -251
  306. package/generators/tests/unit/lib/dbschema-kit/statement-modifier.test.js +0 -105
  307. package/generators/tests/unit/lib/dbschema-kit/statement-splitter.test.js +0 -165
  308. package/generators/tests/unit/lib/dbschema-kit/topological-sort.test.js +0 -135
  309. package/generators/tests/unit/lib/dbschema-kit/validator/check-compatibility-validator.test.js +0 -373
  310. package/generators/tests/unit/lib/dbschema-kit/validator/circular-relation-validator.test.js +0 -454
  311. package/generators/tests/unit/lib/dbschema-kit/validator/cross-model-validator.test.js +0 -512
  312. package/generators/tests/unit/lib/dbschema-kit/validator/enhanced-validate-integration.test.js +0 -390
  313. package/generators/tests/unit/lib/dbschema-kit/validator/naming-convention-validator.test.js +0 -306
  314. package/generators/tests/unit/lib/dbschema-kit/validator/schema-validator.test.js +0 -443
  315. package/generators/tests/unit/lib/dbschema-kit/validator/type-compatibility-validator.test.js +0 -440
  316. package/generators/tests/unit/lib/dbschema-kit/validator/validator-reporter.test.js +0 -172
  317. package/generators/tests/unit/lib/metadata-manager-dashboard.test.js +0 -256
  318. package/generators/tests/unit/lib/payload-validator-fieldpolicy.test.js +0 -240
  319. package/generators/tests/unit/lib/processor-validation-generator.test.js +0 -300
  320. package/generators/tests/unit/lib/sensitive-field-masker.test.js +0 -170
  321. package/generators/tests/unit/lib/sql-table-extractor.test.js +0 -119
  322. package/scripts/generate-integrity-manifest.js +0 -124
  323. package/scripts/snapshot-cli-contracts.js +0 -194
  324. package/scripts/verify-publish.js +0 -56
@@ -1,1333 +0,0 @@
1
- const BaseModel = require('restforgejs/src/models/base-model');
2
- const db = require('restforgejs/src/utils/db');
3
- const fs = require('fs');
4
- const path = require('path');
5
- // AdvancedFilterHelper dihapus — semua filtering ditangani oleh buildObjectFilterClause dan buildComplexWhereClause
6
-
7
- /**
8
- * Item Model - Auto-generated on 2026-04-25 07:34:33
9
- *
10
- * Model untuk item yang mewarisi fungsi-fungsi dari BaseModel
11
- * Table: core.item
12
- * Fields: 12 fields
13
- * Database: PostgreSQL
14
- */
15
- class ItemModel extends BaseModel {
16
- /**
17
- * Constructor
18
- */
19
- constructor() {
20
- // Definisikan validFields - semua field yang valid untuk tabel item
21
- const validFields = [
22
- 'item_id',
23
- 'item_code',
24
- 'item_name',
25
- 'description',
26
- 'uom',
27
- 'unit_price',
28
- 'weight',
29
- 'is_active',
30
- 'created_at',
31
- 'created_by',
32
- 'updated_at',
33
- 'updated_by'
34
- ];
35
-
36
- // Definisikan datatablesWhere sesuai payload
37
- const datatablesWhere = ["item_code","item_name","description","all"];
38
-
39
- // Panggil constructor parent dengan nama tabel, validFields, dan datatablesWhere
40
- super('core.item', validFields, datatablesWhere);
41
-
42
- // Setup primary key dari payload atau fallback ke fieldName pertama
43
- this.primaryKey = 'item_id';
44
-
45
- // Setup viewName untuk operasi read jika berbeda dari tableName
46
- this.viewName = 'core.item';
47
- this.readSource = 'core.item'; // Source untuk operasi read (get, list, lookup, datatables)
48
- this.writeSource = 'core.item'; // Source untuk operasi write (add, update, delete)
49
-
50
- // Flag untuk self-documenting API (endpoint /info)
51
- this.hasViewQuery = false;
52
- this.hasExportQuery = false;
53
-
54
- // Load advanced query templates
55
- this.advancedQueryTemplates = this.loadAdvancedQueryTemplates();
56
-
57
- this.validationConfig = {}; // No field validation config
58
-
59
- // Model metadata
60
- this.modelMetadata = {
61
- endpointName: 'item',
62
- moduleName: 'mini-inventory',
63
- tableName: 'core.item',
64
- viewName: 'core.item',
65
- fieldCount: 12,
66
- databaseType: 'postgres',
67
- generated: '2026-04-25 07:34:33',
68
- features: ["custom_where"]
69
- };
70
- }
71
-
72
- /**
73
- * Load advanced query templates dari file
74
- * @returns {Object} Templates SQL untuk advanced queries
75
- */
76
- loadAdvancedQueryTemplates() {
77
- const templates = {};
78
- // No advanced queries defined
79
-
80
- return templates;
81
- }
82
-
83
- /**
84
- * Override getListQuery untuk menyesuaikan dengan kebutuhan item
85
- * @param {Object} options - Query options
86
- * @returns {string} SQL query dasar untuk item
87
- */
88
- getListQuery(options = {}) {
89
- // Load query dasar dengan placeholder replacement
90
- let baseQuery = `select item_id, item_code, item_name, description, uom, unit_price, weight, is_active, created_at, created_by, updated_at, updated_by from core.item`.trim();
91
-
92
- // Replace any remaining placeholders - gunakan readSource untuk operasi read
93
- baseQuery = baseQuery.replace(/${tableName}/g, this.readSource);
94
- baseQuery = baseQuery.replace(/${this.table}/g, this.readSource);
95
-
96
- return baseQuery;
97
- }
98
-
99
- /**
100
- * Override getReadQuery untuk endpoint /read
101
- * Prioritas: viewName -> viewQuery -> tableName (SELECT * FROM readSource)
102
- * @param {Object} options - Query options
103
- * @returns {string} SQL query dasar untuk /read
104
- */
105
- getReadQuery(options = {}) {
106
- // Priority 1: viewName (real database view)
107
- if (this.viewName && this.viewName !== this.table) {
108
- return 'SELECT * FROM ' + this.viewName;
109
- }
110
- // Fallback: gunakan readSource langsung (semua kolom tersedia)
111
- return 'SELECT * FROM ' + this.readSource;
112
- }
113
-
114
- /**
115
- * Override getDatatables untuk mendukung filter object format
116
- * @param {Object} options - Parameter dari request
117
- * @returns {Object} Hasil query dengan format DataTables
118
- */
119
- async getDatatables(options) {
120
- try {
121
- // Check cache first (if enabled)
122
- const cachedResult = await this.getCachedDatatables(options);
123
- if (cachedResult) {
124
- return cachedResult;
125
- }
126
-
127
- const {
128
- searchValue = '',
129
- searchBy = 'all',
130
- perPage = 10,
131
- start = 0,
132
- sort_columns = [],
133
- filters = {},
134
-
135
- where = null
136
- } = options;
137
-
138
- // Resolve sort columns dengan prioritas: sort_columns > order[0][column] > default
139
- let resolvedSortColumns = sort_columns;
140
-
141
- // Fallback: cek format DataTables bawaan (order[0][column] dan order[0][dir])
142
- if ((!resolvedSortColumns || resolvedSortColumns.length === 0) &&
143
- options['order[0][column]'] !== undefined && options['order[0][dir]'] !== undefined) {
144
- const columnIndex = parseInt(options['order[0][column]']);
145
- const direction = options['order[0][dir]'];
146
-
147
- if (columnIndex >= 0 && columnIndex < this.validFields.length) {
148
- resolvedSortColumns = [{ column: this.validFields[columnIndex], direction: direction.toUpperCase() }];
149
- }
150
- }
151
-
152
- // 1. Mendapatkan query dasar
153
- const baseQuery = this.getListQuery(options);
154
-
155
- // 2. Membuat where clause berdasarkan search dan filter
156
- let whereClause = this.buildWhereClause(searchValue, searchBy);
157
-
158
- // 3. Tambahkan filter object jika ada
159
- const filterClause = this.buildObjectFilterClause(filters);
160
-
161
- if (filterClause) {
162
- if (whereClause) {
163
- whereClause += ' AND ' + filterClause;
164
- } else {
165
- whereClause = 'WHERE ' + filterClause;
166
- }
167
- }
168
-
169
- // 4. Proses parameter where dengan format advanced conditions
170
- let whereParams = [];
171
- if (where && (Array.isArray(where) || (where.conditions && Array.isArray(where.conditions)))) {
172
- try {
173
- let params = [];
174
- let paramIndex = 1;
175
- const whereResult = this.buildComplexWhereClause(where, params, paramIndex);
176
- if (whereResult.sql) {
177
- if (whereClause) {
178
- whereClause += ' AND (' + whereResult.sql + ')';
179
- } else {
180
- whereClause = 'WHERE ' + whereResult.sql;
181
- }
182
- whereParams = whereResult.params;
183
- }
184
- } catch (e) {
185
- const error = new Error('Invalid where conditions: ' + e.message);
186
- error.statusCode = 400;
187
- throw error;
188
- }
189
- }
190
-
191
- // Check if query needs subquery wrapping (CTE or JOIN)
192
- const isCteQuery = baseQuery.toLowerCase().trim().startsWith('with');
193
- const hasJoin = /\b(inner|left|right|cross|full)\s+join\b/i.test(baseQuery) || /\bjoin\b/i.test(baseQuery);
194
- const needsSubquery = isCteQuery || hasJoin;
195
-
196
- // 4. Menghitung total data keseluruhan - gunakan readSource untuk read operations
197
- const countTotalQuery = needsSubquery ?
198
- 'SELECT COUNT(*) as total FROM (' + baseQuery + ') base_query' :
199
- 'SELECT COUNT(*) as total FROM ' + this.readSource;
200
- const countTotalResult = await db.executeQuery(countTotalQuery);
201
- const totalRecords = countTotalResult && countTotalResult[0] ? parseInt(countTotalResult[0].total) : 0;
202
-
203
- // 5. Menghitung jumlah data terfilter - gunakan readSource untuk read operations
204
- let filteredRecords = totalRecords;
205
- if (whereClause) {
206
- // Always wrap CTE/JOIN query for counting with filters
207
- const countFilteredQuery = needsSubquery ?
208
- 'SELECT COUNT(*) as total FROM (' + baseQuery + ') base_query ' + whereClause :
209
- 'SELECT COUNT(*) as total FROM ' + this.readSource + ' ' + whereClause;
210
- console.log('Count Filtered Query:', countFilteredQuery);
211
- console.log('Count Filtered Parameters:', whereParams);
212
- const countFilteredResult = await db.executeQuery(countFilteredQuery, whereParams);
213
- filteredRecords = countFilteredResult && countFilteredResult[0] ? parseInt(countFilteredResult[0].total) : 0;
214
- }
215
-
216
- // 6. Membuat order clause menggunakan buildSortColumnsClause
217
- const orderClause = this.buildSortColumnsClause(resolvedSortColumns);
218
-
219
- // 7. Menambahkan pagination
220
- const limitClause = ` LIMIT ${perPage} OFFSET ${start}`;
221
-
222
- // 8. Menjalankan query final - wrap CTE/JOIN query to avoid column ambiguity
223
- const query = needsSubquery ?
224
- 'SELECT * FROM (' + baseQuery + ') base_query ' + (whereClause || '') + orderClause + limitClause :
225
- baseQuery + " " + whereClause + orderClause + limitClause;
226
- console.log('Final Query:', query);
227
- console.log('Query Parameters:', whereParams);
228
- const data = await db.executeQuery(query, whereParams);
229
-
230
- const result = {
231
- draw: parseInt(options.draw || '1', 10),
232
- recordsTotal: totalRecords,
233
- recordsFiltered: filteredRecords,
234
- data: data || []
235
- };
236
-
237
- // Cache the result (if enabled)
238
- await this.setCachedDatatables(options, result);
239
-
240
- return result;
241
- } catch (error) {
242
- console.error('Error in getDatatables:', error);
243
- throw error;
244
- }
245
- }
246
-
247
- /**
248
- * Get data list dengan manual pagination untuk endpoint /list
249
- * @param {Object} options - Parameter dari request list
250
- * @returns {Object} Hasil query dengan format list pagination
251
- */
252
- async getList(options) {
253
- try {
254
- const {
255
- page = null,
256
- perPage = 10,
257
- offset = 0,
258
- searchValue = '',
259
- searchBy = 'code',
260
- sort_columns = [],
261
- where = null,
262
- select = null,
263
- limit = 1000
264
- } = options;
265
-
266
- const paginate = page !== null;
267
-
268
- // Cache: Check if data exists in cache
269
- const scInfo = sort_columns && sort_columns.length > 0
270
- ? sort_columns.map(s => `${s.column}:${s.direction}`).join(',')
271
- : 'default';
272
- const cacheInfo = `page:${page}, perPage:${perPage}, sort:${scInfo}, search:${searchValue || 'none'}${where ? ', where:yes' : ''}`;
273
- const cachedResult = await this.getCachedList(options);
274
- if (cachedResult) {
275
- console.log(`[Cache] HIT for list - ${cacheInfo}`);
276
- return cachedResult;
277
- }
278
- console.log(`[Cache] MISS for list - ${cacheInfo}`);
279
-
280
- // 1. Mendapatkan query dasar
281
- let baseQuery;
282
- if (select && Array.isArray(select) && select.length > 0) {
283
- const selectedValidColumns = select.filter(col => this.validFields.includes(col));
284
- if (selectedValidColumns.length > 0) {
285
- baseQuery = 'SELECT ' + selectedValidColumns.join(', ') + ' FROM ' + this.readSource;
286
- } else {
287
- baseQuery = 'SELECT * FROM ' + this.readSource;
288
- }
289
- } else {
290
- baseQuery = this.getReadQuery(options);
291
- }
292
-
293
- // 1b. Deteksi apakah query mengandung JOIN atau CTE (perlu subquery wrapping)
294
- const isCteQuery = baseQuery.toLowerCase().trim().startsWith('with');
295
- const hasJoin = /\b(inner|left|right|cross|full)\s+join\b/i.test(baseQuery) || /\bjoin\b/i.test(baseQuery);
296
- const needsSubquery = isCteQuery || hasJoin;
297
-
298
- // 2. Build WHERE clause untuk search
299
- let whereClause = '';
300
- if (searchValue && searchValue.trim() !== '') {
301
- const escapedSearchValue = searchValue.replace(/'/g, "''");
302
- const likeValue = `%${escapedSearchValue}%`;
303
- whereClause = `WHERE UPPER(${searchBy}) LIKE UPPER('${likeValue}')`;
304
- }
305
-
306
- // 3. Proses parameter where dengan format advanced conditions
307
- let whereParams = [];
308
- if (where && (Array.isArray(where) || (where.conditions && Array.isArray(where.conditions)))) {
309
- try {
310
- let params = [];
311
- let paramIndex = 1;
312
- const whereResult = this.buildComplexWhereClause(where, params, paramIndex);
313
- if (whereResult.sql) {
314
- if (whereClause) {
315
- whereClause += ' AND (' + whereResult.sql + ')';
316
- } else {
317
- whereClause = 'WHERE ' + whereResult.sql;
318
- }
319
- whereParams = whereResult.params;
320
- }
321
- } catch (e) {
322
- const error = new Error('Invalid where conditions: ' + e.message);
323
- error.statusCode = 400;
324
- throw error;
325
- }
326
- }
327
-
328
- // 4. Menghitung total data keseluruhan (tanpa filter)
329
- const countTotalQuery = needsSubquery
330
- ? 'SELECT COUNT(*) as total FROM (' + baseQuery + ') base_query'
331
- : 'SELECT COUNT(*) as total FROM ' + this.readSource;
332
- const countTotalResult = await db.executeQuery(countTotalQuery);
333
- const totalRecords = countTotalResult && countTotalResult[0] ? parseInt(countTotalResult[0].total) : 0;
334
-
335
- // 5. Menghitung jumlah data terfilter (jika ada where/search)
336
- let filteredRecords = totalRecords;
337
- if (whereClause) {
338
- const countFilteredQuery = needsSubquery
339
- ? 'SELECT COUNT(*) as total FROM (' + baseQuery + ') base_query ' + whereClause
340
- : 'SELECT COUNT(*) as total FROM ' + this.readSource + ' ' + whereClause;
341
- const countFilteredResult = await db.executeQuery(countFilteredQuery, whereParams.length > 0 ? whereParams : undefined);
342
- filteredRecords = countFilteredResult && countFilteredResult[0] ? parseInt(countFilteredResult[0].total) : 0;
343
- }
344
-
345
- // 6. Build ORDER BY clause
346
- const orderClause = this.buildSortColumnsClause(sort_columns);
347
-
348
- // 7. Build LIMIT dan OFFSET clause (kondisional berdasarkan mode)
349
- let limitClause;
350
- if (paginate) {
351
- limitClause = ` LIMIT ${perPage} OFFSET ${offset}`;
352
- } else {
353
- limitClause = ` LIMIT ${limit}`;
354
- }
355
-
356
- // 8. Menjalankan query final untuk data (subquery wrapping untuk JOIN/CTE)
357
- const query = needsSubquery
358
- ? 'SELECT * FROM (' + baseQuery + ') base_query ' + whereClause + orderClause + limitClause
359
- : baseQuery + ' ' + whereClause + orderClause + limitClause;
360
- console.log(`List SQL Query: ${query}`);
361
- console.log('List Query Parameters:', whereParams);
362
- const data = await db.executeQuery(query, whereParams.length > 0 ? whereParams : undefined);
363
-
364
- const result = {
365
- data: data || [],
366
- totalRecords: filteredRecords,
367
- totalUnfiltered: totalRecords
368
- };
369
-
370
- if (paginate) {
371
- result.page = page;
372
- result.perPage = perPage;
373
- }
374
-
375
- // Cache: Store result in cache
376
- await this.setCachedList(options, result);
377
- console.log(`[Cache] SET for list - ${cacheInfo}`);
378
-
379
- return result;
380
- } catch (error) {
381
- console.error('Error in getList:', error);
382
- throw error;
383
- }
384
- }
385
-
386
- /**
387
- * Membangun WHERE clause dari filter object
388
- * @param {Object} filters - Object filter dengan format {column: value}
389
- * @returns {string} WHERE clause SQL atau empty string
390
- */
391
- buildObjectFilterClause(filters) {
392
- if (!filters || typeof filters !== 'object' || Object.keys(filters).length === 0) {
393
- return '';
394
- }
395
-
396
- const conditions = [];
397
-
398
- for (const [column, value] of Object.entries(filters)) {
399
- // Validasi kolom harus ada dalam validFields
400
- if (this.validFields.includes(column) && value !== null && value !== undefined && value !== '') {
401
- // Escape value untuk mencegah SQL injection
402
- const escapedValue = value.toString().replace(/'/g, "''");
403
- conditions.push(`${column} = '${escapedValue}'`);
404
- }
405
- }
406
-
407
- return conditions.length > 0 ? conditions.join(' AND ') : '';
408
- }
409
-
410
-
411
-
412
-
413
-
414
-
415
- /**
416
- * Mendapatkan FROM clause untuk lookup berdasarkan prioritas resolusi sumber data
417
- * Prioritas: viewName → viewQuery → tableName (konsisten dengan /read dan /first)
418
- * @returns {string} FROM clause dengan alias 'a'
419
- */
420
- getLookupSource() {
421
- const readQuery = this.getReadQuery();
422
- const simpleQuery = 'SELECT * FROM ' + this.readSource;
423
- if (readQuery.trim() !== simpleQuery) {
424
- // viewName atau viewQuery aktif — bungkus sebagai subquery
425
- return '(' + readQuery + ') a';
426
- }
427
- return this.readSource + ' a';
428
- }
429
-
430
- /**
431
- * Override getLookupData untuk menggunakan kolom 'item_id' yang benar
432
- * @param {string} search - Kata kunci pencarian
433
- * @returns {Array} Array objek hasil lookup
434
- */
435
- async getLookupData(search) {
436
- try {
437
- const query = 'SELECT item_id, item_id FROM ' + this.getLookupSource() + ' WHERE upper(a.item_id) LIKE upper($1) ORDER BY a.item_id';
438
-
439
- const params = [`%${search || ''}%`];
440
-
441
- const data = await db.executeQuery(query, params);
442
-
443
- const result = data.map(item => ({
444
- id: item.item_id,
445
- text: item.item_id
446
- }));
447
-
448
- return result;
449
- } catch (error) {
450
- console.error('Error in getLookupData (ItemModel):', error);
451
- throw error;
452
- }
453
- }
454
-
455
- /**
456
- * Dynamic lookup dengan support filtering tambahan dan pencarian multi-field
457
- * @param {string} search - Kata kunci pencarian
458
- * @param {Object} extraFilters - Filter tambahan (misal: company_id)
459
- * @returns {Array} Array objek hasil lookup
460
- */
461
- async getLookupDataDynamic(search, extraFilters = {}) {
462
- try {
463
- // Gunakan custom lookup config jika ada, fallback ke textFields detection
464
- const lookupConfig = null;
465
- const textFields = lookupConfig && lookupConfig.searchFields.length > 0
466
- ? lookupConfig.searchFields
467
- : ["item_code","item_name"];
468
-
469
- let whereConditions = [];
470
- let params = [];
471
- let paramIndex = 1;
472
-
473
- // Add search conditions untuk text fields
474
- if (search && search.trim()) {
475
- const searchConditions = textFields.map(field => {
476
- return `upper(a.${field}) LIKE upper($${paramIndex++})`;
477
- });
478
- whereConditions.push(`(${searchConditions.join(' OR ')})`);
479
-
480
- // Add search parameter for each text field
481
- textFields.forEach(() => {
482
- params.push(`%${search.trim()}%`);
483
- });
484
- }
485
-
486
- // Add extra filters
487
- for (const [key, value] of Object.entries(extraFilters)) {
488
- if (value && ["item_id","item_code","item_name","description","uom","unit_price","weight","is_active","created_at","created_by","updated_at","updated_by"].includes(key)) {
489
- whereConditions.push(`a.${key} = $${paramIndex++}`);
490
- params.push(value);
491
- }
492
- }
493
-
494
- // Build final query - gunakan custom select jika ada
495
- const idField = lookupConfig ? lookupConfig.idField : 'item_id';
496
- const textField = lookupConfig && lookupConfig.hasCustomText
497
- ? lookupConfig.textField
498
- : textFields[0];
499
-
500
- let query = `SELECT ${idField}, ${textField} FROM ${this.getLookupSource()}`;
501
- if (whereConditions.length > 0) {
502
- query += ` WHERE ${whereConditions.join(' AND ')}`;
503
- }
504
- query += ` ORDER BY ${lookupConfig && lookupConfig.hasCustomText ? '2' : 'a.' + textFields[0]}`;
505
-
506
- console.log('=== DEBUG DYNAMIC LOOKUP ===');
507
- console.log('Query:', query);
508
- console.log('Params:', params);
509
- console.log('Extra filters:', extraFilters);
510
- console.log('Lookup config:', lookupConfig);
511
- console.log('=== END DEBUG ===');
512
-
513
- const data = await db.executeQuery(query, params);
514
-
515
- return data.map(item => ({
516
- id: item[idField] || item.item_id,
517
- text: item[lookupConfig && lookupConfig.hasCustomText ? 'display_text' : textFields[0]] || ''
518
- }));
519
- } catch (error) {
520
- console.error('Error in getLookupDataDynamic (ItemModel):', error);
521
- throw error;
522
- }
523
- }
524
-
525
- /**
526
- * Override getStaticLookupData untuk menggunakan kolom 'item_id' yang benar
527
- * @param {string} selectedTag - ID yang dipilih
528
- * @returns {Array} Array objek hasil lookup
529
- */
530
- async getStaticLookupData(selectedTag) {
531
- try {
532
- // Check cache first (if enabled) - cache tanpa selectedTag karena data sama
533
- const cacheOptions = { type: 'static' };
534
- const cachedResult = await this.getCachedLookup(cacheOptions, 'static');
535
- if (cachedResult) {
536
- // Apply selectedTag to cached result
537
- return cachedResult.map(item => {
538
- if (item.id === selectedTag) {
539
- return { ...item, selected: 'true' };
540
- }
541
- return { id: item.id, text: item.text };
542
- });
543
- }
544
-
545
- const query = 'SELECT item_id, item_id FROM ' + this.getLookupSource() + ' ORDER BY a.item_id';
546
-
547
- const data = await db.executeQuery(query);
548
-
549
- // Cache result tanpa selected flag
550
- const cacheData = data.map(item => ({
551
- id: item.item_id,
552
- text: item.item_id
553
- }));
554
- await this.setCachedLookup(cacheOptions, cacheData, 'static');
555
-
556
- // Return dengan selected flag
557
- return data.map(item => {
558
- const result = {
559
- id: item.item_id,
560
- text: item.item_id
561
- };
562
-
563
- if (item.item_id === selectedTag) {
564
- result.selected = 'true';
565
- }
566
-
567
- return result;
568
- });
569
- } catch (error) {
570
- console.error('Error in getStaticLookupData (ItemModel):', error);
571
- throw error;
572
- }
573
- }
574
-
575
- /**
576
- * Method untuk lookup data dengan filtering (where clause) dan custom select
577
- * @param {Object} options - Options dengan where dan select
578
- * @returns {Array} Array objek hasil lookup dengan format {id, text}
579
- */
580
- async getLookupDataWithFilter(options) {
581
- try {
582
- // Check cache first (if enabled)
583
- const cacheOptions = { ...options, type: 'filter' };
584
- const cachedResult = await this.getCachedLookup(cacheOptions, 'filter');
585
- if (cachedResult) {
586
- return cachedResult;
587
- }
588
-
589
- const selectColumns = options.select || ['item_id', 'item_id'];
590
- let params = [];
591
- let paramIndex = 1;
592
-
593
- // Parse dan validasi select columns untuk support SQL expressions
594
- const validTextFields = ["item_code","item_name"];
595
- let selectClause = 'item_id';
596
- let textField = 'item_id';
597
- let aliasField = null;
598
-
599
- // Proses setiap column dalam select
600
- for (const column of selectColumns) {
601
- if (column.toLowerCase() === 'item_id'.toLowerCase()) {
602
- continue; // primary key sudah ada
603
- }
604
-
605
- // Check jika ada SQL expression dengan alias (menggunakan AS)
606
- const aliasRegex = new RegExp('(.+)\\s+as\\s+(\\w+)$', 'i');
607
- const aliasMatch = column.match(aliasRegex);
608
- if (aliasMatch) {
609
- const expression = aliasMatch[1].trim();
610
- const alias = aliasMatch[2].trim();
611
- selectClause += `, ${expression} AS ${alias}`;
612
- textField = alias;
613
- aliasField = alias;
614
- break; // gunakan yang pertama sebagai text field
615
- }
616
-
617
- // Check jika simple field name
618
- if (validTextFields.includes(column) || column === 'item_id') {
619
- selectClause += `, ${column}`;
620
- textField = column;
621
- break; // gunakan yang pertama sebagai text field
622
- }
623
-
624
- // Jika bukan recognized field, masih tambahkan (mungkin computed column)
625
- selectClause += `, ${column}`;
626
- textField = column;
627
- }
628
-
629
- // Bangun query SELECT dengan support expressions
630
- let query = `SELECT ${selectClause} FROM ${this.getLookupSource()} `;
631
-
632
- // Bangun WHERE clause jika ada dan tidak kosong
633
- if ((options.where && Array.isArray(options.where) && options.where.length > 0) ||
634
- (options.where && options.where.conditions && Array.isArray(options.where.conditions) && options.where.conditions.length > 0)) {
635
- try {
636
- const whereResult = this.buildComplexWhereClause(options.where, params, paramIndex);
637
- query += `WHERE ${whereResult.sql} `;
638
- params = whereResult.params;
639
- } catch (e) {
640
- const error = new Error('Invalid where conditions: ' + e.message);
641
- error.statusCode = 400;
642
- throw error;
643
- }
644
- }
645
-
646
- // Handle sort_columns jika ada
647
- if (options.sort_columns && Array.isArray(options.sort_columns) && options.sort_columns.length > 0) {
648
- const orderParts = options.sort_columns.map(item => {
649
- const column = item.column;
650
- const direction = (item.direction || 'ASC').toUpperCase();
651
- if (!column) return null;
652
- if (!this.validFields.includes(column) && column !== 'item_id') return null;
653
- if (direction !== 'ASC' && direction !== 'DESC') return null;
654
- return `${column} ${direction}`;
655
- }).filter(Boolean);
656
-
657
- if (orderParts.length === 0) {
658
- const error = new Error('No valid sort columns provided');
659
- error.statusCode = 400;
660
- throw error;
661
- }
662
- query += `ORDER BY ${orderParts.join(', ')}`;
663
- } else {
664
- // Order by text field (gunakan alias jika ada) - default behavior
665
- query += `ORDER BY ${aliasField || textField}`;
666
- }
667
-
668
- console.log('=== DEBUG ITEM LOOKUP WITH FILTER ===');
669
- console.log('Final SQL:', query);
670
- console.log('Parameters:', params);
671
- console.log('Selected columns:', selectColumns);
672
- console.log('Sort columns:', options.sort_columns || 'none');
673
- console.log('Valid fields for ordering:', this.validFields);
674
- console.log('Text field:', textField);
675
- console.log('Alias field:', aliasField);
676
- console.log('=== END DEBUG ===');
677
-
678
- // Eksekusi query
679
- const data = await db.executeQuery(query, params);
680
-
681
- // Format hasil untuk lookup (id dan text) - gunakan alias jika ada
682
- const textFieldName = aliasField || textField;
683
- const result = data.map(item => ({
684
- id: item.item_id,
685
- text: item[textFieldName] || item.item_id || item.name || item.code || item.description || ''
686
- }));
687
-
688
- // Cache the result (if enabled)
689
- await this.setCachedLookup(cacheOptions, result, 'filter');
690
-
691
- return result;
692
-
693
- } catch (error) {
694
- console.error('Error in getLookupDataWithFilter (ItemModel):', error);
695
- throw error;
696
- }
697
- }
698
-
699
-
700
- /**
701
- * Get field mapping untuk berbagai operasi
702
- * @returns {Object} Field mapping object
703
- */
704
- getFieldMapping() {
705
- return {
706
- allFields: this.validFields,
707
- textFields: ["item_code","item_name"],
708
- dateFields: ["created_at","created_by","updated_at","updated_by"],
709
- requiredFields: ["item_code","item_name"],
710
- primaryTextField: 'item_id',
711
- searchableFields: this.getSearchableColumns ? this.getSearchableColumns().map(col => col.name) : []
712
- };
713
- }
714
-
715
- /**
716
- * Override formatResponseData untuk item
717
- * @param {Object} data - Data dari database
718
- * @returns {Object} Data yang sudah diformat untuk response item
719
- */
720
- formatResponseData(data) {
721
- // Gunakan parent method yang sudah include datetime formatting
722
- return super.formatResponseData(data);
723
- }
724
-
725
- /**
726
- * Execute advanced query berdasarkan nama
727
- * @param {string} queryName - Nama query dari advancedQueryTemplates
728
- * @param {Object} params - Parameter untuk query
729
- * @returns {Array} Hasil query
730
- */
731
- async executeAdvancedQuery(queryName, params = {}) {
732
- if (!this.advancedQueryTemplates[queryName]) {
733
- throw new Error(`Advanced query '${queryName}' not found. Available queries: ${Object.keys(this.advancedQueryTemplates).join(', ')}`);
734
- }
735
-
736
- try {
737
- let query = this.advancedQueryTemplates[queryName];
738
-
739
- // Replace placeholders
740
- query = query.replace(/${tableName}/g, this.table);
741
- query = query.replace(/${this.table}/g, this.table);
742
-
743
- // Replace parameter placeholders
744
- for (const [key, value] of Object.entries(params)) {
745
- const placeholder = new RegExp(`\$\{params\\.${key}\}`, 'g');
746
- query = query.replace(placeholder, value);
747
- }
748
-
749
- console.log(`Executing advanced query '${queryName}': ${query}`);
750
-
751
- const result = await db.executeQuery(query);
752
- return result;
753
- } catch (error) {
754
- console.error(`Error executing advanced query '${queryName}':`, error);
755
- throw error;
756
- }
757
- }
758
-
759
- /**
760
- * Validate data before insert/update operations
761
- * @param {Object} data - Data yang akan divalidasi
762
- * @param {string} operation - Operasi (insert/update)
763
- * @returns {Object} Validation result
764
- */
765
- async validateData(data, operation = 'insert') {
766
- const result = {
767
- isValid: true,
768
- errors: [],
769
- warnings: [],
770
- sanitizedData: {}
771
- };
772
-
773
- try {
774
- // Check if we have fieldValidation config
775
- const hasFieldValidation = this.validationConfig && Object.keys(this.validationConfig).length > 0;
776
-
777
- if (hasFieldValidation) {
778
- // Loop semua field yang ada di validationConfig
779
- for (const fieldName in this.validationConfig) {
780
- let value = data[fieldName];
781
- const config = this.validationConfig[fieldName];
782
- const constraints = config.constraints || {};
783
-
784
- // Auto-generate value jika autoGenerate dan nilai kosong.
785
- // String dan uuid diperlakukan sama: UUID v7 via uuid package
786
- // (konsisten lintas dialect; cocok dengan konvensi payload category.json
787
- // yang memakai type: "string" dengan constraint autoGenerate + primaryKey).
788
- if (operation === 'insert' && constraints.autoGenerate && (!value || value === '')) {
789
- if (config.type === 'uuid' || config.type === 'string') {
790
- value = require('uuid').v7();
791
- data[fieldName] = value; // Update data asli juga
792
- } else if (config.type === 'timestamp' || config.type === 'datetime') {
793
- value = new Date().toISOString();
794
- data[fieldName] = value; // Update data asli juga
795
- } else if (config.type === 'date') {
796
- value = new Date().toISOString().split('T')[0];
797
- data[fieldName] = value; // Update data asli juga
798
- }
799
- }
800
-
801
- // Validate per-field dengan constraints
802
- const fieldResult = await this.validateFieldConstraints(fieldName, value, operation);
803
-
804
- // Accumulate errors
805
- if (!fieldResult.valid) {
806
- result.isValid = false;
807
- result.errors.push(...fieldResult.errors);
808
- }
809
-
810
- // Accumulate warnings
811
- if (fieldResult.warnings && fieldResult.warnings.length > 0) {
812
- result.warnings.push(...fieldResult.warnings);
813
- }
814
-
815
- // Set sanitized value
816
- if (fieldResult.sanitized !== undefined) {
817
- result.sanitizedData[fieldName] = fieldResult.sanitized;
818
- }
819
- }
820
-
821
- // Validate field yang tidak ada di validationConfig (backward compatibility)
822
- for (const field of this.validFields) {
823
- if (!this.validationConfig[field] && data[field] !== undefined) {
824
- // Fallback ke generic sanitization
825
- result.sanitizedData[field] = this.sanitizeFieldValue(field, data[field]);
826
- }
827
- }
828
-
829
- // Cross-field validation (contoh: before/after date)
830
- if (result.isValid) {
831
- const crossFieldResult = await this._validateCrossFieldConstraints(result.sanitizedData, operation);
832
- if (!crossFieldResult.valid) {
833
- result.isValid = false;
834
- result.errors.push(...crossFieldResult.errors);
835
- }
836
- }
837
-
838
- } else {
839
- // Fallback: Tidak ada fieldValidation - gunakan generic validation
840
- for (const field of this.validFields) {
841
- const value = data[field];
842
-
843
- // Required field validation untuk insert
844
- if (operation === 'insert' && (field === 'id' || field === 'name' || field === 'nama')) {
845
- if (value === undefined || value === null || value === '') {
846
- if (field !== 'id') { // ID bisa auto-generated
847
- result.errors.push(`Field '${field}' is required for ${operation} operation`);
848
- result.isValid = false;
849
- }
850
- }
851
- }
852
-
853
- // Sanitize dan validate value jika ada
854
- if (value !== undefined && value !== null) {
855
- result.sanitizedData[field] = this.sanitizeFieldValue(field, value);
856
- }
857
- }
858
-
859
- // Generic email validation
860
- if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
861
- result.errors.push('Invalid email format');
862
- result.isValid = false;
863
- }
864
- }
865
-
866
- } catch (error) {
867
- result.errors.push(`Validation error: ${error.message}`);
868
- result.isValid = false;
869
- }
870
-
871
- return result;
872
- }
873
-
874
- /**
875
- * Sanitize field value berdasarkan tipe field (generic fallback)
876
- * @param {string} fieldName - Nama field
877
- * @param {*} value - Nilai field
878
- * @returns {*} Sanitized value
879
- */
880
- sanitizeFieldValue(fieldName, value) {
881
- if (typeof value === 'string') {
882
- // Trim whitespace
883
- value = value.trim();
884
-
885
- // Escape special characters untuk mencegah injection
886
- value = value.replace(/[<>]/g, '');
887
-
888
- // Truncate jika terlalu panjang
889
- if (value.length > 255) {
890
- value = value.substring(0, 255);
891
- }
892
- }
893
-
894
- return value;
895
- }
896
-
897
- /**
898
- * Validate field dengan constraints
899
- * @param {string} fieldName - Nama field
900
- * @param {*} value - Nilai field
901
- * @param {string} operation - Operation (insert/update)
902
- * @returns {Object} Validation result {valid, errors, warnings, sanitized}
903
- */
904
- async validateFieldConstraints(fieldName, value, operation = 'insert') {
905
- const config = this.validationConfig[fieldName];
906
- if (!config) {
907
- return {valid: true, sanitized: value, errors: [], warnings: []};
908
- }
909
-
910
- const result = {
911
- valid: true,
912
- errors: [],
913
- warnings: [],
914
- sanitized: value
915
- };
916
-
917
- const constraints = config.constraints || {};
918
- const fieldType = config.type || 'string';
919
-
920
- // 1. Check required
921
- if (constraints.required && (value === undefined || value === null || value === '')) {
922
- // Skip: autoGenerate atau primaryKey di insert
923
- if (operation === 'insert' && (constraints.autoGenerate || constraints.primaryKey)) {
924
- // OK — akan di-generate otomatis
925
- }
926
- // Skip: update partial — field tidak dikirim berarti tidak diubah
927
- else if (operation === 'update' && value === undefined) {
928
- // OK — field tidak sedang di-update
929
- }
930
- else {
931
- const message = constraints.requiredMessage || `Field '${fieldName}' is required`;
932
- result.errors.push(message);
933
- result.valid = false;
934
- return result;
935
- }
936
- }
937
-
938
- // Skip validation jika value kosong dan tidak required
939
- if (value === undefined || value === null || value === '') {
940
- return result;
941
- }
942
-
943
- // 2. Type-specific validation
944
- switch (fieldType) {
945
- case 'string':
946
- return await this._validateStringConstraints(fieldName, value, constraints);
947
- case 'integer':
948
- case 'decimal':
949
- case 'number':
950
- return this._validateNumberConstraints(fieldName, value, constraints);
951
- case 'date':
952
- case 'datetime':
953
- case 'timestamp':
954
- case 'time':
955
- return this._validateDateConstraints(fieldName, value, constraints);
956
- case 'boolean':
957
- return this._validateBooleanConstraints(fieldName, value, constraints);
958
- case 'uuid':
959
- return this._validateUuidConstraints(fieldName, value, constraints);
960
- case 'array':
961
- return this._validateArrayConstraints(fieldName, value, constraints);
962
- case 'json':
963
- return this._validateJsonConstraints(fieldName, value, constraints);
964
- default:
965
- return result;
966
- }
967
- }
968
-
969
- /**
970
- * Validate string constraints
971
- */
972
- async _validateStringConstraints(fieldName, value, constraints) {
973
- let sanitized = String(value);
974
- const errors = [];
975
- const isHashField = constraints.hash === 'bcrypt';
976
-
977
- // Trim
978
- if (constraints.trim) {
979
- sanitized = sanitized.trim();
980
- }
981
-
982
- // Case transformation (skip jika hash field — tidak relevan untuk password)
983
- if (!isHashField) {
984
- if (constraints.lowercase) {
985
- sanitized = sanitized.toLowerCase();
986
- } else if (constraints.uppercase) {
987
- sanitized = sanitized.toUpperCase();
988
- }
989
- }
990
-
991
- // Length validation (validasi plaintext sebelum hash)
992
- if (constraints.minLength && sanitized.length < constraints.minLength) {
993
- const message = constraints.minLengthMessage || `Field '${fieldName}' must be at least ${constraints.minLength} characters`;
994
- errors.push(message);
995
- }
996
- if (constraints.maxLength && !isHashField && sanitized.length > constraints.maxLength) {
997
- const message = constraints.maxLengthMessage || `Field '${fieldName}' must not exceed ${constraints.maxLength} characters`;
998
- errors.push(message);
999
- }
1000
-
1001
- // Pattern validation (validasi plaintext sebelum hash)
1002
- if (constraints.pattern) {
1003
- const regex = new RegExp(constraints.pattern);
1004
- if (!regex.test(sanitized)) {
1005
- const message = constraints.patternMessage || `Field '${fieldName}' does not match required pattern`;
1006
- errors.push(message);
1007
- }
1008
- }
1009
-
1010
- // Format validation (email, phone, url, uuid)
1011
- if (constraints.format) {
1012
- const formatValid = this._validateFormat(sanitized, constraints.format);
1013
- if (!formatValid.valid) {
1014
- const message = constraints.formatMessage || `Field '${fieldName}' has invalid ${constraints.format} format`;
1015
- errors.push(message);
1016
- }
1017
- }
1018
-
1019
- // Enum validation
1020
- if (constraints.enum && Array.isArray(constraints.enum)) {
1021
- if (!constraints.enum.includes(sanitized)) {
1022
- const message = constraints.enumMessage || `Field '${fieldName}' must be one of: ${constraints.enum.join(', ')}`;
1023
- errors.push(message);
1024
- }
1025
- }
1026
-
1027
- // Skip unique constraint - database akan handle via UNIQUE index
1028
- // if (constraints.unique) { /* handled by database */ }
1029
-
1030
- // Hash transformation (setelah semua validation pass, sebelum return)
1031
- if (isHashField && errors.length === 0) {
1032
- const bcrypt = require('bcrypt');
1033
- const cost = constraints.hashCost || 10;
1034
- sanitized = await bcrypt.hash(sanitized, cost);
1035
- }
1036
-
1037
- return {
1038
- valid: errors.length === 0,
1039
- errors: errors,
1040
- warnings: [],
1041
- sanitized: sanitized
1042
- };
1043
- }
1044
-
1045
- /**
1046
- * Validate number constraints
1047
- */
1048
- _validateNumberConstraints(fieldName, value, constraints) {
1049
- let sanitized = value;
1050
- const errors = [];
1051
-
1052
- // Parse to number
1053
- if (typeof sanitized === 'string') {
1054
- sanitized = parseFloat(sanitized);
1055
- }
1056
-
1057
- if (isNaN(sanitized)) {
1058
- errors.push(`Field '${fieldName}' must be a valid number`);
1059
- return {valid: false, errors: errors, warnings: [], sanitized: value};
1060
- }
1061
-
1062
- // Min/Max
1063
- if (constraints.min !== undefined && sanitized < constraints.min) {
1064
- const message = constraints.minMessage || `Field '${fieldName}' must be at least ${constraints.min}`;
1065
- errors.push(message);
1066
- }
1067
- if (constraints.max !== undefined && sanitized > constraints.max) {
1068
- const message = constraints.maxMessage || `Field '${fieldName}' must not exceed ${constraints.max}`;
1069
- errors.push(message);
1070
- }
1071
-
1072
- // Positive/Negative
1073
- if (constraints.positive && sanitized <= 0) {
1074
- const message = constraints.positiveMessage || `Field '${fieldName}' must be positive`;
1075
- errors.push(message);
1076
- }
1077
- if (constraints.negative && sanitized >= 0) {
1078
- const message = constraints.negativeMessage || `Field '${fieldName}' must be negative`;
1079
- errors.push(message);
1080
- }
1081
-
1082
- // Integer check
1083
- if (constraints.integer && !Number.isInteger(sanitized)) {
1084
- const message = constraints.integerMessage || `Field '${fieldName}' must be an integer`;
1085
- errors.push(message);
1086
- }
1087
-
1088
- // Precision/Scale for decimal
1089
- if (constraints.precision !== undefined) {
1090
- const decimals = (sanitized.toString().split('.')[1] || '').length;
1091
- if (decimals > constraints.precision) {
1092
- const message = constraints.precisionMessage || `Field '${fieldName}' must have at most ${constraints.precision} decimal places`;
1093
- errors.push(message);
1094
- }
1095
- }
1096
-
1097
- return {
1098
- valid: errors.length === 0,
1099
- errors: errors,
1100
- warnings: [],
1101
- sanitized: sanitized
1102
- };
1103
- }
1104
-
1105
- /**
1106
- * Validate date constraints
1107
- */
1108
- _validateDateConstraints(fieldName, value, constraints) {
1109
- const errors = [];
1110
- let sanitized = value;
1111
-
1112
- // Basic date validation (more complex parsing handled by DateTimeParser in basemodel)
1113
- // Min/Max date range
1114
- if (constraints.min || constraints.max) {
1115
- try {
1116
- const dateValue = new Date(sanitized);
1117
- if (isNaN(dateValue.getTime())) {
1118
- errors.push(`Field '${fieldName}' must be a valid date`);
1119
- } else {
1120
- if (constraints.min) {
1121
- const minDate = new Date(constraints.min);
1122
- if (dateValue < minDate) {
1123
- const message = constraints.minMessage || `Field '${fieldName}' must be on or after ${constraints.min}`;
1124
- errors.push(message);
1125
- }
1126
- }
1127
- if (constraints.max) {
1128
- const maxDate = new Date(constraints.max);
1129
- if (dateValue > maxDate) {
1130
- const message = constraints.maxMessage || `Field '${fieldName}' must be on or before ${constraints.max}`;
1131
- errors.push(message);
1132
- }
1133
- }
1134
- }
1135
- } catch (e) {
1136
- errors.push(`Field '${fieldName}' must be a valid date`);
1137
- }
1138
- }
1139
-
1140
- // Before/After relasi akan divalidasi di _validateCrossFieldConstraints
1141
-
1142
- return {
1143
- valid: errors.length === 0,
1144
- errors: errors,
1145
- warnings: [],
1146
- sanitized: sanitized
1147
- };
1148
- }
1149
-
1150
- /**
1151
- * Validate boolean constraints
1152
- */
1153
- _validateBooleanConstraints(fieldName, value, constraints) {
1154
- const errors = [];
1155
- let sanitized = value;
1156
-
1157
- if (constraints.strict) {
1158
- if (typeof sanitized !== 'boolean') {
1159
- errors.push(`Field '${fieldName}' must be a boolean (true/false)`);
1160
- }
1161
- } else {
1162
- // Convert truthy/falsy
1163
- if (sanitized === 'true' || sanitized === '1' || sanitized === 1) {
1164
- sanitized = true;
1165
- } else if (sanitized === 'false' || sanitized === '0' || sanitized === 0) {
1166
- sanitized = false;
1167
- } else if (typeof sanitized !== 'boolean') {
1168
- sanitized = Boolean(sanitized);
1169
- }
1170
- }
1171
-
1172
- return {
1173
- valid: errors.length === 0,
1174
- errors: errors,
1175
- warnings: [],
1176
- sanitized: sanitized
1177
- };
1178
- }
1179
-
1180
- /**
1181
- * Validate UUID constraints
1182
- */
1183
- _validateUuidConstraints(fieldName, value, constraints) {
1184
- const errors = [];
1185
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1186
-
1187
- if (!uuidRegex.test(value)) {
1188
- const message = constraints.formatMessage || `Field '${fieldName}' must be a valid UUID`;
1189
- errors.push(message);
1190
- }
1191
-
1192
- return {
1193
- valid: errors.length === 0,
1194
- errors: errors,
1195
- warnings: [],
1196
- sanitized: value
1197
- };
1198
- }
1199
-
1200
- /**
1201
- * Validate array constraints
1202
- */
1203
- _validateArrayConstraints(fieldName, value, constraints) {
1204
- const errors = [];
1205
-
1206
- if (!Array.isArray(value)) {
1207
- errors.push(`Field '${fieldName}' must be an array`);
1208
- return {valid: false, errors: errors, warnings: [], sanitized: value};
1209
- }
1210
-
1211
- // minItems/maxItems
1212
- if (constraints.minItems && value.length < constraints.minItems) {
1213
- const message = constraints.minItemsMessage || `Field '${fieldName}' must have at least ${constraints.minItems} items`;
1214
- errors.push(message);
1215
- }
1216
- if (constraints.maxItems && value.length > constraints.maxItems) {
1217
- const message = constraints.maxItemsMessage || `Field '${fieldName}' must not exceed ${constraints.maxItems} items`;
1218
- errors.push(message);
1219
- }
1220
-
1221
- // uniqueItems
1222
- if (constraints.uniqueItems) {
1223
- const unique = [...new Set(value)];
1224
- if (unique.length !== value.length) {
1225
- const message = constraints.uniqueItemsMessage || `Field '${fieldName}' must have unique items`;
1226
- errors.push(message);
1227
- }
1228
- }
1229
-
1230
- return {
1231
- valid: errors.length === 0,
1232
- errors: errors,
1233
- warnings: [],
1234
- sanitized: value
1235
- };
1236
- }
1237
-
1238
- /**
1239
- * Validate JSON constraints
1240
- */
1241
- _validateJsonConstraints(fieldName, value, constraints) {
1242
- const errors = [];
1243
- let sanitized = value;
1244
-
1245
- // Parse jika string
1246
- if (typeof sanitized === 'string') {
1247
- try {
1248
- sanitized = JSON.parse(sanitized);
1249
- } catch (e) {
1250
- errors.push(`Field '${fieldName}' must be valid JSON`);
1251
- return {valid: false, errors: errors, warnings: [], sanitized: value};
1252
- }
1253
- }
1254
-
1255
- // JSON Schema validation bisa ditambahkan di sini jika diperlukan
1256
-
1257
- return {
1258
- valid: errors.length === 0,
1259
- errors: errors,
1260
- warnings: [],
1261
- sanitized: sanitized
1262
- };
1263
- }
1264
-
1265
- /**
1266
- * Validate format (email, phone, url, uuid)
1267
- */
1268
- _validateFormat(value, format) {
1269
- const patterns = {
1270
- email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
1271
- phone: /^[\d\s\-\+\(\)]+$/,
1272
- url: /^https?:\/\/.+/,
1273
- uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
1274
- };
1275
-
1276
- const pattern = patterns[format];
1277
- if (!pattern) {
1278
- return {valid: true};
1279
- }
1280
-
1281
- return {valid: pattern.test(value)};
1282
- }
1283
-
1284
- /**
1285
- * Validate cross-field constraints (before/after date)
1286
- */
1287
- async _validateCrossFieldConstraints(data, operation) {
1288
- const errors = [];
1289
-
1290
- for (const fieldName in this.validationConfig) {
1291
- const config = this.validationConfig[fieldName];
1292
- const constraints = config.constraints || {};
1293
-
1294
- if (constraints.before) {
1295
- const beforeField = constraints.before;
1296
- if (data[fieldName] && data[beforeField]) {
1297
- try {
1298
- if (new Date(data[fieldName]) >= new Date(data[beforeField])) {
1299
- const message = constraints.beforeMessage || `Field '${fieldName}' must be before '${beforeField}'`;
1300
- errors.push(message);
1301
- }
1302
- } catch (e) {
1303
- errors.push(`Invalid date format for field '${fieldName}': cannot compare dates`);
1304
- }
1305
- }
1306
- }
1307
-
1308
- if (constraints.after) {
1309
- const afterField = constraints.after;
1310
- if (data[fieldName] && data[afterField]) {
1311
- try {
1312
- if (new Date(data[fieldName]) <= new Date(data[afterField])) {
1313
- const message = constraints.afterMessage || `Field '${fieldName}' must be after '${afterField}'`;
1314
- errors.push(message);
1315
- }
1316
- } catch (e) {
1317
- errors.push(`Invalid date format for field '${fieldName}': cannot compare dates`);
1318
- }
1319
- }
1320
- }
1321
- }
1322
-
1323
- return {
1324
- valid: errors.length === 0,
1325
- errors: errors
1326
- };
1327
- }
1328
-
1329
-
1330
- }
1331
-
1332
- // Export singleton instance
1333
- module.exports = new ItemModel();