@restforgejs/platform 4.1.1 → 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.
- package/SECURITY.md +83 -4
- package/bin/sdf-tools.exe +0 -0
- package/build-info.json +2 -2
- package/cli/consumer-deploy.js +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/dashboard/create.js +4 -1
- package/generators/cli/endpoint/create.js +43 -4
- package/generators/cli/key/generate.js +2 -1
- package/generators/cli/key/revoke.js +2 -1
- package/generators/cli/payload/diff.js +3 -2
- package/generators/cli/payload/generate.js +3 -2
- package/generators/cli/payload/sync.js +3 -2
- package/generators/cli/payload/validate.js +3 -2
- package/generators/cli/processor/create.js +14 -3
- package/generators/cli/project/delete.js +2 -1
- package/generators/cli/query/validate.js +3 -2
- package/generators/cli/schema/apply.js +526 -0
- package/generators/cli/schema/describe.js +3 -2
- package/generators/cli/schema/diff.js +322 -0
- package/generators/cli/schema/generate-ddl.js +7 -10
- package/generators/cli/schema/init.js +95 -172
- package/generators/cli/schema/introspect.js +3 -2
- package/generators/cli/schema/list.js +3 -2
- package/generators/cli/schema/migrate.js +13 -18
- package/generators/cli/schema/models.js +8 -12
- package/generators/cli/schema/template.js +222 -0
- package/generators/cli/schema/validate.js +8 -12
- package/generators/cli-entry.js +17 -2
- package/generators/lib/dbschema-kit/apply-engine.js +582 -0
- package/generators/lib/dbschema-kit/diff-engine.js +703 -0
- package/generators/lib/dbschema-kit/diff-reporter.js +272 -0
- package/generators/lib/dbschema-kit/emitters/alter-table.js +275 -0
- package/generators/lib/migration/audit-table-runner.js +213 -215
- package/generators/lib/payload/endpoint-schema-validator.js +171 -0
- package/generators/lib/payload/payload-runner.js +137 -220
- package/generators/lib/payload/schema-diff.js +277 -0
- package/generators/lib/templates/dashboard-catalog.js +1 -437
- package/generators/lib/templates/db-connection-env.js +1 -212
- package/generators/lib/templates/dbschema-catalog.js +1 -489
- package/generators/lib/templates/field-validation-catalog.js +1 -531
- package/generators/lib/templates/mysql-template.js +1 -3863
- package/generators/lib/templates/oracle-template.js +1 -3915
- package/generators/lib/templates/postgres-template.js +1 -5838
- package/generators/lib/templates/query-declarative-catalog.js +1 -199
- package/generators/lib/templates/sqlite-template.js +1 -3440
- package/generators/lib/utils/audit-columns.js +181 -0
- package/generators/lib/utils/cli-output.js +17 -0
- package/generators/lib/utils/database-introspector.js +16 -13
- package/generators/lib/utils/env-manager.js +6 -0
- package/generators/lib/utils/path-validator.js +71 -0
- package/generators/lib/validators/payload-validator.js +1 -2
- package/integrity-manifest.json +28 -10
- package/package.json +11 -3
- package/scripts/verify-integrity.js +1 -1
- package/server.js +1 -1
- package/src/components/handlers/adjust_handler.js +1 -1
- package/src/components/handlers/audit_handler.js +1 -1
- package/src/components/handlers/delete_handler.js +1 -1
- package/src/components/handlers/export_handler.js +1 -1
- package/src/components/handlers/import_handler.js +1 -1
- package/src/components/handlers/insert_handler.js +1 -1
- package/src/components/handlers/update_handler.js +1 -1
- package/src/components/handlers/upload_handler.js +1 -1
- package/src/components/handlers/workflow_handler.js +1 -1
- package/src/components/integrations/webhook.js +1 -1
- package/src/consumers/baseConsumer.js +1 -1
- package/src/consumers/declarativeMapper.js +1 -1
- package/src/consumers/handlers/apiHandler.js +1 -1
- package/src/consumers/handlers/consoleHandler.js +1 -1
- package/src/consumers/handlers/databaseHandler.js +1 -1
- package/src/consumers/handlers/index.js +1 -1
- package/src/consumers/handlers/kafkaHandler.js +1 -1
- package/src/consumers/index.js +1 -1
- package/src/consumers/messageTransformer.js +1 -1
- package/src/consumers/validator.js +1 -1
- package/src/core/db/dialect/base-dialect.js +1 -1
- package/src/core/db/dialect/index.js +1 -1
- package/src/core/db/dialect/mysql-dialect.js +1 -1
- package/src/core/db/dialect/oracle-dialect.js +1 -1
- package/src/core/db/dialect/postgres-dialect.js +1 -1
- package/src/core/db/dialect/sqlite-dialect.js +1 -1
- package/src/core/db/flatten-helper.js +1 -1
- package/src/core/db/query-builder-error.js +1 -1
- package/src/core/db/query-builder.js +1 -1
- package/src/core/db/relation-helper.js +1 -1
- package/src/core/handlers/delete_handler.js +1 -1
- package/src/core/handlers/insert_handler.js +1 -1
- package/src/core/handlers/update_handler.js +1 -1
- package/src/core/models/base-model.js +1 -1
- package/src/core/utils/cache-manager.js +1 -1
- package/src/core/utils/component-engine.js +1 -1
- package/src/core/utils/context-builder.js +1 -1
- package/src/core/utils/datetime-formatter.js +1 -1
- package/src/core/utils/datetime-parser.js +1 -1
- package/src/core/utils/db.js +1 -1
- package/src/core/utils/logger.js +1 -1
- package/src/core/utils/payload-loader.js +1 -1
- package/src/core/utils/security-checks.js +1 -1
- package/src/middleware/body-options.js +1 -1
- package/src/middleware/cors.js +1 -1
- package/src/middleware/idempotency.js +1 -1
- package/src/middleware/rate-limiter.js +1 -1
- package/src/middleware/request-logger.js +1 -1
- package/src/middleware/security-headers.js +1 -1
- package/src/models/base-model-mysql.js +1 -1
- package/src/models/base-model-oracle.js +1 -1
- package/src/models/base-model-sqlite.js +1 -1
- package/src/models/base-model.js +1 -1
- package/src/pro/caching/redis-client.js +1 -1
- package/src/pro/caching/redis-helper.js +1 -1
- package/src/pro/consumers/baseConsumer.js +1 -1
- package/src/pro/consumers/declarativeMapper.js +1 -1
- package/src/pro/consumers/handlers/apiHandler.js +1 -1
- package/src/pro/consumers/handlers/consoleHandler.js +1 -1
- package/src/pro/consumers/handlers/databaseHandler.js +1 -1
- package/src/pro/consumers/handlers/index.js +1 -1
- package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
- package/src/pro/consumers/index.js +1 -1
- package/src/pro/consumers/messageTransformer.js +1 -1
- package/src/pro/consumers/validator.js +1 -1
- package/src/pro/database/base-model-mysql.js +1 -1
- package/src/pro/database/base-model-oracle.js +1 -1
- package/src/pro/database/base-model-sqlite.js +1 -1
- package/src/pro/database/db-mysql.js +1 -1
- package/src/pro/database/db-oracle.js +1 -1
- package/src/pro/database/db-sqlite.js +1 -1
- package/src/pro/excel/excel-generator.js +1 -1
- package/src/pro/excel/excel-parser.js +1 -1
- package/src/pro/excel/export-service.js +1 -1
- package/src/pro/excel/export_handler.js +1 -1
- package/src/pro/excel/import-service.js +1 -1
- package/src/pro/excel/import-validator.js +1 -1
- package/src/pro/excel/import_handler.js +1 -1
- package/src/pro/excel/upsert-builder.js +1 -1
- package/src/pro/idgen/idgen-routes.js +1 -1
- package/src/pro/integrations/lookup-resolver.js +1 -1
- package/src/pro/integrations/upload-handler-v2.js +1 -1
- package/src/pro/integrations/upload-handler.js +1 -1
- package/src/pro/integrations/webhook.js +1 -1
- package/src/pro/locking/lock-routes.js +1 -1
- package/src/pro/locking/resource-lock-manager.js +1 -1
- package/src/pro/messaging/kafkaConsumerService.js +1 -1
- package/src/pro/messaging/kafkaService.js +1 -1
- package/src/pro/messaging/messagehubService.js +1 -1
- package/src/pro/messaging/rabbitmqService.js +1 -1
- package/src/pro/scheduler/job-manager.js +1 -1
- package/src/pro/scheduler/job-routes.js +1 -1
- package/src/pro/scheduler/job-validator.js +1 -1
- package/src/pro/storage/base-storage-provider.js +1 -1
- package/src/pro/storage/file-metadata-helper.js +1 -1
- package/src/pro/storage/index.js +1 -1
- package/src/pro/storage/local-storage-provider.js +1 -1
- package/src/pro/storage/s3-storage-provider.js +1 -1
- package/src/pro/storage/upload-cleanup-job.js +1 -1
- package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
- package/src/pro/storage/upload-pending-tracker.js +1 -1
- package/src/pro/websocket/broadcast-helper.js +1 -1
- package/src/pro/websocket/index.js +1 -1
- package/src/pro/websocket/livesync-server.js +1 -1
- package/src/pro/websocket/ws-broadcaster.js +1 -1
- package/src/services/export-service.js +1 -1
- package/src/services/import-service.js +1 -1
- package/src/services/kafkaConsumerService.js +1 -1
- package/src/services/kafkaService.js +1 -1
- package/src/services/messagehubService.js +1 -1
- package/src/services/rabbitmqService.js +1 -1
- package/src/utils/cache-invalidation-registry.js +1 -1
- package/src/utils/cache-manager.js +1 -1
- package/src/utils/component-engine.js +1 -1
- package/src/utils/config-extractor.js +1 -1
- package/src/utils/consumerLogger.js +1 -1
- package/src/utils/context-builder.js +1 -1
- package/src/utils/dashboard-helpers.js +1 -1
- package/src/utils/dateHelper.js +1 -1
- package/src/utils/datetime-formatter.js +1 -1
- package/src/utils/datetime-parser.js +1 -1
- package/src/utils/db-bootstrap.js +1 -1
- package/src/utils/db-mysql.js +1 -1
- package/src/utils/db-oracle.js +1 -1
- package/src/utils/db-sqlite.js +1 -1
- package/src/utils/db.js +1 -1
- package/src/utils/demo-generator.js +1 -1
- package/src/utils/excel-generator.js +1 -1
- package/src/utils/excel-parser.js +1 -1
- package/src/utils/file-watcher.js +1 -1
- package/src/utils/id-generator.js +1 -1
- package/src/utils/idempotency-manager.js +1 -1
- package/src/utils/import-validator.js +1 -1
- package/src/utils/license-client.js +1 -1
- package/src/utils/lock-manager.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/lookup-resolver.js +1 -1
- package/src/utils/payload-loader.js +1 -1
- package/src/utils/processor-response.js +1 -1
- package/src/utils/rabbitmq.js +1 -1
- package/src/utils/redis-client.js +1 -1
- package/src/utils/redis-helper.js +1 -1
- package/src/utils/request-scope.js +1 -1
- package/src/utils/security-checks.js +1 -1
- package/src/utils/service-resolver.js +1 -1
- package/src/utils/shutdown-coordinator.js +1 -1
- package/src/utils/trusted-keys.js +1 -1
- package/src/utils/upload-handler.js +1 -1
- package/src/utils/upsert-builder.js +1 -1
- package/src/utils/workflow-hook-executor.js +1 -1
- package/generators/metadata/global.json +0 -58
- package/generators/metadata/test-mysql-workbench.json +0 -118
- package/generators/metadata/test-mysql.json +0 -56
- package/generators/metadata/test-oracle-workbench.json +0 -118
- package/generators/metadata/test-oracle.json +0 -56
- package/generators/metadata/test-pg-workbench.json +0 -118
- package/generators/metadata/test-pg.json +0 -56
- package/generators/scripts/obfuscate-source.js +0 -356
- package/generators/scripts/validate-catalog.js +0 -430
- package/generators/scripts/validate-dbschema-catalog.js +0 -708
- package/generators/tests/baseline/mysql/mini_inventory_item/src/models/mini-inventory/item.js +0 -944
- package/generators/tests/baseline/mysql/mini_inventory_item/src/modules/mini-inventory/item.js +0 -740
- package/generators/tests/baseline/mysql/mini_inventory_item/src/modules/mini-inventory.js +0 -336
- package/generators/tests/baseline/oracle/mini_inventory_item/src/models/mini-inventory/item.js +0 -1002
- package/generators/tests/baseline/oracle/mini_inventory_item/src/modules/mini-inventory/item.js +0 -740
- package/generators/tests/baseline/oracle/mini_inventory_item/src/modules/mini-inventory.js +0 -336
- package/generators/tests/baseline/postgres/mini_inventory_item/src/models/mini-inventory/item.js +0 -1333
- package/generators/tests/baseline/postgres/mini_inventory_item/src/modules/mini-inventory/item.js +0 -1173
- package/generators/tests/baseline/postgres/mini_inventory_item/src/modules/mini-inventory.js +0 -496
- package/generators/tests/fixtures/payloads/custom-sensitive.json +0 -27
- package/generators/tests/fixtures/payloads/dynamic-search-optout.json +0 -23
- package/generators/tests/fixtures/payloads/login-with-password.json +0 -22
- package/generators/tests/fixtures/payloads/order-process.json +0 -52
- package/generators/tests/fixtures/payloads/with-inline-sql.json +0 -26
- package/generators/tests/integration-tahap4b/README.md +0 -145
- package/generators/tests/integration-tahap4b/run-concurrent.js +0 -77
- package/generators/tests/integration-tahap4b/seed.sql +0 -53
- package/generators/tests/integration-tahap4b/verify.sql +0 -110
- package/generators/tests/unit/cli/create-dashboard.test.js +0 -505
- package/generators/tests/unit/cli/create-processor.test.js +0 -319
- package/generators/tests/unit/cli/dispatch-dashboard.test.js +0 -149
- package/generators/tests/unit/lib/dashboard-generator.test.js +0 -895
- package/generators/tests/unit/lib/dashboard-validator.test.js +0 -354
- package/generators/tests/unit/lib/dbschema-kit/apply-executor.test.js +0 -437
- package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-introspect.test.js +0 -393
- package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-kit-generate-ddl.test.js +0 -104
- package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-kit-init.test.js +0 -119
- package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-kit-list.test.js +0 -48
- package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-kit-migrate.test.js +0 -175
- package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-kit-validate.test.js +0 -102
- package/generators/tests/unit/lib/dbschema-kit/cli/dbschema-models.test.js +0 -43
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/all-schemas-listing.js +0 -84
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/connection-error.js +0 -13
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/empty.js +0 -12
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/multi-schema.js +0 -124
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/single-schema-inventory.js +0 -64
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/introspect-stubs/two-tables.js +0 -66
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/migrate-stubs/connection-error.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/migrate-stubs/partial.js +0 -29
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/migrate-stubs/rollback.js +0 -26
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/migrate-stubs/success.js +0 -43
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/multi-schema/audit/events.js +0 -18
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/multi-schema/inventory/products.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/cli/fixtures/multi-schema/users.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/connection.test.js +0 -112
- package/generators/tests/unit/lib/dbschema-kit/ddl-generator.test.js +0 -205
- package/generators/tests/unit/lib/dbschema-kit/define-model.test.js +0 -56
- package/generators/tests/unit/lib/dbschema-kit/dialect/index.test.js +0 -46
- package/generators/tests/unit/lib/dbschema-kit/dialect/mysql.test.js +0 -126
- package/generators/tests/unit/lib/dbschema-kit/dialect/oracle.test.js +0 -126
- package/generators/tests/unit/lib/dbschema-kit/dialect/postgres.test.js +0 -131
- package/generators/tests/unit/lib/dbschema-kit/dialect/sqlite.test.js +0 -126
- package/generators/tests/unit/lib/dbschema-kit/driver-loader.test.js +0 -93
- package/generators/tests/unit/lib/dbschema-kit/emitters/create-index.test.js +0 -173
- package/generators/tests/unit/lib/dbschema-kit/emitters/create-table.test.js +0 -376
- package/generators/tests/unit/lib/dbschema-kit/emitters/drop-table.test.js +0 -78
- package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/invalid-dialect.env +0 -6
- package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/missing-dialect.env +0 -5
- package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/missing-host.env +0 -5
- package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/oracle-valid.env +0 -6
- package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/postgres-valid.env +0 -7
- package/generators/tests/unit/lib/dbschema-kit/fixtures/connection/sqlite-valid.env +0 -2
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/category.js +0 -11
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/item_product.js +0 -11
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/stock_inbound.js +0 -24
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/stock_inbound_item.js +0 -28
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/supplier.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory/warehouse.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-invalid/orphan.js +0 -17
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/master/category.js +0 -11
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/master/item_product.js +0 -11
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/master/supplier.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/master/warehouse.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/transactions/stock_inbound.js +0 -24
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/mini-inventory-multifolder/transactions/stock_inbound_item.js +0 -28
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/multi-schema/audit/events.js +0 -18
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/multi-schema/inventory/products.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/fixtures/integration/multi-schema/public/users.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/duplicate-subfolder/extra/category.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/duplicate-subfolder/master/category.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/duplicate-tablename/bar.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/duplicate-tablename/foo.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/empty-folder/README.md +0 -1
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/invalid-export/plain.js +0 -3
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/invalid-schema/bad.js +0 -6
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/legacy-pattern/legacy.js +0 -12
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/multi-schema-distinct/audit/products.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/multi-schema-distinct/inventory/products.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/multi-schema-duplicate/a/products.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/multi-schema-duplicate/b/products.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/nested-deep/a/b/c/deep_table.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/recursive-multi-folder/.hidden/ignored.js +0 -7
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/recursive-multi-folder/master/category.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/recursive-multi-folder/master/supplier.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/recursive-multi-folder/transactions/stock_inbound.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/recursive-multi-folder/transactions/stock_inbound_item.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/valid-multiple/category.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/valid-multiple/item_product.js +0 -9
- package/generators/tests/unit/lib/dbschema-kit/fixtures/loader/valid-single/category.js +0 -8
- package/generators/tests/unit/lib/dbschema-kit/integration.test.js +0 -217
- package/generators/tests/unit/lib/dbschema-kit/introspect-mapper.test.js +0 -403
- package/generators/tests/unit/lib/dbschema-kit/ir-builder.test.js +0 -390
- package/generators/tests/unit/lib/dbschema-kit/loader.test.js +0 -128
- package/generators/tests/unit/lib/dbschema-kit/naming.test.js +0 -170
- package/generators/tests/unit/lib/dbschema-kit/parser/shorthand-parser.test.js +0 -237
- package/generators/tests/unit/lib/dbschema-kit/schema-printer.test.js +0 -251
- package/generators/tests/unit/lib/dbschema-kit/statement-modifier.test.js +0 -105
- package/generators/tests/unit/lib/dbschema-kit/statement-splitter.test.js +0 -165
- package/generators/tests/unit/lib/dbschema-kit/topological-sort.test.js +0 -135
- package/generators/tests/unit/lib/dbschema-kit/validator/check-compatibility-validator.test.js +0 -373
- package/generators/tests/unit/lib/dbschema-kit/validator/circular-relation-validator.test.js +0 -454
- package/generators/tests/unit/lib/dbschema-kit/validator/cross-model-validator.test.js +0 -512
- package/generators/tests/unit/lib/dbschema-kit/validator/enhanced-validate-integration.test.js +0 -390
- package/generators/tests/unit/lib/dbschema-kit/validator/naming-convention-validator.test.js +0 -306
- package/generators/tests/unit/lib/dbschema-kit/validator/schema-validator.test.js +0 -443
- package/generators/tests/unit/lib/dbschema-kit/validator/type-compatibility-validator.test.js +0 -440
- package/generators/tests/unit/lib/dbschema-kit/validator/validator-reporter.test.js +0 -172
- package/generators/tests/unit/lib/metadata-manager-dashboard.test.js +0 -256
- package/generators/tests/unit/lib/payload-validator-fieldpolicy.test.js +0 -240
- package/generators/tests/unit/lib/processor-validation-generator.test.js +0 -300
- package/generators/tests/unit/lib/sensitive-field-masker.test.js +0 -170
- package/generators/tests/unit/lib/sql-table-extractor.test.js +0 -119
- package/scripts/generate-integrity-manifest.js +0 -124
- package/scripts/snapshot-cli-contracts.js +0 -194
- package/scripts/verify-publish.js +0 -56
package/generators/tests/baseline/postgres/mini_inventory_item/src/models/mini-inventory/item.js
DELETED
|
@@ -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();
|