diff --git a/.gitignore b/.gitignore index 2c458a3..1b017a0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.dylib *.test *.out +__debug_* # Go build output /bin/ diff --git a/docker-compose.yml b/docker-compose.yml index 46b5e52..0b23af2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,47 +20,47 @@ services: redis: image: 'redis:7-alpine' ports: - - '6379:6379' + - '6374:6379' redis-insight: image: redis/redisinsight:latest ports: - "5540:5540" environment: - RI_REDIS_HOST=redis - - RI_REDIS_PORT=6379 + - RI_REDIS_PORT=6374 - RI_REDIS_ALIAS=local-redis - gitlab: - image: "gitlab/gitlab-ce:latest" - container_name: gitlab - ports: - - "80:80" - - "443:443" - - "2222:22" - environment: - GITLAB_OMNIBUS_CONFIG: | - external_url 'http://gitlab' - gitlab_rails['initial_root_password'] = 'password' - gitlab_rails['allow_local_requests_from_web_hooks_and_services'] = true - gitlab_rails['allow_local_requests_from_system_hooks'] = true - volumes: - - "./gitlab/config/gitlab:/etc/gitlab" - - "./gitlab/logs/gitlab:/var/log/gitlab" - - "./gitlab/data/gitlab:/var/opt/gitlab" - extra_hosts: - - host.docker.internal:host-gateway - networks: - - ci - gitlab-runner: - image: gitlab/gitlab-runner:latest - depends_on: - - gitlab - volumes: - - "/var/run/docker.sock:/var/run/docker.sock" - - "./gitlab-runner/config:/etc/gitlab-runner" - extra_hosts: - - host.docker.internal:host-gateway - networks: - - ci + # gitlab: + # image: "gitlab/gitlab-ce:latest" + # container_name: gitlab + # ports: + # - "80:80" + # - "443:443" + # - "2222:22" + # environment: + # GITLAB_OMNIBUS_CONFIG: | + # external_url 'http://gitlab' + # gitlab_rails['initial_root_password'] = 'password' + # gitlab_rails['allow_local_requests_from_web_hooks_and_services'] = true + # gitlab_rails['allow_local_requests_from_system_hooks'] = true + # volumes: + # - "./gitlab/config/gitlab:/etc/gitlab" + # - "./gitlab/logs/gitlab:/var/log/gitlab" + # - "./gitlab/data/gitlab:/var/opt/gitlab" + # extra_hosts: + # - host.docker.internal:host-gateway + # networks: + # - ci + # gitlab-runner: + # image: gitlab/gitlab-runner:latest + # depends_on: + # - gitlab + # volumes: + # - "/var/run/docker.sock:/var/run/docker.sock" + # - "./gitlab-runner/config:/etc/gitlab-runner" + # extra_hosts: + # - host.docker.internal:host-gateway + # networks: + # - ci smtp4dev: image: rnwood/smtp4dev:latest container_name: skemr-smtp4dev diff --git a/go.work.sum b/go.work.sum index 34d7881..f077078 100644 --- a/go.work.sum +++ b/go.work.sum @@ -18,6 +18,14 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM= github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw= github.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE= @@ -93,6 +101,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= @@ -112,6 +122,8 @@ github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7X github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mfridman/xflag v0.1.0 h1:TWZrZwG1QklFX5S4j1vxfF1sZbZeZSGofMwPMLAF29M= github.com/mfridman/xflag v0.1.0/go.mod h1:/483ywM5ZO5SuMVjrIGquYNE5CzLrj5Ux/LxWWnjRaE= github.com/microsoft/go-mssqldb v1.9.6 h1:1MNQg5UiSsokiPz3++K2KPx4moKrwIqly1wv+RyCKTw= @@ -131,6 +143,8 @@ github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s= github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= @@ -215,7 +229,6 @@ golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGN golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/http/ai.http b/http/ai.http new file mode 100644 index 0000000..c67a494 --- /dev/null +++ b/http/ai.http @@ -0,0 +1,9 @@ +@host = http://localhost:8080 +@apiBaseUrl = {{host}}/api/v1 +@jwt = replace-with-jwt +@projectId = aaaabbbb-aaaa-aaaa-aaaa-aaaabbbbcccc +@databaseId = 11112222-3333-4444-5555-666677778888 + +### List database changes +POST {{apiBaseUrl}}/projects/{{projectId}}/ai/complete +Authorization: Bearer {{jwt}} diff --git a/http/database-changes.http b/http/database-changes.http new file mode 100644 index 0000000..5b19122 --- /dev/null +++ b/http/database-changes.http @@ -0,0 +1,9 @@ +@host = http://localhost:8080 +@apiBaseUrl = {{host}}/api/v1 +@jwt = replace-with-jwt +@projectId = aaaabbbb-aaaa-aaaa-aaaa-aaaabbbbcccc +@databaseId = 11112222-3333-4444-5555-666677778888 + +### List database changes +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/changes +Authorization: Bearer {{jwt}} diff --git a/http/database-entities.http b/http/database-entities.http new file mode 100644 index 0000000..8ceb501 --- /dev/null +++ b/http/database-entities.http @@ -0,0 +1,23 @@ +@host = http://localhost:8080 +@apiBaseUrl = {{host}}/api/v1 +@jwt = replace-with-jwt +@projectId = aaaabbbb-aaaa-4aaa-8aaa-aaaabbbbcccc +@databaseId = 11112222-3333-4444-5555-666677778888 +@databaseEntityId = 22223333-4444-4aaa-8bbb-777788889999 +@parentEntityId = 33334444-5555-4aaa-8bbb-888899990000 + +### List all entities discovered in a database +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/entities +Authorization: Bearer {{jwt}} + +### List table entities +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/entities?type=table +Authorization: Bearer {{jwt}} + +### List entities below a parent entity +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/entities?parentId={{parentEntityId}} +Authorization: Bearer {{jwt}} + +### Get an entity +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/entities/{{databaseEntityId}} +Authorization: Bearer {{jwt}} diff --git a/http/databases.http b/http/databases.http new file mode 100644 index 0000000..d8098a9 --- /dev/null +++ b/http/databases.http @@ -0,0 +1,49 @@ +@host = http://localhost:8080 +@apiBaseUrl = {{host}}/api/v1 +@jwt = replace-with-jwt +@projectId = aaaabbbb-aaaa-aaaa-aaaa-aaaabbbbcccc +@databaseId = 11112222-3333-4444-5555-666677778888 + +### List databases in a project +GET {{apiBaseUrl}}/projects/{{projectId}}/databases +Authorization: Bearer {{jwt}} + +### Create a PostgreSQL database connection +# @name createDatabase +POST {{apiBaseUrl}}/projects/{{projectId}}/databases +Authorization: Bearer {{jwt}} +Content-Type: application/json + +{ + "displayName": "Test Database", + "dbName": "test_db", + "username": "test_user", + "password": "test_pass", + "host": "localhost", + "port": 5433, + "sslMode": "disable", + "databaseType": "postgres" +} + +### Get a database +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}} +Authorization: Bearer {{jwt}} + +### Update a database connection +PATCH {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}} +Authorization: Bearer {{jwt}} +Content-Type: application/json + +{ + "displayName": "Test Database", + "dbName": "test_db", + "username": "test_user", + "password": "test_pass", + "host": "localhost", + "port": 5433, + "sslMode": "disable" +} + +### Queue a manual database schema sync +POST {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/sync +Authorization: Bearer {{jwt}} diff --git a/http/health.http b/http/health.http new file mode 100644 index 0000000..ee003e5 --- /dev/null +++ b/http/health.http @@ -0,0 +1,4 @@ +### Health check +@host = http://localhost:8080 + +GET {{host}}/health diff --git a/http/integrations.http b/http/integrations.http new file mode 100644 index 0000000..34bf2ab --- /dev/null +++ b/http/integrations.http @@ -0,0 +1,20 @@ +@host = http://localhost:8080 +@apiBaseUrl = {{host}}/api/v1 +@projectId = aaaabbbb-aaaa-aaaa-aaaa-aaaabbbbcccc +@databaseId = 11112222-3333-4444-5555-666677778888 +@projectAccessToken = replace-with-project-access-token + +### List CI/CD rules using a project access token +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/integrations/ci-cd/rules +Authorization: Bearer {{projectAccessToken}} + +### Report a completed CI/CD pipeline run +POST {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/integrations/ci-cd/pipeline-runs +Authorization: Bearer {{projectAccessToken}} +Content-Type: application/json + +{ + "environment": "production", + "status": "completed", + "completedAt": "2026-06-23T12:00:00Z" +} diff --git a/http/pipeline-runs.http b/http/pipeline-runs.http new file mode 100644 index 0000000..0ab0fca --- /dev/null +++ b/http/pipeline-runs.http @@ -0,0 +1,14 @@ +@host = http://localhost:8080 +@apiBaseUrl = {{host}}/api/v1 +@jwt = replace-with-jwt +@projectId = aaaabbbb-aaaa-aaaa-aaaa-aaaabbbbcccc +@databaseId = 11112222-3333-4444-5555-666677778888 +@pipelineRunId = 55556666-7777-4aaa-8bbb-000011112222 + +### List pipeline runs +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/pipeline-runs +Authorization: Bearer {{jwt}} + +### Get a pipeline run +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/pipeline-runs/{{pipelineRunId}} +Authorization: Bearer {{jwt}} diff --git a/http/projects.http b/http/projects.http new file mode 100644 index 0000000..f715131 --- /dev/null +++ b/http/projects.http @@ -0,0 +1,22 @@ +@host = http://localhost:8080 +@apiBaseUrl = {{host}}/api/v1 +@jwt = replace-with-jwt +@projectId = aaaabbbb-aaaa-aaaa-aaaa-aaaabbbbcccc + +### List projects +GET {{apiBaseUrl}}/projects +Authorization: Bearer {{jwt}} + +### Create a project +# @name createProject +POST {{apiBaseUrl}}/projects +Authorization: Bearer {{jwt}} +Content-Type: application/json + +{ + "name": "Example project" +} + +### Get a project +GET {{apiBaseUrl}}/projects/{{projectId}} +Authorization: Bearer {{jwt}} diff --git a/http/rules.http b/http/rules.http new file mode 100644 index 0000000..e95a117 --- /dev/null +++ b/http/rules.http @@ -0,0 +1,32 @@ +@host = http://localhost:8080 +@apiBaseUrl = {{host}}/api/v1 +@jwt = replace-with-jwt +@projectId = aaaabbbb-aaaa-aaaa-aaaa-aaaabbbbcccc +@databaseId = 11112222-3333-4444-5555-666677778888 +@databaseEntityId = 22223333-4444-4aaa-8bbb-777788889999 +@ruleId = 33334444-5555-4aaa-8bbb-888899990000 + +### Create an advisory rule for a database entity +# @name createRule +POST {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/rules +Authorization: Bearer {{jwt}} +Content-Type: application/json + +{ + "name": "Review users table changes", + "ruleType": "advisory", + "databaseEntityId": "{{databaseEntityId}}", + "attributes": {} +} + +### List rules for a database +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/rules +Authorization: Bearer {{jwt}} + +### Get a rule +GET {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/rules/{{ruleId}} +Authorization: Bearer {{jwt}} + +### Delete a rule +DELETE {{apiBaseUrl}}/projects/{{projectId}}/databases/{{databaseId}}/rules/{{ruleId}} +Authorization: Bearer {{jwt}} diff --git a/http/secrets.http b/http/secrets.http new file mode 100644 index 0000000..fb56574 --- /dev/null +++ b/http/secrets.http @@ -0,0 +1,30 @@ +@host = http://localhost:8080 +@apiBaseUrl = {{host}}/api/v1 +@jwt = replace-with-jwt +@projectId = aaaabbbb-aaaa-aaaa-aaaa-aaaabbbbcccc +@secretId = 44445555-6666-4aaa-8bbb-999900001111 + +### List project access tokens +GET {{apiBaseUrl}}/projects/{{projectId}}/secrets +Authorization: Bearer {{jwt}} + +### Create a project access token +# The token is returned once. Store it in your private HTTP environment as projectAccessToken. +# @name createSecret +POST {{apiBaseUrl}}/projects/{{projectId}}/secrets +Authorization: Bearer {{jwt}} +Content-Type: application/json + +{ + "name": "CI token", + "expiresAt": "2026-12-31T23:59:59Z" +} + +### Get a project access token +# This route is currently exposed by the API but returns "Not implemented". +GET {{apiBaseUrl}}/projects/{{projectId}}/secrets/{{secretId}} +Authorization: Bearer {{jwt}} + +### Delete a project access token +DELETE {{apiBaseUrl}}/projects/{{projectId}}/secrets/{{secretId}} +Authorization: Bearer {{jwt}} diff --git a/http/webhooks.http b/http/webhooks.http new file mode 100644 index 0000000..9e40091 --- /dev/null +++ b/http/webhooks.http @@ -0,0 +1,34 @@ +@host = http://localhost:8080 +@apiBaseUrl = {{host}}/api/v1 +@gitlabWebhookToken = replace-with-gitlab-webhook-token + +### Send a GitLab merge-request webhook +# GitLab sets the X-Gitlab-* headers and request body automatically in production. +POST {{apiBaseUrl}}/webhooks/gitlab +Content-Type: application/json +X-Gitlab-Token: {{gitlabWebhookToken}} +X-Gitlab-Event: Merge Request Hook + +{ + "object_kind": "merge_request", + "event_type": "merge_request", + "user": { + "name": "Example User", + "username": "example" + }, + "project": { + "id": 1, + "name": "example-project", + "path_with_namespace": "group/example-project", + "web_url": "https://gitlab.example.com/group/example-project" + }, + "object_attributes": { + "id": 1, + "iid": 1, + "title": "Example merge request", + "state": "opened", + "source_branch": "feature/example", + "target_branch": "main" + } +} + diff --git a/skemr-api/cmd/server/__debug_bin92027419 b/skemr-api/cmd/server/__debug_bin92027419 new file mode 100755 index 0000000..b0bec80 Binary files /dev/null and b/skemr-api/cmd/server/__debug_bin92027419 differ diff --git a/skemr-api/cmd/server/main.go b/skemr-api/cmd/server/main.go index f1a9662..dc20a04 100644 --- a/skemr-api/cmd/server/main.go +++ b/skemr-api/cmd/server/main.go @@ -16,6 +16,7 @@ import ( "github.com/pressly/goose/v3" "github.com/walmaa/skemr-api/config" "github.com/walmaa/skemr-api/db/sqlc" + "github.com/walmaa/skemr-api/internal/ai" "github.com/walmaa/skemr-api/internal/routers" "github.com/walmaa/skemr-api/internal/service" "github.com/walmaa/skemr-api/internal/tasks" @@ -98,6 +99,13 @@ func main() { } queries := sqlc.New(conn) + + toolService := ai.NewToolService(queries) + toolRegistry := ai.NewToolRegistry( + ai.NewDatabaseEntityTool(toolService), + ai.NewDatabaseTool(toolService), + ) + aiClient := ai.NewOpenAIClient(toolRegistry) scopeResolver := service.NewScopeResolver(queries) projectService := service.NewProjectService(queries) databaseChangeService := service.NewDatabaseChangeService(queries, scopeResolver) @@ -120,6 +128,7 @@ func main() { IntegrationService: integrationService, DatabaseChangeService: databaseChangeService, PipelineRunService: pipelineRunService, + AIClient: aiClient, } worker.StartTaskWorkers(queries, cfg) diff --git a/skemr-api/config/config.go b/skemr-api/config/config.go index 7c4e9c0..cb802a0 100644 --- a/skemr-api/config/config.go +++ b/skemr-api/config/config.go @@ -33,6 +33,9 @@ type Config struct { Password string DB int } + OpenAI struct { + APIKey string + } } func LoadConfig() (*Config, error) { @@ -64,6 +67,8 @@ func LoadConfig() (*Config, error) { viper.SetDefault("redis.port", defaultRedisPort) viper.SetDefault("redis.password", "") viper.SetDefault("redis.db", 0) + // OpenAI defaults + viper.SetDefault("openai.api_key", "") // Enable reading from environment variables viper.AutomaticEnv() @@ -108,6 +113,10 @@ func LoadConfig() (*Config, error) { return nil, err } + if err := viper.BindEnv("openai.api_key", "OPENAI_API_KEY"); err != nil { + return nil, err + } + var cfg Config if err := viper.Unmarshal(&cfg); err != nil { diff --git a/skemr-api/db/queries/databases.sql b/skemr-api/db/queries/databases.sql index aaf14eb..fb8468b 100644 --- a/skemr-api/db/queries/databases.sql +++ b/skemr-api/db/queries/databases.sql @@ -65,7 +65,7 @@ DELETE FROM databases WHERE id = $1; --- name: ListDatabasesByProject :many +-- name: GetDatabasesByProjectId :many SELECT * FROM databases WHERE project_id = $1; diff --git a/skemr-api/db/sqlc/database_changes.sql.go b/skemr-api/db/sqlc/database_changes.sql.go new file mode 100644 index 0000000..2783b76 --- /dev/null +++ b/skemr-api/db/sqlc/database_changes.sql.go @@ -0,0 +1,105 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: database_changes.sql + +package sqlc + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const createDatabaseChange = `-- name: CreateDatabaseChange :one +INSERT INTO database_changes + (database_id, entity_id, action) +VALUES ($1, $2, $3) +RETURNING id, database_id, entity_id, action, created_at +` + +type CreateDatabaseChangeParams struct { + DatabaseID uuid.UUID `json:"database_id"` + EntityID uuid.UUID `json:"entity_id"` + Action MigrationStatementAction `json:"action"` +} + +func (q *Queries) CreateDatabaseChange(ctx context.Context, arg CreateDatabaseChangeParams) (DatabaseChange, error) { + row := q.db.QueryRow(ctx, createDatabaseChange, arg.DatabaseID, arg.EntityID, arg.Action) + var i DatabaseChange + err := row.Scan( + &i.ID, + &i.DatabaseID, + &i.EntityID, + &i.Action, + &i.CreatedAt, + ) + return i, err +} + +const getDatabaseChangeByDatabaseIdAndId = `-- name: GetDatabaseChangeByDatabaseIdAndId :one +SELECT id, database_id, entity_id, action, created_at +FROM database_changes c +WHERE c.id = $1 + AND c.database_id = $2 +LIMIT 1 +` + +type GetDatabaseChangeByDatabaseIdAndIdParams struct { + ID uuid.UUID `json:"id"` + DatabaseID uuid.UUID `json:"database_id"` +} + +func (q *Queries) GetDatabaseChangeByDatabaseIdAndId(ctx context.Context, arg GetDatabaseChangeByDatabaseIdAndIdParams) (DatabaseChange, error) { + row := q.db.QueryRow(ctx, getDatabaseChangeByDatabaseIdAndId, arg.ID, arg.DatabaseID) + var i DatabaseChange + err := row.Scan( + &i.ID, + &i.DatabaseID, + &i.EntityID, + &i.Action, + &i.CreatedAt, + ) + return i, err +} + +const getDatabaseChangesByDatabaseIdAndId = `-- name: GetDatabaseChangesByDatabaseIdAndId :many +SELECT id, database_id, entity_id, action, created_at +FROM database_changes c +WHERE c.database_id = $1 +ORDER BY c.created_at DESC +LIMIT $3::int OFFSET $2::int +` + +type GetDatabaseChangesByDatabaseIdAndIdParams struct { + DatabaseID uuid.UUID `json:"database_id"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +func (q *Queries) GetDatabaseChangesByDatabaseIdAndId(ctx context.Context, arg GetDatabaseChangesByDatabaseIdAndIdParams) ([]DatabaseChange, error) { + rows, err := q.db.Query(ctx, getDatabaseChangesByDatabaseIdAndId, arg.DatabaseID, arg.Offset, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []DatabaseChange{} + for rows.Next() { + var i DatabaseChange + if err := rows.Scan( + &i.ID, + &i.DatabaseID, + &i.EntityID, + &i.Action, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/skemr-api/db/sqlc/database_entities.sql.go b/skemr-api/db/sqlc/database_entities.sql.go index a19228f..2bf920e 100644 --- a/skemr-api/db/sqlc/database_entities.sql.go +++ b/skemr-api/db/sqlc/database_entities.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: database_entities.sql package sqlc diff --git a/skemr-api/db/sqlc/databases.sql.go b/skemr-api/db/sqlc/databases.sql.go index 69cd1e8..3864497 100644 --- a/skemr-api/db/sqlc/databases.sql.go +++ b/skemr-api/db/sqlc/databases.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: databases.sql package sqlc @@ -211,14 +211,14 @@ func (q *Queries) GetDatabaseByNameAndProject(ctx context.Context, arg GetDataba return i, err } -const listDatabasesByProject = `-- name: ListDatabasesByProject :many +const getDatabasesByProjectId = `-- name: GetDatabasesByProjectId :many SELECT id, display_name, db_name, username, password, host, port, ssl_mode, database_type, project_id, last_synced_at, last_sync_error, failed_connection_attempts, created_at, updated_at FROM databases WHERE project_id = $1 ` -func (q *Queries) ListDatabasesByProject(ctx context.Context, projectID uuid.UUID) ([]Database, error) { - rows, err := q.db.Query(ctx, listDatabasesByProject, projectID) +func (q *Queries) GetDatabasesByProjectId(ctx context.Context, projectID uuid.UUID) ([]Database, error) { + rows, err := q.db.Query(ctx, getDatabasesByProjectId, projectID) if err != nil { return nil, err } diff --git a/skemr-api/db/sqlc/db.go b/skemr-api/db/sqlc/db.go index 2725108..a28f6fc 100644 --- a/skemr-api/db/sqlc/db.go +++ b/skemr-api/db/sqlc/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 package sqlc diff --git a/skemr-api/db/sqlc/models.go b/skemr-api/db/sqlc/models.go index a218857..1af31fd 100644 --- a/skemr-api/db/sqlc/models.go +++ b/skemr-api/db/sqlc/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 package sqlc diff --git a/skemr-api/db/sqlc/pipeline_runs.sql.go b/skemr-api/db/sqlc/pipeline_runs.sql.go new file mode 100644 index 0000000..c7bca77 --- /dev/null +++ b/skemr-api/db/sqlc/pipeline_runs.sql.go @@ -0,0 +1,110 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: pipeline_runs.sql + +package sqlc + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const createPipelineRun = `-- name: CreatePipelineRun :one +INSERT INTO pipeline_runs + (database_id, status, environment, completed_at) +VALUES ($1, $2, $3, $4) +RETURNING id, database_id, status, environment, started_at, completed_at, created_at +` + +type CreatePipelineRunParams struct { + DatabaseID uuid.UUID `json:"database_id"` + Status MigrationStatus `json:"status"` + Environment pgtype.Text `json:"environment"` + CompletedAt pgtype.Timestamptz `json:"completed_at"` +} + +func (q *Queries) CreatePipelineRun(ctx context.Context, arg CreatePipelineRunParams) (PipelineRun, error) { + row := q.db.QueryRow(ctx, createPipelineRun, + arg.DatabaseID, + arg.Status, + arg.Environment, + arg.CompletedAt, + ) + var i PipelineRun + err := row.Scan( + &i.ID, + &i.DatabaseID, + &i.Status, + &i.Environment, + &i.StartedAt, + &i.CompletedAt, + &i.CreatedAt, + ) + return i, err +} + +const getPipelineRunByDatabaseIdAndId = `-- name: GetPipelineRunByDatabaseIdAndId :one +SELECT id, database_id, status, environment, started_at, completed_at, created_at +FROM pipeline_runs +WHERE database_id = $1 + AND id = $2 +LIMIT 1 +` + +type GetPipelineRunByDatabaseIdAndIdParams struct { + DatabaseID uuid.UUID `json:"database_id"` + ID uuid.UUID `json:"id"` +} + +func (q *Queries) GetPipelineRunByDatabaseIdAndId(ctx context.Context, arg GetPipelineRunByDatabaseIdAndIdParams) (PipelineRun, error) { + row := q.db.QueryRow(ctx, getPipelineRunByDatabaseIdAndId, arg.DatabaseID, arg.ID) + var i PipelineRun + err := row.Scan( + &i.ID, + &i.DatabaseID, + &i.Status, + &i.Environment, + &i.StartedAt, + &i.CompletedAt, + &i.CreatedAt, + ) + return i, err +} + +const getPipelineRunsByDatabaseId = `-- name: GetPipelineRunsByDatabaseId :many +SELECT id, database_id, status, environment, started_at, completed_at, created_at +FROM pipeline_runs +WHERE database_id = $1 +ORDER BY created_at DESC +` + +func (q *Queries) GetPipelineRunsByDatabaseId(ctx context.Context, databaseID uuid.UUID) ([]PipelineRun, error) { + rows, err := q.db.Query(ctx, getPipelineRunsByDatabaseId, databaseID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []PipelineRun{} + for rows.Next() { + var i PipelineRun + if err := rows.Scan( + &i.ID, + &i.DatabaseID, + &i.Status, + &i.Environment, + &i.StartedAt, + &i.CompletedAt, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/skemr-api/db/sqlc/project_access_tokens.sql.go b/skemr-api/db/sqlc/project_access_tokens.sql.go index 3800152..fdd12d1 100644 --- a/skemr-api/db/sqlc/project_access_tokens.sql.go +++ b/skemr-api/db/sqlc/project_access_tokens.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: project_access_tokens.sql package sqlc diff --git a/skemr-api/db/sqlc/projects.sql.go b/skemr-api/db/sqlc/projects.sql.go index 2d527f2..cbe7ee5 100644 --- a/skemr-api/db/sqlc/projects.sql.go +++ b/skemr-api/db/sqlc/projects.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: projects.sql package sqlc diff --git a/skemr-api/db/sqlc/querier.go b/skemr-api/db/sqlc/querier.go index 7cb8c9d..282bbd2 100644 --- a/skemr-api/db/sqlc/querier.go +++ b/skemr-api/db/sqlc/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 package sqlc @@ -37,6 +37,7 @@ type Querier interface { GetDatabaseEntityByFingerprint(ctx context.Context, arg GetDatabaseEntityByFingerprintParams) (DatabaseEntity, error) GetDatabaseEntityByProjectIdAndId(ctx context.Context, arg GetDatabaseEntityByProjectIdAndIdParams) (DatabaseEntity, error) GetDatabaseEntityByProjectIdDatabaseIdAndId(ctx context.Context, arg GetDatabaseEntityByProjectIdDatabaseIdAndIdParams) (DatabaseEntity, error) + GetDatabasesByProjectId(ctx context.Context, projectID uuid.UUID) ([]Database, error) GetHashByPrefixAndProjectID(ctx context.Context, arg GetHashByPrefixAndProjectIDParams) (string, error) GetPipelineRunByDatabaseIdAndId(ctx context.Context, arg GetPipelineRunByDatabaseIdAndIdParams) (PipelineRun, error) GetPipelineRunsByDatabaseId(ctx context.Context, databaseID uuid.UUID) ([]PipelineRun, error) @@ -49,7 +50,6 @@ type Querier interface { GetRuleByDatabaseAndName(ctx context.Context, arg GetRuleByDatabaseAndNameParams) (Rule, error) GetRuleWithEntity(ctx context.Context, arg GetRuleWithEntityParams) (GetRuleWithEntityRow, error) GetRulesWithEntities(ctx context.Context, arg GetRulesWithEntitiesParams) ([]GetRulesWithEntitiesRow, error) - ListDatabasesByProject(ctx context.Context, projectID uuid.UUID) ([]Database, error) ListRulesByCriteria(ctx context.Context, arg ListRulesByCriteriaParams) ([]Rule, error) ListRulesByDatabaseId(ctx context.Context, databaseID uuid.UUID) ([]Rule, error) UpdateDatabase(ctx context.Context, arg UpdateDatabaseParams) (Database, error) diff --git a/skemr-api/db/sqlc/rules.sql.go b/skemr-api/db/sqlc/rules.sql.go index fe6b61c..f794018 100644 --- a/skemr-api/db/sqlc/rules.sql.go +++ b/skemr-api/db/sqlc/rules.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.31.1 // source: rules.sql package sqlc diff --git a/skemr-api/go.mod b/skemr-api/go.mod index 53ccbaa..d8752bc 100644 --- a/skemr-api/go.mod +++ b/skemr-api/go.mod @@ -69,6 +69,7 @@ require ( github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/openai/openai-go/v3 v3.41.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -87,6 +88,10 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/testcontainers/testcontainers-go v0.39.0 // indirect + github.com/tidwall/gjson v1.19.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect diff --git a/skemr-api/go.sum b/skemr-api/go.sum index 019dfd6..3af1add 100644 --- a/skemr-api/go.sum +++ b/skemr-api/go.sum @@ -147,6 +147,8 @@ github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/openai/openai-go/v3 v3.41.0 h1:9GkxcN02U5NG0WGdQjZ0cTSu/pMXEyzL2LfF0ruZCck= +github.com/openai/openai-go/v3 v3.41.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -196,6 +198,17 @@ github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0 h1:REJz+XwNpGC/dCgTfYvM4SKqobNqDBfvhq74s2oHTUM= github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0/go.mod h1:4K2OhtHEeT+JSIFX4V8DkGKsyLa96Y2vLdd3xsxD5HE= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= diff --git a/skemr-api/internal/ai/client.go b/skemr-api/internal/ai/client.go new file mode 100644 index 0000000..69cf23a --- /dev/null +++ b/skemr-api/internal/ai/client.go @@ -0,0 +1,130 @@ +package ai + +import ( + "context" + "encoding/json" + "log/slog" + + "github.com/google/uuid" + "github.com/openai/openai-go/v3" + "github.com/openai/openai-go/v3/responses" +) + +type ToolCall struct { + ID string `json:"id"` + CallID string `json:"callId"` + Name string `json:"name"` + Arguments json.RawMessage `json:"arguments"` +} + +type Completion struct { + Text string `json:"text"` + ToolCalls []ToolCall `json:"toolCalls,omitempty"` +} + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type Model interface { + Complete(ctx context.Context, msgs []Message, actor Actor) (Completion, error) +} + +type OpenAIClient struct { + client *openai.Client + toolRegistry *ToolRegistry +} + +type Actor struct { + UserID uuid.UUID + ProjectID uuid.UUID +} + +func NewOpenAIClient(toolRegistry *ToolRegistry) *OpenAIClient { + client := openai.NewClient() + return &OpenAIClient{client: &client, toolRegistry: toolRegistry} +} + +func (c *OpenAIClient) Complete(ctx context.Context, msgs []Message, actor Actor) (Completion, error) { + slog.Info("Completing with OpenAI", "actor", actor, "messages", msgs) + + params := responses.ResponseNewParams{ + Input: responses.ResponseNewParamsInputUnion{ + OfString: openai.String("What databases are currently active?"), + }, + Model: openai.ChatModelGPT5Nano, + Tools: c.toolRegistry.toToolUnionParams(), + } + + response, err := c.prompt(ctx, params) + + if err != nil { + slog.Error("Error generating response", "err", err) + return Completion{}, err + } + + var outputs []responses.ResponseInputItemUnionParam + // Tool call handling + for _, item := range response.Output { + if item.Type != "function_call" { + continue + } + toolCall := item.AsFunctionCall() + toolCallResult, err := c.toolRegistry.Run(ctx, toolCall.Name, nil, actor) + + if err != nil { + slog.Error("Error running tool", "toolName", toolCall.Name, "err", err) + } + slog.Debug("toolCallResult", "result", toolCallResult) + + responseInput := responses.ResponseInputItemUnionParam{ + OfFunctionCallOutput: &responses.ResponseInputItemFunctionCallOutputParam{ + CallID: toolCall.CallID, + Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{ + OfString: openai.String(toolCallResult), + }, + }, + } + + outputs = append(outputs, responseInput) + + } + toolCallResponseParams := responses.ResponseNewParams{ + Model: openai.ChatModelGPT5Nano, + PreviousResponseID: openai.String(response.ID), + Input: responses.ResponseNewParamsInputUnion{ + OfInputItemList: outputs, + }, + } + + // If there are no tool calls, return the original response + if len(outputs) == 0 { + return Completion{ + Text: response.OutputText(), + }, nil + } + + // Continue conversation with tool call result + toolCallResponse, err := c.prompt(ctx, toolCallResponseParams) + + if err != nil { + slog.Error("Error generating response after tool call", "err", err) + return Completion{}, err + } + + return Completion{ + Text: toolCallResponse.OutputText(), + }, nil +} + +// prompt sends a prompt to the OpenAI API and returns the response. +// Used as a wrapper for logging +func (c *OpenAIClient) prompt(ctx context.Context, params responses.ResponseNewParams) (*responses.Response, error) { + response, err := c.client.Responses.New(ctx, params) + + if err == nil { + slog.Debug("response", "inputTokens", response.Usage.InputTokens, "outputTokens", response.Usage.OutputTokens, "totalTokens", response.Usage.TotalTokens) + } + return response, err +} diff --git a/skemr-api/internal/ai/controller.go b/skemr-api/internal/ai/controller.go new file mode 100644 index 0000000..37e8867 --- /dev/null +++ b/skemr-api/internal/ai/controller.go @@ -0,0 +1,49 @@ +package ai + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" + "github.com/google/uuid" + "github.com/walmaa/skemr-api/internal/controller" + "github.com/walmaa/skemr-api/internal/errormsg" +) + +type AIController struct { + client Model +} + +func NewAIController(client Model) *AIController { + return &AIController{client: client} +} + +func (h *AIController) RegisterRoutes(r chi.Router) { + r.Post("/ai/complete", h.complete) +} + +func (h *AIController) complete(w http.ResponseWriter, r *http.Request) { + projectId, ok := controller.ParseUUIDParam(w, r, "projectId") + if !ok { + return + } + + msgs := []Message{ + {Role: "user", Content: "What databases are currently active?"}, + } + + // TODO: In a real application, you would extract the user ID from the request context or session. + actor := Actor{ + UserID: uuid.Max, + ProjectID: projectId, + } + + result, err := h.client.Complete(r.Context(), msgs, actor) + + if err != nil { + errormsg.WriteErrorResponse(w, r, err) + return + } + render.JSON(w, r, result) + +} diff --git a/skemr-api/internal/ai/databasetool.go b/skemr-api/internal/ai/databasetool.go new file mode 100644 index 0000000..8fa5f55 --- /dev/null +++ b/skemr-api/internal/ai/databasetool.go @@ -0,0 +1,38 @@ +package ai + +import ( + "context" + "encoding/json" + + "github.com/walmaa/skemr-common/models" +) + +type DatabaseToolService interface { + GetDatabases(ctx context.Context, actor Actor) ([]models.Database, error) +} + +type DatabaseTool struct { + service DatabaseToolService +} + +func NewDatabaseTool(service DatabaseToolService) *DatabaseTool { + return &DatabaseTool{service: service} +} + +func (t *DatabaseTool) Spec() ToolSpec { + return ToolSpec{ + Name: "get_databases", + Description: "Get the databases for a given project", + Parameters: nil, + Strict: true, + } +} + +func (t *DatabaseTool) Run(ctx context.Context, input json.RawMessage, actor Actor) (string, error) { + databases, err := t.service.GetDatabases(ctx, actor) + if err != nil { + return toolError("search_failed", err.Error()), nil + } + + return toolJSON(databases) +} \ No newline at end of file diff --git a/skemr-api/internal/ai/entitytool.go b/skemr-api/internal/ai/entitytool.go new file mode 100644 index 0000000..d8c8c15 --- /dev/null +++ b/skemr-api/internal/ai/entitytool.go @@ -0,0 +1,61 @@ +package ai + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/uuid" + "github.com/walmaa/skemr-common/models" +) + +type DatabaseEntityToolService interface { + GetDatabaseEntities(ctx context.Context, databaseId uuid.UUID, actor Actor) ([]models.DatabaseEntity, error) +} + +type DatabaseEntityTool struct { + service DatabaseEntityToolService +} + +func NewDatabaseEntityTool(service DatabaseEntityToolService) *DatabaseEntityTool { + + return &DatabaseEntityTool{service: service} + +} + +func (t *DatabaseEntityTool) Spec() ToolSpec { + + return ToolSpec{ + + Name: "get_database_entities", + + Description: "Get the database entities for a given project and database", + + Parameters: json.RawMessage(`{"projectId": "string", "databaseId": "string"}`), + + Strict: true, + } + +} + +func (t *DatabaseEntityTool) Run(ctx context.Context, input json.RawMessage, actor Actor) (string, error) { + + var params struct { + DatabaseId uuid.UUID `json:"databaseId"` + } + + if err := json.Unmarshal(input, ¶ms); err != nil { + + return "", fmt.Errorf("invalid input parameters: %w", err) + + } + + entities, err := t.service.GetDatabaseEntities(ctx, params.DatabaseId, actor) + + if err != nil { + return toolError("search_failed", err.Error()), nil + } + + return toolJSON(entities) + +} diff --git a/skemr-api/internal/ai/helpers.go b/skemr-api/internal/ai/helpers.go new file mode 100644 index 0000000..01a98d2 --- /dev/null +++ b/skemr-api/internal/ai/helpers.go @@ -0,0 +1,24 @@ +package ai + +import "encoding/json" + +func toolJSON(v any) (string, error) { + b, err := json.Marshal(v) + if err != nil { + return "", err + } + + return string(b), nil +} + +func toolError(code string, message string) string { + b, _ := json.Marshal(map[string]any{ + "ok": false, + "error": map[string]string{ + "code": code, + "message": message, + }, + }) + + return string(b) +} diff --git a/skemr-api/internal/ai/toolregistry.go b/skemr-api/internal/ai/toolregistry.go new file mode 100644 index 0000000..20568e2 --- /dev/null +++ b/skemr-api/internal/ai/toolregistry.go @@ -0,0 +1,60 @@ +package ai + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/openai/openai-go/v3" + "github.com/openai/openai-go/v3/responses" +) + +type Tool interface { + Spec() ToolSpec + Run(ctx context.Context, input json.RawMessage, actor Actor) (string, error) +} + +type ToolSpec struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters json.RawMessage `json:"parameters"` + Strict bool `json:"strict"` +} + +type ToolRegistry struct { + tools map[string]Tool +} + +func NewToolRegistry(tools ...Tool) *ToolRegistry { + toolMap := make(map[string]Tool) + + for _, tool := range tools { + toolMap[tool.Spec().Name] = tool + } + return &ToolRegistry{ + tools: toolMap, + } +} + +func (r *ToolRegistry) toToolUnionParams() []responses.ToolUnionParam { + var toolUnionParams []responses.ToolUnionParam + for _, tool := range r.tools { + spec := tool.Spec() + toolUnionParams = append(toolUnionParams, responses.ToolUnionParam{ + OfFunction: &responses.FunctionToolParam{ + Name: spec.Name, + Description: openai.String(spec.Description), + }, + }) + } + return toolUnionParams +} + +// Run executes the specified tool with the given input and actor context. +func (r *ToolRegistry) Run(ctx context.Context, toolName string, input json.RawMessage, actor Actor) (string, error) { + tool, ok := r.tools[toolName] + if !ok { + return "", fmt.Errorf("unknown tool: %s", toolName) + } + return tool.Run(ctx, input, actor) +} diff --git a/skemr-api/internal/ai/toolservice.go b/skemr-api/internal/ai/toolservice.go new file mode 100644 index 0000000..2978783 --- /dev/null +++ b/skemr-api/internal/ai/toolservice.go @@ -0,0 +1,47 @@ +package ai + +import ( + "context" + "log/slog" + + "github.com/google/uuid" + "github.com/walmaa/skemr-api/db/sqlc" + "github.com/walmaa/skemr-api/internal/mapper" + "github.com/walmaa/skemr-common/models" +) + +type ToolService struct { + db sqlc.Querier +} + +func NewToolService(db sqlc.Querier) *ToolService { + return &ToolService{db: db} +} + +func (s *ToolService) GetDatabaseEntities(ctx context.Context, databaseId uuid.UUID, actor Actor) ([]models.DatabaseEntity, error) { + slog.Info("ToolService: GetDatabaseEntities", "actor", actor) + + entities, err := s.db.GetDatabaseEntities(ctx, sqlc.GetDatabaseEntitiesParams{ + DatabaseID: databaseId, + }) + + if err != nil { + slog.Error("Error getting database entities", "error", err) + return nil, err + } + + return mapper.ToDomainDatabaseEntities(entities), nil + +} + +func (s *ToolService) GetDatabases(ctx context.Context, actor Actor) ([]models.Database, error) { + slog.Info("ToolService: GetDatabases", "actor", actor) + + databases, err := s.db.GetDatabasesByProjectId(ctx, actor.ProjectID) + if err != nil { + slog.Error("Error getting databases", "error", err) + return nil, err + } + + return mapper.ToDomainDatabases(databases), nil +} diff --git a/skemr-api/internal/controller/pipeline_run_controller.go b/skemr-api/internal/controller/pipeline_run_controller.go index 6830dfd..0997a7e 100644 --- a/skemr-api/internal/controller/pipeline_run_controller.go +++ b/skemr-api/internal/controller/pipeline_run_controller.go @@ -66,6 +66,7 @@ func (h *PipelineRunController) getPipelineRun(w http.ResponseWriter, r *http.Re pipelineRun, err := h.Service.GetPipelineRun(r.Context(), projectId, databaseId, pipelineRunId) if err != nil { errormsg.WriteErrorResponse(w, r, err) + return } render.JSON(w, r, pipelineRun) diff --git a/skemr-api/internal/routers/router.go b/skemr-api/internal/routers/router.go index 7e0c3dd..2fab045 100644 --- a/skemr-api/internal/routers/router.go +++ b/skemr-api/internal/routers/router.go @@ -8,6 +8,7 @@ import ( chimiddleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" "github.com/go-chi/render" + "github.com/walmaa/skemr-api/internal/ai" "github.com/walmaa/skemr-api/internal/controller" "github.com/walmaa/skemr-api/internal/middleware" "github.com/walmaa/skemr-api/internal/service" @@ -23,6 +24,7 @@ type Services struct { IntegrationService *service.IntegrationService DatabaseChangeService *service.DatabaseChangeService PipelineRunService *service.PipelineRunService + AIClient ai.Model } func InitRouter(services *Services) http.Handler { @@ -82,6 +84,7 @@ func InitRouter(services *Services) http.Handler { databaseEntityController := controller.NewDatabaseEntityController(services.DatabaseEntityService) databaseChangeController := controller.NewDatabaseChangeController(services.DatabaseChangeService) pipelineRunController := controller.NewPipelineRunController(services.PipelineRunService) + aiController := ai.NewAIController(services.AIClient) // register routes databaseController.RegisterRoutes(r) @@ -90,6 +93,7 @@ func InitRouter(services *Services) http.Handler { databaseEntityController.RegisterRoutes(r) databaseChangeController.RegisterRoutes(r) pipelineRunController.RegisterRoutes(r) + aiController.RegisterRoutes(r) r.Get("/", projectController.GetProject) r.Delete("/", projectController.DeleteProject) diff --git a/skemr-api/internal/service/database_service.go b/skemr-api/internal/service/database_service.go index 04c51ae..a7caa33 100644 --- a/skemr-api/internal/service/database_service.go +++ b/skemr-api/internal/service/database_service.go @@ -143,7 +143,7 @@ func (r *DatabaseService) ListDatabasesByProject(c context.Context, projectId uu slog.Error("Could not get project") return nil, err } - databases, err := r.db.ListDatabasesByProject(c, project.ID) + databases, err := r.db.GetDatabasesByProjectId(c, project.ID) if err != nil { slog.Error("Unable to get databases", "project_id", projectId, "err", err) diff --git a/skemr-api/test/mocks/rule_store_mock.go b/skemr-api/test/mocks/rule_store_mock.go new file mode 100644 index 0000000..415a684 --- /dev/null +++ b/skemr-api/test/mocks/rule_store_mock.go @@ -0,0 +1,362 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + mock "github.com/stretchr/testify/mock" + "github.com/walmaa/skemr-api/db/sqlc" +) + +// NewMockRuleStore creates a new instance of MockRuleStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRuleStore(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRuleStore { + mock := &MockRuleStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockRuleStore is an autogenerated mock type for the RuleStore type +type MockRuleStore struct { + mock.Mock +} + +type MockRuleStore_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRuleStore) EXPECT() *MockRuleStore_Expecter { + return &MockRuleStore_Expecter{mock: &_m.Mock} +} + +// CreateRule provides a mock function for the type MockRuleStore +func (_mock *MockRuleStore) CreateRule(ctx context.Context, dto sqlc.CreateRuleParams) (sqlc.Rule, error) { + ret := _mock.Called(ctx, dto) + + if len(ret) == 0 { + panic("no return value specified for CreateRule") + } + + var r0 sqlc.Rule + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, sqlc.CreateRuleParams) (sqlc.Rule, error)); ok { + return returnFunc(ctx, dto) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, sqlc.CreateRuleParams) sqlc.Rule); ok { + r0 = returnFunc(ctx, dto) + } else { + r0 = ret.Get(0).(sqlc.Rule) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, sqlc.CreateRuleParams) error); ok { + r1 = returnFunc(ctx, dto) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockRuleStore_CreateRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRule' +type MockRuleStore_CreateRule_Call struct { + *mock.Call +} + +// CreateRule is a helper method to define mock.On call +// - ctx context.Context +// - dto sqlc.CreateRuleParams +func (_e *MockRuleStore_Expecter) CreateRule(ctx interface{}, dto interface{}) *MockRuleStore_CreateRule_Call { + return &MockRuleStore_CreateRule_Call{Call: _e.mock.On("CreateRule", ctx, dto)} +} + +func (_c *MockRuleStore_CreateRule_Call) Run(run func(ctx context.Context, dto sqlc.CreateRuleParams)) *MockRuleStore_CreateRule_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 sqlc.CreateRuleParams + if args[1] != nil { + arg1 = args[1].(sqlc.CreateRuleParams) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRuleStore_CreateRule_Call) Return(rule sqlc.Rule, err error) *MockRuleStore_CreateRule_Call { + _c.Call.Return(rule, err) + return _c +} + +func (_c *MockRuleStore_CreateRule_Call) RunAndReturn(run func(ctx context.Context, dto sqlc.CreateRuleParams) (sqlc.Rule, error)) *MockRuleStore_CreateRule_Call { + _c.Call.Return(run) + return _c +} + +// DeleteRule provides a mock function for the type MockRuleStore +func (_mock *MockRuleStore) DeleteRule(ctx context.Context, params sqlc.DeleteRuleParams) error { + ret := _mock.Called(ctx, params) + + if len(ret) == 0 { + panic("no return value specified for DeleteRule") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, sqlc.DeleteRuleParams) error); ok { + r0 = returnFunc(ctx, params) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRuleStore_DeleteRule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteRule' +type MockRuleStore_DeleteRule_Call struct { + *mock.Call +} + +// DeleteRule is a helper method to define mock.On call +// - ctx context.Context +// - params sqlc.DeleteRuleParams +func (_e *MockRuleStore_Expecter) DeleteRule(ctx interface{}, params interface{}) *MockRuleStore_DeleteRule_Call { + return &MockRuleStore_DeleteRule_Call{Call: _e.mock.On("DeleteRule", ctx, params)} +} + +func (_c *MockRuleStore_DeleteRule_Call) Run(run func(ctx context.Context, params sqlc.DeleteRuleParams)) *MockRuleStore_DeleteRule_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 sqlc.DeleteRuleParams + if args[1] != nil { + arg1 = args[1].(sqlc.DeleteRuleParams) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRuleStore_DeleteRule_Call) Return(err error) *MockRuleStore_DeleteRule_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRuleStore_DeleteRule_Call) RunAndReturn(run func(ctx context.Context, params sqlc.DeleteRuleParams) error) *MockRuleStore_DeleteRule_Call { + _c.Call.Return(run) + return _c +} + +// GetRuleByDatabaseAndName provides a mock function for the type MockRuleStore +func (_mock *MockRuleStore) GetRuleByDatabaseAndName(ctx context.Context, params sqlc.GetRuleByDatabaseAndNameParams) (sqlc.Rule, error) { + ret := _mock.Called(ctx, params) + + if len(ret) == 0 { + panic("no return value specified for GetRuleByDatabaseAndName") + } + + var r0 sqlc.Rule + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, sqlc.GetRuleByDatabaseAndNameParams) (sqlc.Rule, error)); ok { + return returnFunc(ctx, params) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, sqlc.GetRuleByDatabaseAndNameParams) sqlc.Rule); ok { + r0 = returnFunc(ctx, params) + } else { + r0 = ret.Get(0).(sqlc.Rule) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, sqlc.GetRuleByDatabaseAndNameParams) error); ok { + r1 = returnFunc(ctx, params) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockRuleStore_GetRuleByDatabaseAndName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRuleByDatabaseAndName' +type MockRuleStore_GetRuleByDatabaseAndName_Call struct { + *mock.Call +} + +// GetRuleByDatabaseAndName is a helper method to define mock.On call +// - ctx context.Context +// - params sqlc.GetRuleByDatabaseAndNameParams +func (_e *MockRuleStore_Expecter) GetRuleByDatabaseAndName(ctx interface{}, params interface{}) *MockRuleStore_GetRuleByDatabaseAndName_Call { + return &MockRuleStore_GetRuleByDatabaseAndName_Call{Call: _e.mock.On("GetRuleByDatabaseAndName", ctx, params)} +} + +func (_c *MockRuleStore_GetRuleByDatabaseAndName_Call) Run(run func(ctx context.Context, params sqlc.GetRuleByDatabaseAndNameParams)) *MockRuleStore_GetRuleByDatabaseAndName_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 sqlc.GetRuleByDatabaseAndNameParams + if args[1] != nil { + arg1 = args[1].(sqlc.GetRuleByDatabaseAndNameParams) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRuleStore_GetRuleByDatabaseAndName_Call) Return(rule sqlc.Rule, err error) *MockRuleStore_GetRuleByDatabaseAndName_Call { + _c.Call.Return(rule, err) + return _c +} + +func (_c *MockRuleStore_GetRuleByDatabaseAndName_Call) RunAndReturn(run func(ctx context.Context, params sqlc.GetRuleByDatabaseAndNameParams) (sqlc.Rule, error)) *MockRuleStore_GetRuleByDatabaseAndName_Call { + _c.Call.Return(run) + return _c +} + +// GetRuleWithEntity provides a mock function for the type MockRuleStore +func (_mock *MockRuleStore) GetRuleWithEntity(ctx context.Context, params sqlc.GetRuleWithEntityParams) (sqlc.GetRuleWithEntityRow, error) { + ret := _mock.Called(ctx, params) + + if len(ret) == 0 { + panic("no return value specified for GetRuleWithEntity") + } + + var r0 sqlc.GetRuleWithEntityRow + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, sqlc.GetRuleWithEntityParams) (sqlc.GetRuleWithEntityRow, error)); ok { + return returnFunc(ctx, params) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, sqlc.GetRuleWithEntityParams) sqlc.GetRuleWithEntityRow); ok { + r0 = returnFunc(ctx, params) + } else { + r0 = ret.Get(0).(sqlc.GetRuleWithEntityRow) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, sqlc.GetRuleWithEntityParams) error); ok { + r1 = returnFunc(ctx, params) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockRuleStore_GetRuleWithEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRuleWithEntity' +type MockRuleStore_GetRuleWithEntity_Call struct { + *mock.Call +} + +// GetRuleWithEntity is a helper method to define mock.On call +// - ctx context.Context +// - params sqlc.GetRuleWithEntityParams +func (_e *MockRuleStore_Expecter) GetRuleWithEntity(ctx interface{}, params interface{}) *MockRuleStore_GetRuleWithEntity_Call { + return &MockRuleStore_GetRuleWithEntity_Call{Call: _e.mock.On("GetRuleWithEntity", ctx, params)} +} + +func (_c *MockRuleStore_GetRuleWithEntity_Call) Run(run func(ctx context.Context, params sqlc.GetRuleWithEntityParams)) *MockRuleStore_GetRuleWithEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 sqlc.GetRuleWithEntityParams + if args[1] != nil { + arg1 = args[1].(sqlc.GetRuleWithEntityParams) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRuleStore_GetRuleWithEntity_Call) Return(getRuleWithEntityRow sqlc.GetRuleWithEntityRow, err error) *MockRuleStore_GetRuleWithEntity_Call { + _c.Call.Return(getRuleWithEntityRow, err) + return _c +} + +func (_c *MockRuleStore_GetRuleWithEntity_Call) RunAndReturn(run func(ctx context.Context, params sqlc.GetRuleWithEntityParams) (sqlc.GetRuleWithEntityRow, error)) *MockRuleStore_GetRuleWithEntity_Call { + _c.Call.Return(run) + return _c +} + +// GetRulesWithEntities provides a mock function for the type MockRuleStore +func (_mock *MockRuleStore) GetRulesWithEntities(ctx context.Context, row sqlc.GetRulesWithEntitiesParams) ([]sqlc.GetRulesWithEntitiesRow, error) { + ret := _mock.Called(ctx, row) + + if len(ret) == 0 { + panic("no return value specified for GetRulesWithEntities") + } + + var r0 []sqlc.GetRulesWithEntitiesRow + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, sqlc.GetRulesWithEntitiesParams) ([]sqlc.GetRulesWithEntitiesRow, error)); ok { + return returnFunc(ctx, row) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, sqlc.GetRulesWithEntitiesParams) []sqlc.GetRulesWithEntitiesRow); ok { + r0 = returnFunc(ctx, row) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]sqlc.GetRulesWithEntitiesRow) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, sqlc.GetRulesWithEntitiesParams) error); ok { + r1 = returnFunc(ctx, row) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockRuleStore_GetRulesWithEntities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRulesWithEntities' +type MockRuleStore_GetRulesWithEntities_Call struct { + *mock.Call +} + +// GetRulesWithEntities is a helper method to define mock.On call +// - ctx context.Context +// - row sqlc.GetRulesWithEntitiesParams +func (_e *MockRuleStore_Expecter) GetRulesWithEntities(ctx interface{}, row interface{}) *MockRuleStore_GetRulesWithEntities_Call { + return &MockRuleStore_GetRulesWithEntities_Call{Call: _e.mock.On("GetRulesWithEntities", ctx, row)} +} + +func (_c *MockRuleStore_GetRulesWithEntities_Call) Run(run func(ctx context.Context, row sqlc.GetRulesWithEntitiesParams)) *MockRuleStore_GetRulesWithEntities_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 sqlc.GetRulesWithEntitiesParams + if args[1] != nil { + arg1 = args[1].(sqlc.GetRulesWithEntitiesParams) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRuleStore_GetRulesWithEntities_Call) Return(getRulesWithEntitiesRows []sqlc.GetRulesWithEntitiesRow, err error) *MockRuleStore_GetRulesWithEntities_Call { + _c.Call.Return(getRulesWithEntitiesRows, err) + return _c +} + +func (_c *MockRuleStore_GetRulesWithEntities_Call) RunAndReturn(run func(ctx context.Context, row sqlc.GetRulesWithEntitiesParams) ([]sqlc.GetRulesWithEntitiesRow, error)) *MockRuleStore_GetRulesWithEntities_Call { + _c.Call.Return(run) + return _c +} diff --git a/skemr-api/test/mocks/scope_resolver_mock.go b/skemr-api/test/mocks/scope_resolver_mock.go new file mode 100644 index 0000000..3e1a0fc --- /dev/null +++ b/skemr-api/test/mocks/scope_resolver_mock.go @@ -0,0 +1,190 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/google/uuid" + mock "github.com/stretchr/testify/mock" + "github.com/walmaa/skemr-common/models" +) + +// NewMockScopeResolver creates a new instance of MockScopeResolver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockScopeResolver(t interface { + mock.TestingT + Cleanup(func()) +}) *MockScopeResolver { + mock := &MockScopeResolver{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockScopeResolver is an autogenerated mock type for the ScopeResolver type +type MockScopeResolver struct { + mock.Mock +} + +type MockScopeResolver_Expecter struct { + mock *mock.Mock +} + +func (_m *MockScopeResolver) EXPECT() *MockScopeResolver_Expecter { + return &MockScopeResolver_Expecter{mock: &_m.Mock} +} + +// RequireDatabase provides a mock function for the type MockScopeResolver +func (_mock *MockScopeResolver) RequireDatabase(c context.Context, projectId uuid.UUID, databaseId uuid.UUID) (models.Database, error) { + ret := _mock.Called(c, projectId, databaseId) + + if len(ret) == 0 { + panic("no return value specified for RequireDatabase") + } + + var r0 models.Database + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) (models.Database, error)); ok { + return returnFunc(c, projectId, databaseId) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) models.Database); ok { + r0 = returnFunc(c, projectId, databaseId) + } else { + r0 = ret.Get(0).(models.Database) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID, uuid.UUID) error); ok { + r1 = returnFunc(c, projectId, databaseId) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockScopeResolver_RequireDatabase_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RequireDatabase' +type MockScopeResolver_RequireDatabase_Call struct { + *mock.Call +} + +// RequireDatabase is a helper method to define mock.On call +// - c context.Context +// - projectId uuid.UUID +// - databaseId uuid.UUID +func (_e *MockScopeResolver_Expecter) RequireDatabase(c interface{}, projectId interface{}, databaseId interface{}) *MockScopeResolver_RequireDatabase_Call { + return &MockScopeResolver_RequireDatabase_Call{Call: _e.mock.On("RequireDatabase", c, projectId, databaseId)} +} + +func (_c *MockScopeResolver_RequireDatabase_Call) Run(run func(c context.Context, projectId uuid.UUID, databaseId uuid.UUID)) *MockScopeResolver_RequireDatabase_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 uuid.UUID + if args[1] != nil { + arg1 = args[1].(uuid.UUID) + } + var arg2 uuid.UUID + if args[2] != nil { + arg2 = args[2].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockScopeResolver_RequireDatabase_Call) Return(database models.Database, err error) *MockScopeResolver_RequireDatabase_Call { + _c.Call.Return(database, err) + return _c +} + +func (_c *MockScopeResolver_RequireDatabase_Call) RunAndReturn(run func(c context.Context, projectId uuid.UUID, databaseId uuid.UUID) (models.Database, error)) *MockScopeResolver_RequireDatabase_Call { + _c.Call.Return(run) + return _c +} + +// RequireDatabaseEntity provides a mock function for the type MockScopeResolver +func (_mock *MockScopeResolver) RequireDatabaseEntity(c context.Context, projectId uuid.UUID, databaseId uuid.UUID, entityId uuid.UUID) (models.DatabaseEntity, error) { + ret := _mock.Called(c, projectId, databaseId, entityId) + + if len(ret) == 0 { + panic("no return value specified for RequireDatabaseEntity") + } + + var r0 models.DatabaseEntity + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID, uuid.UUID) (models.DatabaseEntity, error)); ok { + return returnFunc(c, projectId, databaseId, entityId) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID, uuid.UUID) models.DatabaseEntity); ok { + r0 = returnFunc(c, projectId, databaseId, entityId) + } else { + r0 = ret.Get(0).(models.DatabaseEntity) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, uuid.UUID, uuid.UUID, uuid.UUID) error); ok { + r1 = returnFunc(c, projectId, databaseId, entityId) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockScopeResolver_RequireDatabaseEntity_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RequireDatabaseEntity' +type MockScopeResolver_RequireDatabaseEntity_Call struct { + *mock.Call +} + +// RequireDatabaseEntity is a helper method to define mock.On call +// - c context.Context +// - projectId uuid.UUID +// - databaseId uuid.UUID +// - entityId uuid.UUID +func (_e *MockScopeResolver_Expecter) RequireDatabaseEntity(c interface{}, projectId interface{}, databaseId interface{}, entityId interface{}) *MockScopeResolver_RequireDatabaseEntity_Call { + return &MockScopeResolver_RequireDatabaseEntity_Call{Call: _e.mock.On("RequireDatabaseEntity", c, projectId, databaseId, entityId)} +} + +func (_c *MockScopeResolver_RequireDatabaseEntity_Call) Run(run func(c context.Context, projectId uuid.UUID, databaseId uuid.UUID, entityId uuid.UUID)) *MockScopeResolver_RequireDatabaseEntity_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 uuid.UUID + if args[1] != nil { + arg1 = args[1].(uuid.UUID) + } + var arg2 uuid.UUID + if args[2] != nil { + arg2 = args[2].(uuid.UUID) + } + var arg3 uuid.UUID + if args[3] != nil { + arg3 = args[3].(uuid.UUID) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockScopeResolver_RequireDatabaseEntity_Call) Return(databaseEntity models.DatabaseEntity, err error) *MockScopeResolver_RequireDatabaseEntity_Call { + _c.Call.Return(databaseEntity, err) + return _c +} + +func (_c *MockScopeResolver_RequireDatabaseEntity_Call) RunAndReturn(run func(c context.Context, projectId uuid.UUID, databaseId uuid.UUID, entityId uuid.UUID) (models.DatabaseEntity, error)) *MockScopeResolver_RequireDatabaseEntity_Call { + _c.Call.Return(run) + return _c +}