diff --git a/.gitignore b/.gitignore
index 7c03cc9..1d6139b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,17 @@ coverage/
.tmp/
.cache/
.validate-tmp/
+target/
+*.iml
+.vscode/
+.classpath
+.project
+.settings/
+out/
+hs_err_pid*
+replay_pid*
+postman/*.postman_environment.json
+!postman/*.postman_environment.template.json
# Local AI workflow files (not shared via git)
sdd/
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 26d3352..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index 4eda40e..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/easycode.ignore b/.idea/easycode.ignore
deleted file mode 100644
index 04b63e2..0000000
--- a/.idea/easycode.ignore
+++ /dev/null
@@ -1,13 +0,0 @@
-.idea
-.vscode
-node_modules/
-dist/
-vendor/
-cache/
-.*/
-*.min.*
-*.test.*
-*.spec.*
-*.bundle.*
-*.bundle-min.*
-*.log
diff --git a/.idea/easycode/codebase-v2.xml b/.idea/easycode/codebase-v2.xml
deleted file mode 100644
index 67e8afe..0000000
--- a/.idea/easycode/codebase-v2.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
deleted file mode 100644
index 63e9001..0000000
--- a/.idea/encodings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
deleted file mode 100644
index 712ab9d..0000000
--- a/.idea/jarRepositories.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 9dc782b..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/postman/dynapi.postman_collection.json b/postman/dynapi.postman_collection.json
new file mode 100644
index 0000000..57a7580
--- /dev/null
+++ b/postman/dynapi.postman_collection.json
@@ -0,0 +1,1029 @@
+{
+ "info": {
+ "_postman_id": "1f2d43c2-f0c0-4f74-8fef-0adf6073f100",
+ "name": "Dynapi API",
+ "description": "Dynapi backend API collection with success and error scenario examples for every active endpoint. Includes app-level error envelopes (400/403/404/500) and endpoint-specific success examples.",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "variable": [
+ {
+ "key": "baseUrl",
+ "value": "http://localhost:8080/api"
+ },
+ {
+ "key": "adminToken",
+ "value": ""
+ },
+ {
+ "key": "entity",
+ "value": "tasks"
+ },
+ {
+ "key": "fieldDefinitionId",
+ "value": "priority"
+ },
+ {
+ "key": "fieldGroupId",
+ "value": "task-form"
+ },
+ {
+ "key": "groupId",
+ "value": "task-form"
+ }
+ ],
+ "item": [
+ {
+ "name": "Form API",
+ "item": [
+ {
+ "name": "POST /form - Submit Form",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"group\": \"task-form\",\n \"data\": {\n \"title\": \"Ship v1\",\n \"priority\": 1\n }\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{baseUrl}}/form",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "form"
+ ]
+ },
+ "description": "Submit dynamic payload using a predefined field group."
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": true,\n \"message\": \"Form submitted successfully\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Validation Error",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Field is required\",\n \"data\": null,\n \"errors\": {\n \"title\": \"Field is required\"\n },\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Group Not Found",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Group not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ },
+ {
+ "name": "POST /forms/:groupId/submit - Submit Form",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ },
+ {
+ "key": "Accept-Language",
+ "value": "en-US",
+ "disabled": true
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"title\": \"Ship v1\",\n \"priority\": 1\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{baseUrl}}/forms/{{groupId}}/submit",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "forms",
+ "{{groupId}}",
+ "submit"
+ ]
+ },
+ "description": "Submit dynamic payload using path-based group id. Public endpoint."
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": true,\n \"message\": \"Form submitted successfully\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Validation Error",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Field is required\",\n \"data\": null,\n \"errors\": {\n \"title\": \"Field is required\"\n },\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Group Not Found",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Group not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Query API",
+ "item": [
+ {
+ "name": "POST /query/:entity - Query Records",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"filters\": [\n { \"field\": \"priority\", \"operator\": \"gte\", \"value\": 1 }\n ],\n \"page\": 0,\n \"size\": 10,\n \"sortBy\": \"priority\",\n \"sortDirection\": \"DESC\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{baseUrl}}/query/{{entity}}",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "query",
+ "{{entity}}"
+ ]
+ },
+ "description": "Query dynamic records with guarded filters, sorting, and pagination."
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": true,\n \"message\": \"Query successful\",\n \"data\": {\n \"page\": 0,\n \"size\": 10,\n \"totalElements\": 1,\n \"content\": [\n {\n \"id\": \"66f0f4c2d31234ab12345678\",\n \"data\": {\n \"title\": \"Ship v1\",\n \"priority\": 2\n }\n }\n ],\n \"sortBy\": \"priority\",\n \"sortDirection\": \"DESC\"\n },\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Size Exceeds Max",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Size exceeds max page size: 100\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Filter Field Not Allowed",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Filtering by field is not allowed: unknownField\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Operator Not Allowed",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Operator 'regex' is not allowed for field 'priority' of type NUMBER\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Filter Depth Exceeded",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Filter depth exceeds max: 3\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Schema Group Not Found",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Schema group not found for entity: tasks\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Schema Admin - Field Definitions",
+ "item": [
+ {
+ "name": "POST /admin/schema/field-definitions - Create Field Definition",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{adminToken}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"fieldName\": \"priority\",\n \"type\": \"NUMBER\",\n \"required\": false,\n \"min\": 1,\n \"max\": 5,\n \"version\": 1\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{baseUrl}}/admin/schema/field-definitions",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "admin",
+ "schema",
+ "field-definitions"
+ ]
+ }
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": true,\n \"message\": \"Created\",\n \"data\": {\n \"fieldName\": \"priority\",\n \"type\": \"NUMBER\",\n \"required\": false,\n \"min\": 1.0,\n \"max\": 5.0,\n \"regex\": null,\n \"enumValues\": null,\n \"requiredIf\": null,\n \"subFields\": null,\n \"version\": 1,\n \"permissions\": null\n },\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Illegal Argument",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Invalid request\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Forbidden - Missing or Non-Admin Token",
+ "status": "Forbidden",
+ "code": 403,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"timestamp\": \"2026-02-24T00:00:00.000+00:00\",\n \"status\": 403,\n \"error\": \"Forbidden\",\n \"path\": \"/api/admin/schema/field-definitions\"\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ },
+ {
+ "name": "PUT /admin/schema/field-definitions/:id - Update Field Definition",
+ "request": {
+ "method": "PUT",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{adminToken}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"type\": \"NUMBER\",\n \"required\": true,\n \"min\": 1,\n \"max\": 10\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{baseUrl}}/admin/schema/field-definitions/{{fieldDefinitionId}}",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "admin",
+ "schema",
+ "field-definitions",
+ "{{fieldDefinitionId}}"
+ ]
+ }
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": true,\n \"message\": \"Updated\",\n \"data\": {\n \"fieldName\": \"priority\",\n \"type\": \"NUMBER\",\n \"required\": true\n },\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Illegal Argument",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Invalid request\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Forbidden - Missing or Non-Admin Token",
+ "status": "Forbidden",
+ "code": 403,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"timestamp\": \"2026-02-24T00:00:00.000+00:00\",\n \"status\": 403,\n \"error\": \"Forbidden\",\n \"path\": \"/api/admin/schema/field-definitions/priority\"\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ },
+ {
+ "name": "DELETE /admin/schema/field-definitions/:id - Delete Field Definition",
+ "request": {
+ "method": "DELETE",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{adminToken}}"
+ }
+ ],
+ "url": {
+ "raw": "{{baseUrl}}/admin/schema/field-definitions/{{fieldDefinitionId}}",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "admin",
+ "schema",
+ "field-definitions",
+ "{{fieldDefinitionId}}"
+ ]
+ }
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": true,\n \"message\": \"Deleted\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Illegal Argument",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Invalid request\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Forbidden - Missing or Non-Admin Token",
+ "status": "Forbidden",
+ "code": 403,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"timestamp\": \"2026-02-24T00:00:00.000+00:00\",\n \"status\": 403,\n \"error\": \"Forbidden\",\n \"path\": \"/api/admin/schema/field-definitions/priority\"\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ },
+ {
+ "name": "GET /admin/schema/field-definitions - List Field Definitions",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{adminToken}}"
+ }
+ ],
+ "url": {
+ "raw": "{{baseUrl}}/admin/schema/field-definitions",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "admin",
+ "schema",
+ "field-definitions"
+ ]
+ }
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": true,\n \"message\": \"Fetched\",\n \"data\": [\n {\n \"fieldName\": \"priority\",\n \"type\": \"NUMBER\"\n }\n ],\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Illegal Argument",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Invalid request\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Forbidden - Missing or Non-Admin Token",
+ "status": "Forbidden",
+ "code": 403,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"timestamp\": \"2026-02-24T00:00:00.000+00:00\",\n \"status\": 403,\n \"error\": \"Forbidden\",\n \"path\": \"/api/admin/schema/field-definitions\"\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Schema Admin - Field Groups",
+ "item": [
+ {
+ "name": "POST /admin/schema/field-groups - Create Field Group",
+ "request": {
+ "method": "POST",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{adminToken}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"name\": \"task-form\",\n \"entity\": \"tasks\",\n \"fieldNames\": [\"title\", \"priority\"],\n \"version\": 1\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{baseUrl}}/admin/schema/field-groups",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "admin",
+ "schema",
+ "field-groups"
+ ]
+ }
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": true,\n \"message\": \"Created\",\n \"data\": {\n \"name\": \"task-form\",\n \"entity\": \"tasks\",\n \"fieldNames\": [\"title\", \"priority\"]\n },\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Illegal Argument",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Invalid request\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Forbidden - Missing or Non-Admin Token",
+ "status": "Forbidden",
+ "code": 403,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"timestamp\": \"2026-02-24T00:00:00.000+00:00\",\n \"status\": 403,\n \"error\": \"Forbidden\",\n \"path\": \"/api/admin/schema/field-groups\"\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ },
+ {
+ "name": "PUT /admin/schema/field-groups/:id - Update Field Group",
+ "request": {
+ "method": "PUT",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{adminToken}}"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"entity\": \"tasks\",\n \"fieldNames\": [\"title\", \"priority\", \"status\"],\n \"version\": 2\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{baseUrl}}/admin/schema/field-groups/{{fieldGroupId}}",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "admin",
+ "schema",
+ "field-groups",
+ "{{fieldGroupId}}"
+ ]
+ }
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": true,\n \"message\": \"Updated\",\n \"data\": {\n \"name\": \"task-form\",\n \"entity\": \"tasks\",\n \"fieldNames\": [\"title\", \"priority\", \"status\"]\n },\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Illegal Argument",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Invalid request\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Forbidden - Missing or Non-Admin Token",
+ "status": "Forbidden",
+ "code": 403,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"timestamp\": \"2026-02-24T00:00:00.000+00:00\",\n \"status\": 403,\n \"error\": \"Forbidden\",\n \"path\": \"/api/admin/schema/field-groups/task-form\"\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ },
+ {
+ "name": "DELETE /admin/schema/field-groups/:id - Delete Field Group",
+ "request": {
+ "method": "DELETE",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{adminToken}}"
+ }
+ ],
+ "url": {
+ "raw": "{{baseUrl}}/admin/schema/field-groups/{{fieldGroupId}}",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "admin",
+ "schema",
+ "field-groups",
+ "{{fieldGroupId}}"
+ ]
+ }
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": true,\n \"message\": \"Deleted\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Illegal Argument",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Invalid request\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Forbidden - Missing or Non-Admin Token",
+ "status": "Forbidden",
+ "code": 403,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"timestamp\": \"2026-02-24T00:00:00.000+00:00\",\n \"status\": 403,\n \"error\": \"Forbidden\",\n \"path\": \"/api/admin/schema/field-groups/task-form\"\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ },
+ {
+ "name": "GET /admin/schema/field-groups - List Field Groups",
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "value": "Bearer {{adminToken}}"
+ }
+ ],
+ "url": {
+ "raw": "{{baseUrl}}/admin/schema/field-groups",
+ "host": [
+ "{{baseUrl}}"
+ ],
+ "path": [
+ "admin",
+ "schema",
+ "field-groups"
+ ]
+ }
+ },
+ "response": [
+ {
+ "name": "Success",
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": true,\n \"message\": \"Fetched\",\n \"data\": [\n {\n \"name\": \"task-form\",\n \"entity\": \"tasks\",\n \"fieldNames\": [\"title\", \"priority\"]\n }\n ],\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Bad Request - Illegal Argument",
+ "status": "Bad Request",
+ "code": 400,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Invalid request\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Forbidden - Missing or Non-Admin Token",
+ "status": "Forbidden",
+ "code": 403,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"timestamp\": \"2026-02-24T00:00:00.000+00:00\",\n \"status\": 403,\n \"error\": \"Forbidden\",\n \"path\": \"/api/admin/schema/field-groups\"\n}"
+ },
+ {
+ "name": "Not Found - Entity (If Raised by Service)",
+ "status": "Not Found",
+ "code": 404,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "body": "{\n \"success\": false,\n \"message\": \"Entity not found\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ },
+ {
+ "name": "Internal Server Error",
+ "status": "Internal Server Error",
+ "code": 500,
+ "_postman_previewlanguage": "json",
+ "body": "{\n \"success\": false,\n \"message\": \"Internal server error\",\n \"data\": null,\n \"errors\": null,\n \"metadata\": null\n}"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/postman/dynapi.postman_environment.template.json b/postman/dynapi.postman_environment.template.json
new file mode 100644
index 0000000..91d78e1
--- /dev/null
+++ b/postman/dynapi.postman_environment.template.json
@@ -0,0 +1,49 @@
+{
+ "id": "00000000-0000-0000-0000-000000000000",
+ "name": "Dynapi Local (Template)",
+ "values": [
+ {
+ "key": "baseUrl",
+ "value": "http://localhost:8080/api",
+ "enabled": true
+ },
+ {
+ "key": "adminToken",
+ "value": "",
+ "enabled": true
+ },
+ {
+ "key": "entity",
+ "value": "tasks",
+ "enabled": true
+ },
+ {
+ "key": "fieldDefinitionId",
+ "value": "priority",
+ "enabled": true
+ },
+ {
+ "key": "fieldGroupId",
+ "value": "task-form",
+ "enabled": true
+ },
+ {
+ "key": "nonAdminToken",
+ "value": "",
+ "enabled": true
+ },
+ {
+ "key": "invalidToken",
+ "value": "invalid.jwt.token",
+ "enabled": true
+ },
+ {
+ "key": "groupId",
+ "value": "task-form",
+ "enabled": true
+ }
+ ],
+ "_postman_variable_scope": "environment",
+ "_postman_exported_at": "2026-02-23T00:00:00.000Z",
+ "_postman_exported_using": "Codex GPT-5"
+}
diff --git a/src/main/java/com/dynapi/config/QueryGuardrailProperties.java b/src/main/java/com/dynapi/config/QueryGuardrailProperties.java
new file mode 100644
index 0000000..ba72d1c
--- /dev/null
+++ b/src/main/java/com/dynapi/config/QueryGuardrailProperties.java
@@ -0,0 +1,16 @@
+package com.dynapi.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Getter
+@Setter
+@Component
+@ConfigurationProperties(prefix = "dynapi.query.guardrails")
+public class QueryGuardrailProperties {
+ private int maxPageSize = 100;
+ private int maxFilterDepth = 3;
+ private int maxRuleCount = 20;
+}
diff --git a/src/main/java/com/dynapi/interfaces/rest/FormSubmissionController.java b/src/main/java/com/dynapi/interfaces/rest/FormSubmissionController.java
index 2728376..7b609a2 100644
--- a/src/main/java/com/dynapi/interfaces/rest/FormSubmissionController.java
+++ b/src/main/java/com/dynapi/interfaces/rest/FormSubmissionController.java
@@ -1,25 +1,31 @@
package com.dynapi.interfaces.rest;
-import com.dynapi.application.port.input.SubmitFormUseCase;
import com.dynapi.dto.ApiResponse;
+import com.dynapi.dto.FormSubmissionRequest;
+import com.dynapi.service.FormSubmissionService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.Locale;
import java.util.Map;
+@RestController
@RequestMapping("/forms")
+@RequiredArgsConstructor
public class FormSubmissionController {
- private final SubmitFormUseCase submitFormUseCase;
-
- public FormSubmissionController(SubmitFormUseCase submitFormUseCase) {
- this.submitFormUseCase = submitFormUseCase;
- }
+ private final FormSubmissionService formSubmissionService;
@PostMapping("/{groupId}/submit")
public ApiResponse submitForm(
@PathVariable String groupId,
@RequestBody Map formData,
@RequestHeader(name = "Accept-Language", required = false) Locale locale) {
- submitFormUseCase.submitForm(groupId, formData, locale);
+ FormSubmissionRequest request = new FormSubmissionRequest();
+ request.setGroup(groupId);
+ request.setData(formData);
+
+ Locale effectiveLocale = locale == null ? LocaleContextHolder.getLocale() : locale;
+ formSubmissionService.submitForm(request, effectiveLocale);
return ApiResponse.success(null, "Form submitted successfully");
}
}
diff --git a/src/main/java/com/dynapi/repository/FieldGroupRepository.java b/src/main/java/com/dynapi/repository/FieldGroupRepository.java
index 9cb0891..534eacf 100644
--- a/src/main/java/com/dynapi/repository/FieldGroupRepository.java
+++ b/src/main/java/com/dynapi/repository/FieldGroupRepository.java
@@ -3,5 +3,8 @@
import com.dynapi.domain.model.FieldGroup;
import org.springframework.data.mongodb.repository.MongoRepository;
+import java.util.Optional;
+
public interface FieldGroupRepository extends MongoRepository {
+ Optional findByEntity(String entity);
}
diff --git a/src/main/java/com/dynapi/service/DynamicQueryService.java b/src/main/java/com/dynapi/service/DynamicQueryService.java
index ac513ce..738dbc1 100644
--- a/src/main/java/com/dynapi/service/DynamicQueryService.java
+++ b/src/main/java/com/dynapi/service/DynamicQueryService.java
@@ -1,8 +1,15 @@
package com.dynapi.service;
+import com.dynapi.config.QueryGuardrailProperties;
+import com.dynapi.domain.model.FieldDefinition;
+import com.dynapi.domain.model.FieldGroup;
+import com.dynapi.domain.model.FieldType;
import com.dynapi.dto.DynamicQueryRequest;
+import com.dynapi.dto.FilterRule;
import com.dynapi.dto.FormRecordDto;
import com.dynapi.dto.PaginatedResponse;
+import com.dynapi.repository.FieldDefinitionRepository;
+import com.dynapi.repository.FieldGroupRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
@@ -11,88 +18,360 @@
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
+import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
@Service
@RequiredArgsConstructor
public class DynamicQueryService {
+ private static final int DEFAULT_PAGE = 0;
+ private static final int DEFAULT_SIZE = 10;
+
+ private static final Set COMBINATOR_OPERATORS = Set.of("and", "or", "not");
+ private static final Set NUMBER_OPERATORS = Set.of("eq", "ne", "gt", "lt", "gte", "lte", "in");
+ private static final Set DATE_OPERATORS = Set.of("eq", "ne", "gt", "lt", "gte", "lte", "in");
+ private static final Set STRING_OPERATORS = Set.of("eq", "ne", "in", "regex");
+ private static final Set BOOLEAN_OPERATORS = Set.of("eq", "ne", "in");
+ private static final Set OBJECT_ARRAY_OPERATORS = Set.of("eq", "ne");
+
private final MongoTemplate mongoTemplate;
+ private final FieldGroupRepository fieldGroupRepository;
+ private final FieldDefinitionRepository fieldDefinitionRepository;
+ private final QueryGuardrailProperties guardrailProperties;
public PaginatedResponse query(String entity, DynamicQueryRequest request) {
+ DynamicQueryRequest safeRequest = request == null ? new DynamicQueryRequest() : request;
+ int page = resolvePage(safeRequest.getPage());
+ int size = resolveSize(safeRequest.getSize());
+ Map allowedFieldTypes = loadFieldTypesByEntity(entity);
+
+ validateSort(safeRequest.getSortBy(), safeRequest.getSortDirection(), allowedFieldTypes);
+ validateFilters(safeRequest.getFilters(), allowedFieldTypes);
+
Query query = new Query();
- // Build advanced filters
- if (request.getFilters() != null && !request.getFilters().isEmpty()) {
- query.addCriteria(buildCriteria(request.getFilters()));
+ if (safeRequest.getFilters() != null && !safeRequest.getFilters().isEmpty()) {
+ query.addCriteria(buildCriteria(safeRequest.getFilters()));
}
- // Pagination and sorting
- int page = request.getPage() != null ? request.getPage() : 0;
- int size = request.getSize() != null ? request.getSize() : 10;
- if (request.getSortBy() != null && request.getSortDirection() != null) {
- Sort.Direction dir = request.getSortDirection().equalsIgnoreCase("DESC") ? Sort.Direction.DESC : Sort.Direction.ASC;
- query.with(Sort.by(dir, request.getSortBy()));
+
+ if (safeRequest.getSortBy() != null && !safeRequest.getSortBy().isBlank()) {
+ Sort.Direction dir = resolveSortDirection(safeRequest.getSortDirection());
+ query.with(Sort.by(dir, safeRequest.getSortBy().trim()));
}
+
query.with(PageRequest.of(page, size));
- // Query MongoDB
+
List