├── .dockerignore ├── .env ├── .github ├── codecov.yml ├── dependabot.yml └── workflows │ ├── checks.yml │ └── docker.yml ├── .gitignore ├── .sqlx ├── query-017d677fa8c669e19f35eb7aafeedd9c637e9d4a4050a006cf6833ed880da967.json ├── query-13c3c1b488e0e58fef68df99fed2baff4728ff60f5857f48b20fbfccd9208ec4.json ├── query-1ac68dc9329acb4c0f0c60cf27cfc0778132ae4f00bb4ee8a45e3c871cddafc3.json ├── query-1ca605a616bba3f45ff50f7409571e5f0254a8af06b60897d278144a3400c49b.json ├── query-1e4dd74f603ad34fe7a3dd7bbc90fd506264502e76632dfd7c156708e4f2ded6.json ├── query-348aab83a718675aba2850418fe15960712c575ba20d6ede6841714386d9064e.json ├── query-367f84a8c5f7967e974e7e695be843fd4d1308c745b268b19f929178d575d018.json ├── query-3c89d5a7b353321d84c8e2177f83192e6f338d89cf9be41c51a339eb42b3a474.json ├── query-42ba66ac61326b2398a1142b2d1a28f22f00afac27ea0bb4b08318546e1a5a91.json ├── query-464ea7d7d66e684eee3ad7f5103716f1a326917303e196839c2b3600838824f2.json ├── query-4b23b4975ecdedbadaca1d07d0645d30c18f4cd62a565cc44e7511cc34008474.json ├── query-4b2ef0b91c7a5653deb318e196f8c06dfe060616bcafa5ddc38468ed964e8abd.json ├── query-4ccc3142896718d0032b9da549cb1186846f42d49bc190f07ca94b6a5e558602.json ├── query-528ef377a6005365ec7c8e8a8c324d47b3a4d324e74cac449b48a3f9f612afef.json ├── query-61a3a672b60c14241fe120eaadd3ad42176388569a4f942bcd0b305fdc6dd332.json ├── query-6b352614cd8312ec08e45499f8c1c5d0765cc96cafeefa6ff5fe5bd1967a2d23.json ├── query-7ac387d3048467a5a58eea243ef018b1e6b0e3cb637cbb343fb432a721eda328.json ├── query-7c9a8981055380d8f02f608d435699aa9346c8cea7ecea9088ec5dd1539bc70c.json ├── query-82745fe65e7e130ae370679a8c97db85a07f892774e076b732f7ecba2545d44f.json ├── query-83c3eb4af09664e0fd86accedf3b4d160553283d9c8d6339d64cc41d4b1b8920.json ├── query-8776b0b3132a27f3b2d7bcbb60124b5f2b040c0de3d146ede1fb49123e350dfd.json ├── query-87e257fd0527200afa9d3b583859dca75bb5f9e4c5235aacc81a95ae5983ccf5.json ├── query-8d40ce7830507cde558f39040094808c5e7957b9f907053013aa8147bffc7a8b.json ├── query-8eb97945a120e5d67d684307c8ddf79aef9ca9f003c1c8b81878343fcfc05ce8.json ├── query-9202b711abd9f5e60ea86fe1c5d1608f91dcd6b907ca01504737f85cf93f6c03.json ├── query-92550886669e9c23dbc97717ba1d578e778335955f144a6701dcb4fb3424454b.json ├── query-93be606a8174a16168120900a94c75e8e552b90a3261e782d16d3b8805e80b98.json ├── query-9b30e33e870ad8d002f6dbb62d0d06db254138d06f28cf0079aabfa9c2d378f4.json ├── query-9f3e848e6f9fbef3ee5fc0fb1613acfcc6fbc4cd72d2c85abf4545cc3feb2359.json ├── query-a240be1eb2af2386d6ea101fa79a1e19bba44b3cc7d2d382807b750f29f60dc5.json ├── query-aed7cc730ddde8420590b38c674321f97c8961774c043dc5ab643b5e6357eb35.json ├── query-b256e88748a08dddd8ca5b1ab084010a8d8bce8171199ce253a429f68966175f.json ├── query-b2c02b607abacdae18ceeaafeb9daa339a54a686f9d7e256fbe5a0cde2f90628.json ├── query-b4a20eb4fe41ce59b26f20ee4eac1e19f6f2e8463b838456f872e10003eb1355.json ├── query-bb8a8aa90eddaea701908f35c7ad9a384e8af03558116c343cc70839d7e94613.json ├── query-ca3e10b369c5f05bdbc2e3c405992d3c4f2f33948890978fa79caceb5cabf227.json ├── query-cfbc449106654ab6c245538238648c54455fb4fdf08c0783ef3e42071a6b9d76.json ├── query-d07a243cb731a8fbc250e382d6d4b8ec472d278ecb9da5895da8e831f26bd7bf.json ├── query-d1245889f5dab872e8d1f9f178895505b9f10392316312567099acacca9bfc2c.json ├── query-d433cf76e8ec697933389b7d4dbae078dfaab4a5b7cd831a9f813386acb52a37.json ├── query-d52b6338b7923a007751ed891915527cf66f5230a73659f8430c47d0b183b00d.json ├── query-d7f75c56b51c161c14e287f60bbd3984fe9f4d144a7547484999a9598592f9e4.json ├── query-dc7b332e581b7806416ab745b4a30077c4abb5dcdfeee19f02d22fa7250579c3.json ├── query-e0bcb3d6f5cb5820a4c62cff51742671691cbf4d06c6cfec22e6d71cca0f9432.json ├── query-e6a59c961d3c5efc2e12760f19b3e6a819d6ac15412b4c8bd52611aaa909335e.json ├── query-ea3eb886a28adef5ce33230343ad5da9ce740018582ebaac3663724339e195b6.json ├── query-ec971dec31946e39d4d260ee4ef84a0a927c20556f53417ac9072dea64c2d7d0.json ├── query-f4a84cdd22d4cde0609b297c2dcc0c63884f59e8a850980c5c2bc51c9890a7cb.json ├── query-f718be26da8cbaeae4c25baadca3651cb5dcfb45368207cf11f16bd30bd06428.json ├── query-f947a48b3f9c03686fd0e7abd996dd9cda1e9a3cb2004a3f3ad330a24f885c1b.json ├── query-faf3290725899dc80b16c2233995a2b3002ebb0989f4d5284880226579d983b3.json └── query-feaeaa22a9b48b647e30cad8582e09c45ecec073fd32461071f3687ccd9c7e91.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING ├── COPYRIGHT ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── OpenADR_alliance_test_suite.png ├── README.md ├── SECURITY.md ├── clippy.toml ├── deny.toml ├── docker-compose.yml ├── fixtures ├── business.sql ├── events.sql ├── openadr_testsuite_user.sql ├── programs.sql ├── reports.sql ├── resources.sql ├── test_user_credentials.sql ├── users.sql ├── vens-programs.sql └── vens.sql ├── migrations └── 20240826084440_initial_scheme.sql ├── openleadr-client ├── Cargo.toml ├── README.md ├── migrations ├── src │ ├── bin │ │ ├── cli.rs │ │ └── everest.rs │ ├── error.rs │ ├── event.rs │ ├── lib.rs │ ├── program.rs │ ├── report.rs │ ├── resource.rs │ ├── target.rs │ ├── timeline.rs │ └── ven.rs └── tests │ ├── basic-read.rs │ ├── common │ └── mod.rs │ ├── event.rs │ ├── fixtures │ ├── program.rs │ ├── resources.rs │ └── ven.rs ├── openleadr-logo.svg ├── openleadr-vtn ├── Cargo.toml ├── README.md ├── build.rs ├── migrations ├── src │ ├── api │ │ ├── auth.rs │ │ ├── event.rs │ │ ├── fixtures │ │ ├── mod.rs │ │ ├── program.rs │ │ ├── report.rs │ │ ├── resource.rs │ │ ├── user.rs │ │ └── ven.rs │ ├── data_source │ │ ├── mod.rs │ │ └── postgres │ │ │ ├── event.rs │ │ │ ├── fixtures │ │ │ ├── mod.rs │ │ │ ├── program.rs │ │ │ ├── report.rs │ │ │ ├── resource.rs │ │ │ ├── user.rs │ │ │ └── ven.rs │ ├── error.rs │ ├── jwt.rs │ ├── lib.rs │ ├── main.rs │ └── state.rs └── tests │ └── assets │ └── public-rsa.pem ├── openleadr-wire ├── Cargo.toml └── src │ ├── event.rs │ ├── interval.rs │ ├── lib.rs │ ├── oauth.rs │ ├── problem.rs │ ├── program.rs │ ├── report.rs │ ├── resource.rs │ ├── target.rs │ ├── values_map.rs │ └── ven.rs ├── tests ├── dyn-price.oadr.yaml ├── load-sched.oadr.yaml ├── schema.yaml └── state-of-charge.oadr.yaml └── vtn.Dockerfile /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | vtn.Dockerfile 3 | docker-compose.yml 4 | .github -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://openadr:openadr@localhost:5432/openadr 2 | VTN_PORT=3000 3 | PG_PORT=5432 4 | PG_USER=openadr 5 | PG_DB=openadr 6 | PG_PASSWORD=openadr 7 | PG_TZ=Europe/Amsterdam 8 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | project: off 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | open-pull-requests-limit: 10 9 | groups: 10 | ci-dependencies: 11 | patterns: 12 | - "*" 13 | 14 | - package-ecosystem: cargo 15 | directory: "/" 16 | schedule: 17 | interval: daily 18 | versioning-strategy: lockfile-only 19 | open-pull-requests-limit: 10 20 | groups: 21 | cargo-dependencies: 22 | patterns: 23 | - "*" -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker images 2 | 3 | permissions: 4 | packages: write 5 | contents: read 6 | 7 | on: 8 | push: 9 | 10 | jobs: 11 | build-docker: 12 | name: Build Docker image 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout sources 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | with: 18 | persist-credentials: false 19 | - name: Create version tag 20 | shell: bash 21 | run: echo "tag=ghcr.io/openleadr/openleadr-rs:$(git show -s --format="%ct-%h" $GITHUB_SHA)" >> $GITHUB_ENV 22 | - name: Latest tag on main branch 23 | if: github.ref == 'refs/heads/main' 24 | run: echo "tag_main=,ghcr.io/openleadr/openleadr-rs:latest" >> $GITHUB_ENV 25 | - name: Login to GitHub Container Registry 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ghcr.io 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | - name: Set up Docker Buildx 32 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 33 | - name: Build Docker image 34 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 35 | with: 36 | context: . 37 | file: ./vtn.Dockerfile 38 | tags: "${{ env.tag }}${{ env.tag_main }}" 39 | push: true 40 | cache-from: type=gha 41 | cache-to: type=gha,mode=max -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | **/target 3 | oadr*.yaml 4 | *swp 5 | -------------------------------------------------------------------------------- /.sqlx/query-017d677fa8c669e19f35eb7aafeedd9c637e9d4a4050a006cf6833ed880da967.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO program (id,\n created_date_time,\n modification_date_time,\n program_name,\n program_long_name,\n retailer_name,\n retailer_long_name,\n program_type,\n country,\n principal_subdivision,\n interval_period,\n program_descriptions,\n binding_events,\n local_price,\n payload_descriptors,\n targets,\n business_id)\n VALUES (gen_random_uuid(), now(), now(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n RETURNING id,\n created_date_time,\n modification_date_time,\n program_name,\n program_long_name,\n retailer_name,\n retailer_long_name,\n program_type,\n country,\n principal_subdivision,\n interval_period,\n program_descriptions,\n binding_events,\n local_price,\n payload_descriptors,\n targets\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "program_long_name", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "retailer_name", 34 | "type_info": "Text" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "retailer_long_name", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "program_type", 44 | "type_info": "Text" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "country", 49 | "type_info": "Text" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "principal_subdivision", 54 | "type_info": "Text" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "interval_period", 59 | "type_info": "Jsonb" 60 | }, 61 | { 62 | "ordinal": 11, 63 | "name": "program_descriptions", 64 | "type_info": "Jsonb" 65 | }, 66 | { 67 | "ordinal": 12, 68 | "name": "binding_events", 69 | "type_info": "Bool" 70 | }, 71 | { 72 | "ordinal": 13, 73 | "name": "local_price", 74 | "type_info": "Bool" 75 | }, 76 | { 77 | "ordinal": 14, 78 | "name": "payload_descriptors", 79 | "type_info": "Jsonb" 80 | }, 81 | { 82 | "ordinal": 15, 83 | "name": "targets", 84 | "type_info": "Jsonb" 85 | } 86 | ], 87 | "parameters": { 88 | "Left": [ 89 | "Text", 90 | "Text", 91 | "Text", 92 | "Text", 93 | "Text", 94 | "Text", 95 | "Text", 96 | "Jsonb", 97 | "Jsonb", 98 | "Bool", 99 | "Bool", 100 | "Jsonb", 101 | "Jsonb", 102 | "Text" 103 | ] 104 | }, 105 | "nullable": [ 106 | false, 107 | false, 108 | false, 109 | false, 110 | true, 111 | true, 112 | true, 113 | true, 114 | true, 115 | true, 116 | true, 117 | true, 118 | true, 119 | true, 120 | true, 121 | true 122 | ] 123 | }, 124 | "hash": "017d677fa8c669e19f35eb7aafeedd9c637e9d4a4050a006cf6833ed880da967" 125 | } 126 | -------------------------------------------------------------------------------- /.sqlx/query-13c3c1b488e0e58fef68df99fed2baff4728ff60f5857f48b20fbfccd9208ec4.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE resource\n SET modification_date_time = now(),\n resource_name = $3,\n ven_id = $4,\n attributes = $5,\n targets = $6\n WHERE id = $1 AND ven_id = $2\n RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "resource_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "ven_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "attributes", 34 | "type_info": "Jsonb" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "targets", 39 | "type_info": "Jsonb" 40 | } 41 | ], 42 | "parameters": { 43 | "Left": [ 44 | "Text", 45 | "Text", 46 | "Text", 47 | "Text", 48 | "Jsonb", 49 | "Jsonb" 50 | ] 51 | }, 52 | "nullable": [ 53 | false, 54 | false, 55 | false, 56 | false, 57 | false, 58 | true, 59 | true 60 | ] 61 | }, 62 | "hash": "13c3c1b488e0e58fef68df99fed2baff4728ff60f5857f48b20fbfccd9208ec4" 63 | } 64 | -------------------------------------------------------------------------------- /.sqlx/query-1ac68dc9329acb4c0f0c60cf27cfc0778132ae4f00bb4ee8a45e3c871cddafc3.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM resource r\n WHERE r.id = $1 AND r.ven_id = $2\n RETURNING r.*\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "resource_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "ven_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "attributes", 34 | "type_info": "Jsonb" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "targets", 39 | "type_info": "Jsonb" 40 | } 41 | ], 42 | "parameters": { 43 | "Left": [ 44 | "Text", 45 | "Text" 46 | ] 47 | }, 48 | "nullable": [ 49 | false, 50 | false, 51 | false, 52 | false, 53 | false, 54 | true, 55 | true 56 | ] 57 | }, 58 | "hash": "1ac68dc9329acb4c0f0c60cf27cfc0778132ae4f00bb4ee8a45e3c871cddafc3" 59 | } 60 | -------------------------------------------------------------------------------- /.sqlx/query-1ca605a616bba3f45ff50f7409571e5f0254a8af06b60897d278144a3400c49b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO ven_program (program_id, ven_id)\n (SELECT $1, id FROM ven WHERE ven_name = ANY($2))\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text", 9 | "TextArray" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "1ca605a616bba3f45ff50f7409571e5f0254a8af06b60897d278144a3400c49b" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-1e4dd74f603ad34fe7a3dd7bbc90fd506264502e76632dfd7c156708e4f2ded6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM program p\n WHERE id = $1\n AND ($2::text IS NULL OR business_id = $2)\n RETURNING p.id,\n p.created_date_time,\n p.modification_date_time,\n p.program_name,\n p.program_long_name,\n p.retailer_name,\n p.retailer_long_name,\n p.program_type,\n p.country,\n p.principal_subdivision,\n p.interval_period,\n p.program_descriptions,\n p.binding_events,\n p.local_price,\n p.payload_descriptors,\n p.targets\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "program_long_name", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "retailer_name", 34 | "type_info": "Text" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "retailer_long_name", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "program_type", 44 | "type_info": "Text" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "country", 49 | "type_info": "Text" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "principal_subdivision", 54 | "type_info": "Text" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "interval_period", 59 | "type_info": "Jsonb" 60 | }, 61 | { 62 | "ordinal": 11, 63 | "name": "program_descriptions", 64 | "type_info": "Jsonb" 65 | }, 66 | { 67 | "ordinal": 12, 68 | "name": "binding_events", 69 | "type_info": "Bool" 70 | }, 71 | { 72 | "ordinal": 13, 73 | "name": "local_price", 74 | "type_info": "Bool" 75 | }, 76 | { 77 | "ordinal": 14, 78 | "name": "payload_descriptors", 79 | "type_info": "Jsonb" 80 | }, 81 | { 82 | "ordinal": 15, 83 | "name": "targets", 84 | "type_info": "Jsonb" 85 | } 86 | ], 87 | "parameters": { 88 | "Left": [ 89 | "Text", 90 | "Text" 91 | ] 92 | }, 93 | "nullable": [ 94 | false, 95 | false, 96 | false, 97 | false, 98 | true, 99 | true, 100 | true, 101 | true, 102 | true, 103 | true, 104 | true, 105 | true, 106 | true, 107 | true, 108 | true, 109 | true 110 | ] 111 | }, 112 | "hash": "1e4dd74f603ad34fe7a3dd7bbc90fd506264502e76632dfd7c156708e4f2ded6" 113 | } 114 | -------------------------------------------------------------------------------- /.sqlx/query-348aab83a718675aba2850418fe15960712c575ba20d6ede6841714386d9064e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO ven (\n id,\n created_date_time,\n modification_date_time,\n ven_name,\n attributes,\n targets\n )\n VALUES (gen_random_uuid(), now(), now(), $1, $2, $3)\n RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "ven_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "attributes", 29 | "type_info": "Jsonb" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "targets", 34 | "type_info": "Jsonb" 35 | } 36 | ], 37 | "parameters": { 38 | "Left": [ 39 | "Text", 40 | "Jsonb", 41 | "Jsonb" 42 | ] 43 | }, 44 | "nullable": [ 45 | false, 46 | false, 47 | false, 48 | false, 49 | true, 50 | true 51 | ] 52 | }, 53 | "hash": "348aab83a718675aba2850418fe15960712c575ba20d6ede6841714386d9064e" 54 | } 55 | -------------------------------------------------------------------------------- /.sqlx/query-367f84a8c5f7967e974e7e695be843fd4d1308c745b268b19f929178d575d018.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT\n r.id AS \"id!\", \n r.created_date_time AS \"created_date_time!\", \n r.modification_date_time AS \"modification_date_time!\",\n r.resource_name AS \"resource_name!\",\n r.ven_id AS \"ven_id!\",\n r.attributes,\n r.targets\n FROM resource r\n LEFT JOIN LATERAL ( \n \n SELECT targets.r_id,\n (t ->> 'type' = $3) AND\n (t -> 'values' ?| $4) AS target_test\n FROM (SELECT resource.id AS r_id,\n jsonb_array_elements(resource.targets) AS t\n FROM resource) AS targets\n \n )\n ON r.id = r_id\n WHERE r.ven_id = $1\n AND ($2::text IS NULL OR r.resource_name = $2)\n AND ($3 IS NULL OR $4 IS NULL OR target_test)\n ORDER BY r.created_date_time\n OFFSET $5 LIMIT $6\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id!", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time!", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time!", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "resource_name!", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "ven_id!", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "attributes", 34 | "type_info": "Jsonb" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "targets", 39 | "type_info": "Jsonb" 40 | } 41 | ], 42 | "parameters": { 43 | "Left": [ 44 | "Text", 45 | "Text", 46 | "Text", 47 | "TextArray", 48 | "Int8", 49 | "Int8" 50 | ] 51 | }, 52 | "nullable": [ 53 | false, 54 | false, 55 | false, 56 | false, 57 | false, 58 | true, 59 | true 60 | ] 61 | }, 62 | "hash": "367f84a8c5f7967e974e7e695be843fd4d1308c745b268b19f929178d575d018" 63 | } 64 | -------------------------------------------------------------------------------- /.sqlx/query-3c89d5a7b353321d84c8e2177f83192e6f338d89cf9be41c51a339eb42b3a474.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM user_manager WHERE user_id = $1 \n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "3c89d5a7b353321d84c8e2177f83192e6f338d89cf9be41c51a339eb42b3a474" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-42ba66ac61326b2398a1142b2d1a28f22f00afac27ea0bb4b08318546e1a5a91.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT DISTINCT\n v.id AS \"id!\", \n v.created_date_time AS \"created_date_time!\", \n v.modification_date_time AS \"modification_date_time!\",\n v.ven_name AS \"ven_name!\",\n v.attributes,\n v.targets\n FROM ven v\n LEFT JOIN resource r ON r.ven_id = v.id\n LEFT JOIN LATERAL (\n \n SELECT targets.v_id,\n (t ->> 'type' = $2) AND\n (t -> 'values' ?| $3) AS target_test\n FROM (SELECT ven.id AS v_id,\n jsonb_array_elements(ven.targets) AS t\n FROM ven) AS targets\n \n )\n ON v.id = v_id\n WHERE ($1::text IS NULL OR v.ven_name = $1)\n AND ($2 IS NULL OR $3 IS NULL OR target_test)\n AND ($4::text[] IS NULL OR v.id = ANY($4))\n ORDER BY v.created_date_time DESC\n OFFSET $5 LIMIT $6\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id!", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time!", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time!", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "ven_name!", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "attributes", 29 | "type_info": "Jsonb" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "targets", 34 | "type_info": "Jsonb" 35 | } 36 | ], 37 | "parameters": { 38 | "Left": [ 39 | "Text", 40 | "Text", 41 | "TextArray", 42 | "TextArray", 43 | "Int8", 44 | "Int8" 45 | ] 46 | }, 47 | "nullable": [ 48 | false, 49 | false, 50 | false, 51 | false, 52 | true, 53 | true 54 | ] 55 | }, 56 | "hash": "42ba66ac61326b2398a1142b2d1a28f22f00afac27ea0bb4b08318546e1a5a91" 57 | } 58 | -------------------------------------------------------------------------------- /.sqlx/query-464ea7d7d66e684eee3ad7f5103716f1a326917303e196839c2b3600838824f2.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT business_id FROM program WHERE id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "business_id", 9 | "type_info": "Text" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Text" 15 | ] 16 | }, 17 | "nullable": [ 18 | true 19 | ] 20 | }, 21 | "hash": "464ea7d7d66e684eee3ad7f5103716f1a326917303e196839c2b3600838824f2" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-4b23b4975ecdedbadaca1d07d0645d30c18f4cd62a565cc44e7511cc34008474.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE ven\n SET modification_date_time = now(),\n ven_name = $2,\n attributes = $3,\n targets = $4\n WHERE id = $1\n RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "ven_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "attributes", 29 | "type_info": "Jsonb" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "targets", 34 | "type_info": "Jsonb" 35 | } 36 | ], 37 | "parameters": { 38 | "Left": [ 39 | "Text", 40 | "Text", 41 | "Jsonb", 42 | "Jsonb" 43 | ] 44 | }, 45 | "nullable": [ 46 | false, 47 | false, 48 | false, 49 | false, 50 | true, 51 | true 52 | ] 53 | }, 54 | "hash": "4b23b4975ecdedbadaca1d07d0645d30c18f4cd62a565cc44e7511cc34008474" 55 | } 56 | -------------------------------------------------------------------------------- /.sqlx/query-4b2ef0b91c7a5653deb318e196f8c06dfe060616bcafa5ddc38468ed964e8abd.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM user_ven WHERE user_id = $1 \n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "4b2ef0b91c7a5653deb318e196f8c06dfe060616bcafa5ddc38468ed964e8abd" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-4ccc3142896718d0032b9da549cb1186846f42d49bc190f07ca94b6a5e558602.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE \"user\" SET\n reference = $2,\n description = $3,\n modified = now()\n WHERE id = $1\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text", 9 | "Text", 10 | "Text" 11 | ] 12 | }, 13 | "nullable": [] 14 | }, 15 | "hash": "4ccc3142896718d0032b9da549cb1186846f42d49bc190f07ca94b6a5e558602" 16 | } 17 | -------------------------------------------------------------------------------- /.sqlx/query-528ef377a6005365ec7c8e8a8c324d47b3a4d324e74cac449b48a3f9f612afef.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO user_ven (user_id, ven_id) VALUES ($1, $2)\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text", 9 | "Text" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "528ef377a6005365ec7c8e8a8c324d47b3a4d324e74cac449b48a3f9f612afef" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-61a3a672b60c14241fe120eaadd3ad42176388569a4f942bcd0b305fdc6dd332.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO event (id, created_date_time, modification_date_time, program_id, event_name, priority, targets, report_descriptors, payload_descriptors, interval_period, intervals)\n VALUES (gen_random_uuid(), now(), now(), $1, $2, $3, $4, $5, $6, $7, $8)\n RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_id", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "event_name", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "priority", 34 | "type_info": "Int8" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "report_descriptors", 39 | "type_info": "Jsonb" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "payload_descriptors", 44 | "type_info": "Jsonb" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "interval_period", 49 | "type_info": "Jsonb" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "intervals", 54 | "type_info": "Jsonb" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "targets", 59 | "type_info": "Jsonb" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Text", 65 | "Text", 66 | "Int8", 67 | "Jsonb", 68 | "Jsonb", 69 | "Jsonb", 70 | "Jsonb", 71 | "Jsonb" 72 | ] 73 | }, 74 | "nullable": [ 75 | false, 76 | false, 77 | false, 78 | false, 79 | true, 80 | true, 81 | true, 82 | true, 83 | true, 84 | false, 85 | true 86 | ] 87 | }, 88 | "hash": "61a3a672b60c14241fe120eaadd3ad42176388569a4f942bcd0b305fdc6dd332" 89 | } 90 | -------------------------------------------------------------------------------- /.sqlx/query-6b352614cd8312ec08e45499f8c1c5d0765cc96cafeefa6ff5fe5bd1967a2d23.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT\n id,\n created_date_time,\n modification_date_time,\n resource_name,\n ven_id,\n attributes,\n targets\n FROM resource\n WHERE ven_id = ANY($1)\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "resource_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "ven_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "attributes", 34 | "type_info": "Jsonb" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "targets", 39 | "type_info": "Jsonb" 40 | } 41 | ], 42 | "parameters": { 43 | "Left": [ 44 | "TextArray" 45 | ] 46 | }, 47 | "nullable": [ 48 | false, 49 | false, 50 | false, 51 | false, 52 | false, 53 | true, 54 | true 55 | ] 56 | }, 57 | "hash": "6b352614cd8312ec08e45499f8c1c5d0765cc96cafeefa6ff5fe5bd1967a2d23" 58 | } 59 | -------------------------------------------------------------------------------- /.sqlx/query-7ac387d3048467a5a58eea243ef018b1e6b0e3cb637cbb343fb432a721eda328.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT r.* \n FROM report r \n JOIN program p ON p.id = r.program_id \n LEFT JOIN ven_program vp ON vp.program_id = r.program_id\n WHERE r.id = $1 \n AND (\n ($2 AND (vp.ven_id IS NULL OR vp.ven_id = ANY($3))) \n OR \n ($4 AND ($5::text[] IS NULL OR p.business_id = ANY ($5)))\n )\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_id", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "event_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "client_name", 34 | "type_info": "Text" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "report_name", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "payload_descriptors", 44 | "type_info": "Jsonb" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "resources", 49 | "type_info": "Jsonb" 50 | } 51 | ], 52 | "parameters": { 53 | "Left": [ 54 | "Text", 55 | "Bool", 56 | "TextArray", 57 | "Bool", 58 | "TextArray" 59 | ] 60 | }, 61 | "nullable": [ 62 | false, 63 | false, 64 | false, 65 | false, 66 | false, 67 | false, 68 | true, 69 | true, 70 | false 71 | ] 72 | }, 73 | "hash": "7ac387d3048467a5a58eea243ef018b1e6b0e3cb637cbb343fb432a721eda328" 74 | } 75 | -------------------------------------------------------------------------------- /.sqlx/query-7c9a8981055380d8f02f608d435699aa9346c8cea7ecea9088ec5dd1539bc70c.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM user_credentials WHERE user_id = $1 AND client_id = $2\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text", 9 | "Text" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "7c9a8981055380d8f02f608d435699aa9346c8cea7ecea9088ec5dd1539bc70c" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-82745fe65e7e130ae370679a8c97db85a07f892774e076b732f7ecba2545d44f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM ven_program WHERE program_id = $1\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "82745fe65e7e130ae370679a8c97db85a07f892774e076b732f7ecba2545d44f" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-83c3eb4af09664e0fd86accedf3b4d160553283d9c8d6339d64cc41d4b1b8920.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM any_business_user WHERE user_id = $1 \n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "83c3eb4af09664e0fd86accedf3b4d160553283d9c8d6339d64cc41d4b1b8920" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-8776b0b3132a27f3b2d7bcbb60124b5f2b040c0de3d146ede1fb49123e350dfd.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO ven_program (program_id, ven_id)\n (SELECT $1, id FROM ven WHERE ven_name = ANY ($2))\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text", 9 | "TextArray" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "8776b0b3132a27f3b2d7bcbb60124b5f2b040c0de3d146ede1fb49123e350dfd" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-87e257fd0527200afa9d3b583859dca75bb5f9e4c5235aacc81a95ae5983ccf5.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT r.*\n FROM report r\n JOIN program p ON p.id = r.program_id\n LEFT JOIN ven_program v ON v.program_id = r.program_id\n WHERE ($1::text IS NULL OR $1 like r.program_id)\n AND ($2::text IS NULL OR $2 like r.event_id)\n AND ($3::text IS NULL OR $3 like r.client_name)\n AND (\n ($4 AND (v.ven_id IS NULL OR v.ven_id = ANY($5))) \n OR \n ($6 AND ($7::text[] IS NULL OR p.business_id = ANY ($7)))\n )\n ORDER BY r.created_date_time DESC\n OFFSET $8 LIMIT $9\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_id", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "event_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "client_name", 34 | "type_info": "Text" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "report_name", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "payload_descriptors", 44 | "type_info": "Jsonb" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "resources", 49 | "type_info": "Jsonb" 50 | } 51 | ], 52 | "parameters": { 53 | "Left": [ 54 | "Text", 55 | "Text", 56 | "Text", 57 | "Bool", 58 | "TextArray", 59 | "Bool", 60 | "TextArray", 61 | "Int8", 62 | "Int8" 63 | ] 64 | }, 65 | "nullable": [ 66 | false, 67 | false, 68 | false, 69 | false, 70 | false, 71 | false, 72 | true, 73 | true, 74 | false 75 | ] 76 | }, 77 | "hash": "87e257fd0527200afa9d3b583859dca75bb5f9e4c5235aacc81a95ae5983ccf5" 78 | } 79 | -------------------------------------------------------------------------------- /.sqlx/query-8d40ce7830507cde558f39040094808c5e7957b9f907053013aa8147bffc7a8b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO user_business (user_id, business_id) VALUES ($1, $2)\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text", 9 | "Text" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "8d40ce7830507cde558f39040094808c5e7957b9f907053013aa8147bffc7a8b" 15 | } 16 | -------------------------------------------------------------------------------- /.sqlx/query-8eb97945a120e5d67d684307c8ddf79aef9ca9f003c1c8b81878343fcfc05ce8.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO any_business_user (user_id) VALUES ($1)\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "8eb97945a120e5d67d684307c8ddf79aef9ca9f003c1c8b81878343fcfc05ce8" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-9202b711abd9f5e60ea86fe1c5d1608f91dcd6b907ca01504737f85cf93f6c03.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT\n id,\n created_date_time,\n modification_date_time,\n resource_name,\n ven_id,\n attributes,\n targets\n FROM resource\n WHERE id = $1 AND ven_id = $2\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "resource_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "ven_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "attributes", 34 | "type_info": "Jsonb" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "targets", 39 | "type_info": "Jsonb" 40 | } 41 | ], 42 | "parameters": { 43 | "Left": [ 44 | "Text", 45 | "Text" 46 | ] 47 | }, 48 | "nullable": [ 49 | false, 50 | false, 51 | false, 52 | false, 53 | false, 54 | true, 55 | true 56 | ] 57 | }, 58 | "hash": "9202b711abd9f5e60ea86fe1c5d1608f91dcd6b907ca01504737f85cf93f6c03" 59 | } 60 | -------------------------------------------------------------------------------- /.sqlx/query-92550886669e9c23dbc97717ba1d578e778335955f144a6701dcb4fb3424454b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT\n id,\n created_date_time,\n modification_date_time,\n resource_name,\n ven_id,\n attributes,\n targets\n FROM resource\n WHERE ven_id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "resource_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "ven_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "attributes", 34 | "type_info": "Jsonb" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "targets", 39 | "type_info": "Jsonb" 40 | } 41 | ], 42 | "parameters": { 43 | "Left": [ 44 | "Text" 45 | ] 46 | }, 47 | "nullable": [ 48 | false, 49 | false, 50 | false, 51 | false, 52 | false, 53 | true, 54 | true 55 | ] 56 | }, 57 | "hash": "92550886669e9c23dbc97717ba1d578e778335955f144a6701dcb4fb3424454b" 58 | } 59 | -------------------------------------------------------------------------------- /.sqlx/query-93be606a8174a16168120900a94c75e8e552b90a3261e782d16d3b8805e80b98.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT id,\n client_secret\n FROM \"user\"\n JOIN user_credentials ON user_id = id\n WHERE client_id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "client_secret", 14 | "type_info": "Text" 15 | } 16 | ], 17 | "parameters": { 18 | "Left": [ 19 | "Text" 20 | ] 21 | }, 22 | "nullable": [ 23 | false, 24 | false 25 | ] 26 | }, 27 | "hash": "93be606a8174a16168120900a94c75e8e552b90a3261e782d16d3b8805e80b98" 28 | } 29 | -------------------------------------------------------------------------------- /.sqlx/query-9b30e33e870ad8d002f6dbb62d0d06db254138d06f28cf0079aabfa9c2d378f4.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT program_id AS id FROM event WHERE id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Text" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "9b30e33e870ad8d002f6dbb62d0d06db254138d06f28cf0079aabfa9c2d378f4" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-9f3e848e6f9fbef3ee5fc0fb1613acfcc6fbc4cd72d2c85abf4545cc3feb2359.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT p.id AS \"id!\", \n p.created_date_time AS \"created_date_time!\", \n p.modification_date_time AS \"modification_date_time!\",\n p.program_name AS \"program_name!\",\n p.program_long_name,\n p.retailer_name,\n p.retailer_long_name,\n p.program_type,\n p.country,\n p.principal_subdivision,\n p.interval_period,\n p.program_descriptions,\n p.binding_events,\n p.local_price,\n p.payload_descriptors,\n p.targets\n FROM program p\n LEFT JOIN ven_program vp ON p.id = vp.program_id\n LEFT JOIN ven v ON v.id = vp.ven_id\n LEFT JOIN LATERAL (\n\n SELECT targets.p_id,\n (t ->> 'type' = $1) AND\n (t -> 'values' ?| $2) AS target_test\n FROM (SELECT program.id AS p_id,\n jsonb_array_elements(program.targets) AS t\n FROM program) AS targets\n \n )\n ON p.id = p_id\n WHERE ($1 IS NULL OR $2 IS NULL OR target_test)\n AND (\n ($3 AND (vp.ven_id IS NULL OR vp.ven_id = ANY($4)))\n OR\n ($5)\n )\n GROUP BY p.id, p.created_date_time\n ORDER BY p.created_date_time DESC\n OFFSET $6 LIMIT $7\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id!", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time!", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time!", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_name!", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "program_long_name", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "retailer_name", 34 | "type_info": "Text" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "retailer_long_name", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "program_type", 44 | "type_info": "Text" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "country", 49 | "type_info": "Text" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "principal_subdivision", 54 | "type_info": "Text" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "interval_period", 59 | "type_info": "Jsonb" 60 | }, 61 | { 62 | "ordinal": 11, 63 | "name": "program_descriptions", 64 | "type_info": "Jsonb" 65 | }, 66 | { 67 | "ordinal": 12, 68 | "name": "binding_events", 69 | "type_info": "Bool" 70 | }, 71 | { 72 | "ordinal": 13, 73 | "name": "local_price", 74 | "type_info": "Bool" 75 | }, 76 | { 77 | "ordinal": 14, 78 | "name": "payload_descriptors", 79 | "type_info": "Jsonb" 80 | }, 81 | { 82 | "ordinal": 15, 83 | "name": "targets", 84 | "type_info": "Jsonb" 85 | } 86 | ], 87 | "parameters": { 88 | "Left": [ 89 | "Text", 90 | "TextArray", 91 | "Bool", 92 | "TextArray", 93 | "Bool", 94 | "Int8", 95 | "Int8" 96 | ] 97 | }, 98 | "nullable": [ 99 | false, 100 | false, 101 | false, 102 | false, 103 | true, 104 | true, 105 | true, 106 | true, 107 | true, 108 | true, 109 | true, 110 | true, 111 | true, 112 | true, 113 | true, 114 | true 115 | ] 116 | }, 117 | "hash": "9f3e848e6f9fbef3ee5fc0fb1613acfcc6fbc4cd72d2c85abf4545cc3feb2359" 118 | } 119 | -------------------------------------------------------------------------------- /.sqlx/query-a240be1eb2af2386d6ea101fa79a1e19bba44b3cc7d2d382807b750f29f60dc5.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM ven\n WHERE id = $1\n RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "ven_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "attributes", 29 | "type_info": "Jsonb" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "targets", 34 | "type_info": "Jsonb" 35 | } 36 | ], 37 | "parameters": { 38 | "Left": [ 39 | "Text" 40 | ] 41 | }, 42 | "nullable": [ 43 | false, 44 | false, 45 | false, 46 | false, 47 | true, 48 | true 49 | ] 50 | }, 51 | "hash": "a240be1eb2af2386d6ea101fa79a1e19bba44b3cc7d2d382807b750f29f60dc5" 52 | } 53 | -------------------------------------------------------------------------------- /.sqlx/query-aed7cc730ddde8420590b38c674321f97c8961774c043dc5ab643b5e6357eb35.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO \"user\" (id, reference, description, created, modified)\n VALUES (gen_random_uuid(), $1, $2, now(), now())\n RETURNING id\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Text", 15 | "Text" 16 | ] 17 | }, 18 | "nullable": [ 19 | false 20 | ] 21 | }, 22 | "hash": "aed7cc730ddde8420590b38c674321f97c8961774c043dc5ab643b5e6357eb35" 23 | } 24 | -------------------------------------------------------------------------------- /.sqlx/query-b256e88748a08dddd8ca5b1ab084010a8d8bce8171199ce253a429f68966175f.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT u.*,\n array_agg(DISTINCT c.client_id) FILTER ( WHERE c.client_id IS NOT NULL ) AS client_ids,\n array_agg(DISTINCT b.business_id) FILTER ( WHERE b.business_id IS NOT NULL ) AS business_ids,\n array_agg(DISTINCT ven.ven_id) FILTER ( WHERE ven.ven_id IS NOT NULL ) AS ven_ids,\n ab.user_id IS NOT NULL AS \"is_any_business_user!\",\n um.user_id IS NOT NULL AS \"is_user_manager!\",\n vm.user_id IS NOT NULL AS \"is_ven_manager!\"\n FROM \"user\" u\n LEFT JOIN user_credentials c ON c.user_id = u.id\n LEFT JOIN any_business_user ab ON u.id = ab.user_id\n LEFT JOIN user_business b ON u.id = b.user_id\n LEFT JOIN user_manager um ON u.id = um.user_id\n LEFT JOIN user_ven ven ON u.id = ven.user_id\n LEFT JOIN ven_manager vm ON u.id = vm.user_id\n GROUP BY u.id,\n u.created,\n b.user_id,\n ab.user_id,\n um.user_id,\n ven.user_id,\n vm.user_id\n ORDER BY u.created\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "reference", 14 | "type_info": "Text" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "description", 19 | "type_info": "Text" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "created", 24 | "type_info": "Timestamptz" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "modified", 29 | "type_info": "Timestamptz" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "client_ids", 34 | "type_info": "TextArray" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "business_ids", 39 | "type_info": "TextArray" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "ven_ids", 44 | "type_info": "TextArray" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "is_any_business_user!", 49 | "type_info": "Bool" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "is_user_manager!", 54 | "type_info": "Bool" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "is_ven_manager!", 59 | "type_info": "Bool" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [] 64 | }, 65 | "nullable": [ 66 | false, 67 | false, 68 | true, 69 | false, 70 | false, 71 | null, 72 | null, 73 | null, 74 | null, 75 | null, 76 | null 77 | ] 78 | }, 79 | "hash": "b256e88748a08dddd8ca5b1ab084010a8d8bce8171199ce253a429f68966175f" 80 | } 81 | -------------------------------------------------------------------------------- /.sqlx/query-b2c02b607abacdae18ceeaafeb9daa339a54a686f9d7e256fbe5a0cde2f90628.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT *\n FROM ven\n WHERE id = $1\n AND ($2::text[] IS NULL OR id = ANY($2))\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "ven_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "attributes", 29 | "type_info": "Jsonb" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "targets", 34 | "type_info": "Jsonb" 35 | } 36 | ], 37 | "parameters": { 38 | "Left": [ 39 | "Text", 40 | "TextArray" 41 | ] 42 | }, 43 | "nullable": [ 44 | false, 45 | false, 46 | false, 47 | false, 48 | true, 49 | true 50 | ] 51 | }, 52 | "hash": "b2c02b607abacdae18ceeaafeb9daa339a54a686f9d7e256fbe5a0cde2f90628" 53 | } 54 | -------------------------------------------------------------------------------- /.sqlx/query-b4a20eb4fe41ce59b26f20ee4eac1e19f6f2e8463b838456f872e10003eb1355.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE program p\n SET modification_date_time = now(),\n program_name = $2,\n program_long_name = $3,\n retailer_name = $4,\n retailer_long_name = $5,\n program_type = $6,\n country = $7,\n principal_subdivision = $8,\n interval_period = $9,\n program_descriptions = $10,\n binding_events = $11,\n local_price = $12,\n payload_descriptors = $13,\n targets = $14\n WHERE id = $1\n AND ($15::text IS NULL OR business_id = $15)\n RETURNING p.id,\n p.created_date_time,\n p.modification_date_time,\n p.program_name,\n p.program_long_name,\n p.retailer_name,\n p.retailer_long_name,\n p.program_type,\n p.country,\n p.principal_subdivision,\n p.interval_period,\n p.program_descriptions,\n p.binding_events,\n p.local_price,\n p.payload_descriptors,\n p.targets\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "program_long_name", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "retailer_name", 34 | "type_info": "Text" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "retailer_long_name", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "program_type", 44 | "type_info": "Text" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "country", 49 | "type_info": "Text" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "principal_subdivision", 54 | "type_info": "Text" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "interval_period", 59 | "type_info": "Jsonb" 60 | }, 61 | { 62 | "ordinal": 11, 63 | "name": "program_descriptions", 64 | "type_info": "Jsonb" 65 | }, 66 | { 67 | "ordinal": 12, 68 | "name": "binding_events", 69 | "type_info": "Bool" 70 | }, 71 | { 72 | "ordinal": 13, 73 | "name": "local_price", 74 | "type_info": "Bool" 75 | }, 76 | { 77 | "ordinal": 14, 78 | "name": "payload_descriptors", 79 | "type_info": "Jsonb" 80 | }, 81 | { 82 | "ordinal": 15, 83 | "name": "targets", 84 | "type_info": "Jsonb" 85 | } 86 | ], 87 | "parameters": { 88 | "Left": [ 89 | "Text", 90 | "Text", 91 | "Text", 92 | "Text", 93 | "Text", 94 | "Text", 95 | "Text", 96 | "Text", 97 | "Jsonb", 98 | "Jsonb", 99 | "Bool", 100 | "Bool", 101 | "Jsonb", 102 | "Jsonb", 103 | "Text" 104 | ] 105 | }, 106 | "nullable": [ 107 | false, 108 | false, 109 | false, 110 | false, 111 | true, 112 | true, 113 | true, 114 | true, 115 | true, 116 | true, 117 | true, 118 | true, 119 | true, 120 | true, 121 | true, 122 | true 123 | ] 124 | }, 125 | "hash": "b4a20eb4fe41ce59b26f20ee4eac1e19f6f2e8463b838456f872e10003eb1355" 126 | } 127 | -------------------------------------------------------------------------------- /.sqlx/query-bb8a8aa90eddaea701908f35c7ad9a384e8af03558116c343cc70839d7e94613.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT p.id,\n p.created_date_time,\n p.modification_date_time,\n p.program_name,\n p.program_long_name,\n p.retailer_name,\n p.retailer_long_name,\n p.program_type,\n p.country,\n p.principal_subdivision,\n p.interval_period,\n p.program_descriptions,\n p.binding_events,\n p.local_price,\n p.payload_descriptors,\n p.targets\n FROM program p\n LEFT JOIN ven_program vp ON p.id = vp.program_id\n WHERE id = $1\n AND (NOT $2 OR vp.ven_id IS NULL OR vp.ven_id = ANY($3)) -- Filter for VEN ids\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "program_long_name", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "retailer_name", 34 | "type_info": "Text" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "retailer_long_name", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "program_type", 44 | "type_info": "Text" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "country", 49 | "type_info": "Text" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "principal_subdivision", 54 | "type_info": "Text" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "interval_period", 59 | "type_info": "Jsonb" 60 | }, 61 | { 62 | "ordinal": 11, 63 | "name": "program_descriptions", 64 | "type_info": "Jsonb" 65 | }, 66 | { 67 | "ordinal": 12, 68 | "name": "binding_events", 69 | "type_info": "Bool" 70 | }, 71 | { 72 | "ordinal": 13, 73 | "name": "local_price", 74 | "type_info": "Bool" 75 | }, 76 | { 77 | "ordinal": 14, 78 | "name": "payload_descriptors", 79 | "type_info": "Jsonb" 80 | }, 81 | { 82 | "ordinal": 15, 83 | "name": "targets", 84 | "type_info": "Jsonb" 85 | } 86 | ], 87 | "parameters": { 88 | "Left": [ 89 | "Text", 90 | "Bool", 91 | "TextArray" 92 | ] 93 | }, 94 | "nullable": [ 95 | false, 96 | false, 97 | false, 98 | false, 99 | true, 100 | true, 101 | true, 102 | true, 103 | true, 104 | true, 105 | true, 106 | true, 107 | true, 108 | true, 109 | true, 110 | true 111 | ] 112 | }, 113 | "hash": "bb8a8aa90eddaea701908f35c7ad9a384e8af03558116c343cc70839d7e94613" 114 | } 115 | -------------------------------------------------------------------------------- /.sqlx/query-ca3e10b369c5f05bdbc2e3c405992d3c4f2f33948890978fa79caceb5cabf227.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT e.*\n FROM event e\n JOIN program p on p.id = e.program_id\n LEFT JOIN ven_program vp ON p.id = vp.program_id\n LEFT JOIN LATERAL (\n \n SELECT targets.e_id,\n (t ->> 'type' = $2) AND\n (t -> 'values' ?| $3) AS target_test\n FROM (SELECT event.id AS e_id,\n jsonb_array_elements(event.targets) AS t\n FROM event) AS targets\n \n )\n ON e.id = e_id\n WHERE ($1::text IS NULL OR e.program_id like $1)\n AND ($2 IS NULL OR $3 IS NULL OR target_test)\n AND (\n ($4 AND (vp.ven_id IS NULL OR vp.ven_id = ANY($5)))\n OR \n ($6 AND ($7::text[] IS NULL OR p.business_id = ANY ($7)))\n )\n GROUP BY e.id, e.priority, e.created_date_time\n ORDER BY priority ASC , created_date_time DESC\n OFFSET $8 LIMIT $9\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_id", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "event_name", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "priority", 34 | "type_info": "Int8" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "report_descriptors", 39 | "type_info": "Jsonb" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "payload_descriptors", 44 | "type_info": "Jsonb" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "interval_period", 49 | "type_info": "Jsonb" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "intervals", 54 | "type_info": "Jsonb" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "targets", 59 | "type_info": "Jsonb" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Text", 65 | "Text", 66 | "TextArray", 67 | "Bool", 68 | "TextArray", 69 | "Bool", 70 | "TextArray", 71 | "Int8", 72 | "Int8" 73 | ] 74 | }, 75 | "nullable": [ 76 | false, 77 | false, 78 | false, 79 | false, 80 | true, 81 | true, 82 | true, 83 | true, 84 | true, 85 | false, 86 | true 87 | ] 88 | }, 89 | "hash": "ca3e10b369c5f05bdbc2e3c405992d3c4f2f33948890978fa79caceb5cabf227" 90 | } 91 | -------------------------------------------------------------------------------- /.sqlx/query-cfbc449106654ab6c245538238648c54455fb4fdf08c0783ef3e42071a6b9d76.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO user_manager (user_id) VALUES ($1)\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "cfbc449106654ab6c245538238648c54455fb4fdf08c0783ef3e42071a6b9d76" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-d07a243cb731a8fbc250e382d6d4b8ec472d278ecb9da5895da8e831f26bd7bf.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE report r\n SET modification_date_time = now(),\n program_id = $6,\n event_id = $7,\n client_name = $8,\n report_name = $9,\n payload_descriptors = $10,\n resources = $11\n FROM program p\n LEFT JOIN ven_program v ON p.id = v.program_id\n WHERE r.id = $1\n AND (p.id = r.program_id)\n AND (\n ($2 AND (v.ven_id IS NULL OR v.ven_id = ANY($3))) \n OR \n ($4 AND ($5::text[] IS NULL OR p.business_id = ANY ($5)))\n )\n RETURNING r.*\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_id", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "event_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "client_name", 34 | "type_info": "Text" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "report_name", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "payload_descriptors", 44 | "type_info": "Jsonb" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "resources", 49 | "type_info": "Jsonb" 50 | } 51 | ], 52 | "parameters": { 53 | "Left": [ 54 | "Text", 55 | "Bool", 56 | "TextArray", 57 | "Bool", 58 | "TextArray", 59 | "Text", 60 | "Text", 61 | "Text", 62 | "Text", 63 | "Jsonb", 64 | "Jsonb" 65 | ] 66 | }, 67 | "nullable": [ 68 | false, 69 | false, 70 | false, 71 | false, 72 | false, 73 | false, 74 | true, 75 | true, 76 | false 77 | ] 78 | }, 79 | "hash": "d07a243cb731a8fbc250e382d6d4b8ec472d278ecb9da5895da8e831f26bd7bf" 80 | } 81 | -------------------------------------------------------------------------------- /.sqlx/query-d1245889f5dab872e8d1f9f178895505b9f10392316312567099acacca9bfc2c.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM ven_manager WHERE user_id = $1 \n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "d1245889f5dab872e8d1f9f178895505b9f10392316312567099acacca9bfc2c" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-d433cf76e8ec697933389b7d4dbae078dfaab4a5b7cd831a9f813386acb52a37.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO user_credentials \n (user_id, client_id, client_secret) \n VALUES \n ($1, $2, $3)\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text", 9 | "Text", 10 | "Text" 11 | ] 12 | }, 13 | "nullable": [] 14 | }, 15 | "hash": "d433cf76e8ec697933389b7d4dbae078dfaab4a5b7cd831a9f813386acb52a37" 16 | } 17 | -------------------------------------------------------------------------------- /.sqlx/query-d52b6338b7923a007751ed891915527cf66f5230a73659f8430c47d0b183b00d.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO ven_manager (user_id) VALUES ($1)\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "d52b6338b7923a007751ed891915527cf66f5230a73659f8430c47d0b183b00d" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-d7f75c56b51c161c14e287f60bbd3984fe9f4d144a7547484999a9598592f9e4.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT e.*\n FROM event e\n JOIN program p ON e.program_id = p.id\n LEFT JOIN ven_program vp ON p.id = vp.program_id\n WHERE e.id = $1\n AND (\n ($2 AND (vp.ven_id IS NULL OR vp.ven_id = ANY($3))) \n OR \n ($4 AND ($5::text[] IS NULL OR p.business_id = ANY ($5)))\n )\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_id", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "event_name", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "priority", 34 | "type_info": "Int8" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "report_descriptors", 39 | "type_info": "Jsonb" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "payload_descriptors", 44 | "type_info": "Jsonb" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "interval_period", 49 | "type_info": "Jsonb" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "intervals", 54 | "type_info": "Jsonb" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "targets", 59 | "type_info": "Jsonb" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Text", 65 | "Bool", 66 | "TextArray", 67 | "Bool", 68 | "TextArray" 69 | ] 70 | }, 71 | "nullable": [ 72 | false, 73 | false, 74 | false, 75 | false, 76 | true, 77 | true, 78 | true, 79 | true, 80 | true, 81 | false, 82 | true 83 | ] 84 | }, 85 | "hash": "d7f75c56b51c161c14e287f60bbd3984fe9f4d144a7547484999a9598592f9e4" 86 | } 87 | -------------------------------------------------------------------------------- /.sqlx/query-dc7b332e581b7806416ab745b4a30077c4abb5dcdfeee19f02d22fa7250579c3.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM event WHERE id = $1 RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_id", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "event_name", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "priority", 34 | "type_info": "Int8" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "report_descriptors", 39 | "type_info": "Jsonb" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "payload_descriptors", 44 | "type_info": "Jsonb" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "interval_period", 49 | "type_info": "Jsonb" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "intervals", 54 | "type_info": "Jsonb" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "targets", 59 | "type_info": "Jsonb" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Text" 65 | ] 66 | }, 67 | "nullable": [ 68 | false, 69 | false, 70 | false, 71 | false, 72 | true, 73 | true, 74 | true, 75 | true, 76 | true, 77 | false, 78 | true 79 | ] 80 | }, 81 | "hash": "dc7b332e581b7806416ab745b4a30077c4abb5dcdfeee19f02d22fa7250579c3" 82 | } 83 | -------------------------------------------------------------------------------- /.sqlx/query-e0bcb3d6f5cb5820a4c62cff51742671691cbf4d06c6cfec22e6d71cca0f9432.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO resource (\n id,\n created_date_time,\n modification_date_time,\n resource_name,\n ven_id,\n attributes,\n targets\n )\n VALUES (gen_random_uuid(), now(), now(), $1, $2, $3, $4)\n RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "resource_name", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "ven_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "attributes", 34 | "type_info": "Jsonb" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "targets", 39 | "type_info": "Jsonb" 40 | } 41 | ], 42 | "parameters": { 43 | "Left": [ 44 | "Text", 45 | "Text", 46 | "Jsonb", 47 | "Jsonb" 48 | ] 49 | }, 50 | "nullable": [ 51 | false, 52 | false, 53 | false, 54 | false, 55 | false, 56 | true, 57 | true 58 | ] 59 | }, 60 | "hash": "e0bcb3d6f5cb5820a4c62cff51742671691cbf4d06c6cfec22e6d71cca0f9432" 61 | } 62 | -------------------------------------------------------------------------------- /.sqlx/query-e6a59c961d3c5efc2e12760f19b3e6a819d6ac15412b4c8bd52611aaa909335e.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT u.*,\n array_agg(DISTINCT c.client_id) FILTER ( WHERE c.client_id IS NOT NULL ) AS client_ids,\n array_agg(DISTINCT b.business_id) FILTER ( WHERE b.business_id IS NOT NULL ) AS business_ids,\n array_agg(DISTINCT ven.ven_id) FILTER ( WHERE ven.ven_id IS NOT NULL ) AS ven_ids,\n ab.user_id IS NOT NULL AS \"is_any_business_user!\",\n um.user_id IS NOT NULL AS \"is_user_manager!\",\n vm.user_id IS NOT NULL AS \"is_ven_manager!\"\n FROM \"user\" u\n LEFT JOIN user_credentials c ON c.user_id = u.id\n LEFT JOIN any_business_user ab ON u.id = ab.user_id\n LEFT JOIN user_business b ON u.id = b.user_id\n LEFT JOIN user_manager um ON u.id = um.user_id\n LEFT JOIN user_ven ven ON u.id = ven.user_id\n LEFT JOIN ven_manager vm ON u.id = vm.user_id\n WHERE u.id = $1\n GROUP BY u.id,\n b.user_id,\n ab.user_id,\n um.user_id,\n ven.user_id,\n vm.user_id\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "reference", 14 | "type_info": "Text" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "description", 19 | "type_info": "Text" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "created", 24 | "type_info": "Timestamptz" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "modified", 29 | "type_info": "Timestamptz" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "client_ids", 34 | "type_info": "TextArray" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "business_ids", 39 | "type_info": "TextArray" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "ven_ids", 44 | "type_info": "TextArray" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "is_any_business_user!", 49 | "type_info": "Bool" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "is_user_manager!", 54 | "type_info": "Bool" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "is_ven_manager!", 59 | "type_info": "Bool" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Text" 65 | ] 66 | }, 67 | "nullable": [ 68 | false, 69 | false, 70 | true, 71 | false, 72 | false, 73 | null, 74 | null, 75 | null, 76 | null, 77 | null, 78 | null 79 | ] 80 | }, 81 | "hash": "e6a59c961d3c5efc2e12760f19b3e6a819d6ac15412b4c8bd52611aaa909335e" 82 | } 83 | -------------------------------------------------------------------------------- /.sqlx/query-ea3eb886a28adef5ce33230343ad5da9ce740018582ebaac3663724339e195b6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "SELECT program_id AS id FROM event WHERE id = $1", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Text" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "ea3eb886a28adef5ce33230343ad5da9ce740018582ebaac3663724339e195b6" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-ec971dec31946e39d4d260ee4ef84a0a927c20556f53417ac9072dea64c2d7d0.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n SELECT ven_id AS id FROM ven_program WHERE program_id = $1\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | } 11 | ], 12 | "parameters": { 13 | "Left": [ 14 | "Text" 15 | ] 16 | }, 17 | "nullable": [ 18 | false 19 | ] 20 | }, 21 | "hash": "ec971dec31946e39d4d260ee4ef84a0a927c20556f53417ac9072dea64c2d7d0" 22 | } 23 | -------------------------------------------------------------------------------- /.sqlx/query-f4a84cdd22d4cde0609b297c2dcc0c63884f59e8a850980c5c2bc51c9890a7cb.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n INSERT INTO report (id, created_date_time, modification_date_time, program_id, event_id, client_name, report_name, payload_descriptors, resources)\n VALUES (gen_random_uuid(), now(), now(), $1, $2, $3, $4, $5, $6)\n RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_id", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "event_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "client_name", 34 | "type_info": "Text" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "report_name", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "payload_descriptors", 44 | "type_info": "Jsonb" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "resources", 49 | "type_info": "Jsonb" 50 | } 51 | ], 52 | "parameters": { 53 | "Left": [ 54 | "Text", 55 | "Text", 56 | "Text", 57 | "Text", 58 | "Jsonb", 59 | "Jsonb" 60 | ] 61 | }, 62 | "nullable": [ 63 | false, 64 | false, 65 | false, 66 | false, 67 | false, 68 | false, 69 | true, 70 | true, 71 | false 72 | ] 73 | }, 74 | "hash": "f4a84cdd22d4cde0609b297c2dcc0c63884f59e8a850980c5c2bc51c9890a7cb" 75 | } 76 | -------------------------------------------------------------------------------- /.sqlx/query-f718be26da8cbaeae4c25baadca3651cb5dcfb45368207cf11f16bd30bd06428.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM user_business WHERE user_id = $1 \n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "f718be26da8cbaeae4c25baadca3651cb5dcfb45368207cf11f16bd30bd06428" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-f947a48b3f9c03686fd0e7abd996dd9cda1e9a3cb2004a3f3ad330a24f885c1b.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM report r \n USING program p \n WHERE r.id = $1 \n AND r.program_id = p.id \n AND ($2::text[] IS NULL OR p.business_id = ANY($2))\n RETURNING r.*\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_id", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "event_id", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "client_name", 34 | "type_info": "Text" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "report_name", 39 | "type_info": "Text" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "payload_descriptors", 44 | "type_info": "Jsonb" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "resources", 49 | "type_info": "Jsonb" 50 | } 51 | ], 52 | "parameters": { 53 | "Left": [ 54 | "Text", 55 | "TextArray" 56 | ] 57 | }, 58 | "nullable": [ 59 | false, 60 | false, 61 | false, 62 | false, 63 | false, 64 | false, 65 | true, 66 | true, 67 | false 68 | ] 69 | }, 70 | "hash": "f947a48b3f9c03686fd0e7abd996dd9cda1e9a3cb2004a3f3ad330a24f885c1b" 71 | } 72 | -------------------------------------------------------------------------------- /.sqlx/query-faf3290725899dc80b16c2233995a2b3002ebb0989f4d5284880226579d983b3.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n UPDATE event\n SET modification_date_time = now(),\n program_id = $2,\n event_name = $3,\n priority = $4,\n targets = $5,\n report_descriptors = $6,\n payload_descriptors = $7,\n interval_period = $8,\n intervals = $9\n WHERE id = $1\n RETURNING *\n ", 4 | "describe": { 5 | "columns": [ 6 | { 7 | "ordinal": 0, 8 | "name": "id", 9 | "type_info": "Text" 10 | }, 11 | { 12 | "ordinal": 1, 13 | "name": "created_date_time", 14 | "type_info": "Timestamptz" 15 | }, 16 | { 17 | "ordinal": 2, 18 | "name": "modification_date_time", 19 | "type_info": "Timestamptz" 20 | }, 21 | { 22 | "ordinal": 3, 23 | "name": "program_id", 24 | "type_info": "Text" 25 | }, 26 | { 27 | "ordinal": 4, 28 | "name": "event_name", 29 | "type_info": "Text" 30 | }, 31 | { 32 | "ordinal": 5, 33 | "name": "priority", 34 | "type_info": "Int8" 35 | }, 36 | { 37 | "ordinal": 6, 38 | "name": "report_descriptors", 39 | "type_info": "Jsonb" 40 | }, 41 | { 42 | "ordinal": 7, 43 | "name": "payload_descriptors", 44 | "type_info": "Jsonb" 45 | }, 46 | { 47 | "ordinal": 8, 48 | "name": "interval_period", 49 | "type_info": "Jsonb" 50 | }, 51 | { 52 | "ordinal": 9, 53 | "name": "intervals", 54 | "type_info": "Jsonb" 55 | }, 56 | { 57 | "ordinal": 10, 58 | "name": "targets", 59 | "type_info": "Jsonb" 60 | } 61 | ], 62 | "parameters": { 63 | "Left": [ 64 | "Text", 65 | "Text", 66 | "Text", 67 | "Int8", 68 | "Jsonb", 69 | "Jsonb", 70 | "Jsonb", 71 | "Jsonb", 72 | "Jsonb" 73 | ] 74 | }, 75 | "nullable": [ 76 | false, 77 | false, 78 | false, 79 | false, 80 | true, 81 | true, 82 | true, 83 | true, 84 | true, 85 | false, 86 | true 87 | ] 88 | }, 89 | "hash": "faf3290725899dc80b16c2233995a2b3002ebb0989f4d5284880226579d983b3" 90 | } 91 | -------------------------------------------------------------------------------- /.sqlx/query-feaeaa22a9b48b647e30cad8582e09c45ecec073fd32461071f3687ccd9c7e91.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "\n DELETE FROM \"user\" WHERE id = $1\n ", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Text" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "feaeaa22a9b48b647e30cad8582e09c45ecec073fd32461071f3687ccd9c7e91" 14 | } 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official email address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [team@openleadr.org](mailto:team@openleadr.org). 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | 135 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this 7 | license document, but changing it is not allowed. 8 | 9 | 10 | Developer's Certificate of Origin 1.1 11 | 12 | By making a contribution to this project, I certify that: 13 | 14 | (a) The contribution was created in whole or in part by me and I 15 | have the right to submit it under the open source license 16 | indicated in the file; or 17 | 18 | (b) The contribution is based upon previous work that, to the best 19 | of my knowledge, is covered under an appropriate open source 20 | license and I have the right under that license to submit that 21 | work with modifications, whether created in whole or in part 22 | by me, under the same open source license (unless I am 23 | permitted to submit under a different license), as indicated 24 | in the file; or 25 | 26 | (c) The contribution was provided directly to me by some other 27 | person who certified (a), (b) or (c) and I have not modified 28 | it. 29 | 30 | (d) I understand and agree that this project and the contribution 31 | are public and that a record of the contribution (including all 32 | personal information I submit with it, including my sign-off) is 33 | maintained indefinitely and may be redistributed consistent with 34 | this project or the open source license(s) involved. -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Stichting ElaadNL and Contributors 2 | 3 | Except as otherwise noted (below and/or in individual files), openleadr-rs is 4 | licensed under the Apache License, Version 2.0 or 5 | or the MIT license 6 | or , at your option. 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "openleadr-vtn", 4 | "openleadr-client", 5 | "openleadr-wire" 6 | ] 7 | exclude = [] 8 | 9 | resolver = "2" 10 | 11 | [workspace.package] 12 | version = "0.0.4" 13 | edition = "2021" 14 | rust-version = "1.85" # MSRV 15 | license = "Apache-2.0 OR MIT" 16 | repository = "https://github.com/OpenLEADR/openleadr-rs" 17 | homepage = "https://github.com/OpenLEADR/openleadr-rs" 18 | publish = true 19 | description = "An OpenADR 3.0 VTN/VEN implementation" 20 | keywords = ["energy", "openadr", "lf-energy"] 21 | 22 | [workspace.dependencies] 23 | openleadr-wire = { version = "0.0.4", path = "openleadr-wire" } 24 | openleadr-vtn = { version = "0.0.4", path = "openleadr-vtn" } 25 | openleadr-client = { version = "0.0.4", path = "openleadr-client" } 26 | 27 | serde = { version = "1.0.203", features = ["derive"] } 28 | serde_json = "1.0.117" 29 | serde_with = { version = "3.8.1", features = ["macros"] } 30 | 31 | reqwest = { version = "0.12.4", default-features = false, features = ["http2", "charset", "rustls-tls-native-roots", "json"] } 32 | tokio = { version = "1.37.0", features = ["full", "test-util"] } 33 | tokio-test = "0.4.4" 34 | axum = { version = "0.8.1", features = ["macros"] } 35 | axum-extra = { version = "0.10.0", features = ["query", "typed-header"] } 36 | tower = { version = "0.5", features = ["util"] } 37 | 38 | tracing = "0.1.40" 39 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 40 | tracing-test = "0.2.5" 41 | 42 | chrono = "0.4.38" 43 | iso8601-duration = { version = "0.2.0", features = ["chrono"] } 44 | rangemap = "1.5.1" 45 | 46 | thiserror = "2.0.3" 47 | validator = { version = "0.20.0", features = ["derive"] } 48 | uuid = { version = "1.8.0", features = ["v4"] } 49 | url = "2.5.0" 50 | http = "^1.0.0" 51 | mime = "0.3" 52 | tower-http = { version = "0.6.1", features = ["trace"] } 53 | http-body-util = "0.1.0" 54 | jsonwebtoken = { version = "9.3.0", default-features = false, features = ["use_pem"] } 55 | base64 = "0.22.1" 56 | rand = "0.9.0" 57 | async-trait = "0.1.81" 58 | 59 | quickcheck = "1.0.3" 60 | 61 | sqlx = { version = "0.8.2", features = ["postgres", "runtime-tokio", "chrono", "migrate", "macros", "json"], default-features = false } 62 | argon2 = "0.5.3" 63 | dotenvy = "0.15.7" 64 | 65 | serial_test = "3.1.1" 66 | 67 | iso_currency = { version = "0.5.0", features = ["with-serde"] } 68 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Stichting ElaadNL and Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /OpenADR_alliance_test_suite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenLEADR/openleadr-rs/bee9cf4bcd9041eef84d29c0271d874c5bbeff9e/OpenADR_alliance_test_suite.png -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security policy 2 | **Do not report security vulnerabilities through public GitHub issues.** 3 | 4 | Instead, you can report them using [our security page](https://github.com/OpenLEADR/openleadr-rs/security). 5 | Alternatively, you can also send them by email to security@openleadr.org. 6 | 7 | Include as much of the following information as you can: 8 | 9 | * Type of issue (e.g., buffer overflow, privilege escalation, etc.). 10 | * The location of the affected source code (tag/branch/commit or direct URL). 11 | * Any special configuration required to reproduce the issue. 12 | * Step-by-step instructions to reproduce the issue. 13 | * Impact of the issue, including how an attacker might exploit the issue. 14 | 15 | ## Preferred Languages 16 | We prefer to receive reports in English. If necessary, we also understand German and Dutch. 17 | 18 | ## Disclosure Policy 19 | We adhere to the principle of [Coordinated Vulnerability Disclosure](https://vuls.cert.org/confluence/display/CVD/Executive+Summary). 20 | 21 | # Security Advisories 22 | Security advisories will be published [on GitHub](https://github.com/OpenLEADR/openleadr-rs/security/advisories) and possibly through other channels. -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.85" 2 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | targets = [ 3 | { triple = "x86_64-unknown-linux-musl" }, 4 | { triple = "x86_64-unknown-linux-gnu" }, 5 | { triple = "aarch64-unknown-linux-gnu" }, 6 | ] 7 | 8 | [licenses] 9 | version = 2 10 | private = { ignore = true } 11 | allow = [ 12 | "MIT", 13 | "Apache-2.0", 14 | "Unicode-DFS-2016", 15 | "Unicode-3.0", 16 | "BSD-3-Clause", 17 | "ISC", 18 | "OpenSSL", 19 | "Zlib" 20 | ] 21 | 22 | [[licenses.clarify]] 23 | name = "ring" 24 | expression = "ISC AND MIT AND OpenSSL" 25 | license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] 26 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | vtn: 3 | build: 4 | dockerfile: vtn.Dockerfile 5 | context: . 6 | ports: 7 | - ${VTN_PORT}:3000 8 | environment: 9 | RUST_LOG: debug 10 | DATABASE_URL: postgres://openadr:openadr@db:5432/openadr 11 | healthcheck: 12 | test: curl --fail http://127.0.0.1:3000/health || exit 1 13 | interval: 15s 14 | timeout: 5s 15 | retries: 3 16 | depends_on: 17 | - db 18 | 19 | db: 20 | image: ghcr.io/tweedegolf/postgres:16 21 | environment: 22 | POSTGRES_USER: $PG_USER 23 | POSTGRES_DB: $PG_DB 24 | POSTGRES_PASSWORD: $PG_PASSWORD 25 | POSTGRES_HOST_AUTH_METHOD: trust 26 | TZ: $PG_TZ 27 | healthcheck: 28 | test: [ "CMD-SHELL", "pg_isready -U openadr" ] 29 | interval: 5s 30 | timeout: 5s 31 | retries: 5 32 | ports: 33 | - ${PG_PORT}:5432 34 | volumes: 35 | - database-data:/var/lib/postgresql/data/ 36 | 37 | volumes: 38 | database-data: 39 | -------------------------------------------------------------------------------- /fixtures/business.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO business (id) 2 | VALUES ('business-1'); 3 | 4 | INSERT INTO user_business (user_id, business_id) 5 | VALUES ('user-1', 'business-1'); 6 | 7 | UPDATE program 8 | SET business_id = 'business-1' 9 | WHERE id = 'program-3'; -------------------------------------------------------------------------------- /fixtures/events.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO event (id, created_date_time, modification_date_time, program_id, event_name, priority, targets, 2 | report_descriptors, payload_descriptors, interval_period, intervals) 3 | VALUES ('event-1', 4 | '2024-07-25 08:31:10.776000 +00:00', 5 | '2024-07-25 08:31:10.776000 +00:00', 6 | 'program-1', 7 | 'event-1-name', 8 | '4', 9 | '[ 10 | { 11 | "type": "GROUP", 12 | "values": [ 13 | "group-1" 14 | ] 15 | }, 16 | { 17 | "type": "PRIVATE_LABEL", 18 | "values": [ 19 | "private value" 20 | ] 21 | } 22 | ]'::jsonb, 23 | null, 24 | null, 25 | '{ 26 | "start": "2023-06-15T09:30:00+00:00", 27 | "duration": "P0Y0M0DT1H0M0S", 28 | "randomizeStart": "P0Y0M0DT1H0M0S" 29 | }'::jsonb, 30 | '[ 31 | { 32 | "id": 3, 33 | "payloads": [ 34 | { 35 | "type": "PRICE", 36 | "values": [ 37 | 0.17 38 | ] 39 | } 40 | ], 41 | "intervalPeriod": { 42 | "start": "2023-06-15T09:30:00+00:00", 43 | "duration": "P0Y0M0DT1H0M0S", 44 | "randomizeStart": "P0Y0M0DT1H0M0S" 45 | } 46 | } 47 | ]'::jsonb), 48 | ('event-2', 49 | '2024-07-25 08:31:10.776000 +00:00', 50 | '2024-07-25 08:31:10.776000 +00:00', 51 | 'program-2', 52 | 'event-2-name', 53 | null, 54 | '[ 55 | { 56 | "type": "SOME_TARGET", 57 | "values": [ 58 | "target-1" 59 | ] 60 | } 61 | ]'::jsonb, 62 | null, 63 | null, 64 | null, 65 | '[ 66 | { 67 | "id": 3, 68 | "payloads": [ 69 | { 70 | "type": "SOME_PAYLOAD", 71 | "values": [ 72 | "value" 73 | ] 74 | } 75 | ] 76 | } 77 | ]'::jsonb), 78 | ('event-3', 79 | '2024-07-25 08:31:10.776000 +00:00', 80 | '2024-07-25 08:31:10.776000 +00:00', 81 | 'program-3', 82 | 'event-3-name', 83 | null, 84 | '[ 85 | { 86 | "type": "SOME_TARGET", 87 | "values": [ 88 | "target-1" 89 | ] 90 | } 91 | ]'::jsonb, 92 | null, 93 | null, 94 | null, 95 | '[ 96 | { 97 | "id": 3, 98 | "payloads": [ 99 | { 100 | "type": "SOME_PAYLOAD", 101 | "values": [ 102 | "value" 103 | ] 104 | } 105 | ] 106 | } 107 | ]'::jsonb); -------------------------------------------------------------------------------- /fixtures/openadr_testsuite_user.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO "user" (id, reference, description, created, modified) 2 | VALUES ('bl_user', 3 | 'bl_test_user', 4 | null, 5 | now(), 6 | now()); 7 | 8 | -- secret: 1001 9 | INSERT INTO user_credentials (user_id, client_id, client_secret) 10 | VALUES ('bl_user', 'bl_client', 11 | '$argon2id$v=19$m=16,t=2,p=1$YmJkMTJrU0ptMVprYVJLSQ$mu1Fbbt5PzBsE/dJevKazw'); 12 | 13 | INSERT INTO any_business_user (user_id) VALUES ('bl_user'); 14 | INSERT INTO ven_manager (user_id) VALUES ('bl_user'); 15 | 16 | INSERT INTO "user" (id, reference, description, created, modified) 17 | VALUES ('ven_user', 18 | 'ven_test_user', 19 | null, 20 | now(), 21 | now()); 22 | 23 | -- secret: 999 24 | INSERT INTO user_credentials (user_id, client_id, client_secret) 25 | VALUES ('ven_user', 'ven_client', 26 | '$argon2id$v=19$m=16,t=2,p=1$RGhDTmVkbEl5cEZDY0Fubg$qPtSCpK6Z5XKQkOLHC/+qg'); 27 | 28 | INSERT INTO ven (id, 29 | created_date_time, 30 | modification_date_time, 31 | ven_name, 32 | attributes, 33 | targets) 34 | VALUES ('ven-1', 35 | '2024-07-25 08:31:10.776000 +00:00', 36 | '2024-07-25 08:31:10.776000 +00:00', 37 | 'ven-1-name', 38 | NULL, 39 | NULL); 40 | 41 | INSERT INTO user_ven VALUES ('ven-1', 'ven_user'); 42 | INSERT INTO ven_manager (user_id) VALUES ('ven_user'); 43 | -------------------------------------------------------------------------------- /fixtures/programs.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO program (id, 2 | created_date_time, 3 | modification_date_time, 4 | program_name, 5 | program_long_name, 6 | retailer_name, 7 | retailer_long_name, 8 | program_type, 9 | country, 10 | principal_subdivision, 11 | interval_period, 12 | program_descriptions, 13 | binding_events, 14 | local_price, 15 | payload_descriptors, 16 | targets) 17 | VALUES ('program-1', 18 | '2024-07-25 08:31:10.776000 +00:00', 19 | '2024-07-25 08:31:10.776000 +00:00', 20 | 'program-1', 21 | 'program long name', 22 | 'retailer name', 23 | 'retailer long name', 24 | 'program type', 25 | 'country', 26 | 'principal-subdivision', 27 | '{ 28 | "start": "2024-07-25T08:31:10.776Z" 29 | }', 30 | '[ 31 | { 32 | "URL": "https://program-description-1.com" 33 | } 34 | ]', 35 | false, 36 | true, 37 | '[ 38 | { 39 | "objectType": "EVENT_PAYLOAD_DESCRIPTOR", 40 | "payloadType": "EXPORT_PRICE" 41 | } 42 | ]', 43 | '[ 44 | { 45 | "type": "GROUP", 46 | "values": [ 47 | "group-1" 48 | ] 49 | }, 50 | { 51 | "type": "PRIVATE_LABEL", 52 | "values": [ 53 | "private value" 54 | ] 55 | } 56 | ]'), 57 | ('program-2', 58 | '2024-07-25 08:31:10.776000 +00:00', 59 | '2024-07-25 08:31:10.776000 +00:00', 60 | 'program-2', 61 | NULL, 62 | NULL, 63 | NULL, 64 | NULL, 65 | NULL, 66 | NULL, 67 | NULL, 68 | NULL, 69 | NULL, 70 | NULL, 71 | NULL, 72 | '[ 73 | { 74 | "type": "GROUP", 75 | "values": [ 76 | "group-1", 77 | "group-2" 78 | ] 79 | } 80 | ]'), 81 | ('program-3', 82 | '2024-07-25 08:31:10.776000 +00:00', 83 | '2024-07-25 08:31:10.776000 +00:00', 84 | 'program-3', 85 | NULL, 86 | NULL, 87 | NULL, 88 | NULL, 89 | NULL, 90 | NULL, 91 | NULL, 92 | NULL, 93 | NULL, 94 | NULL, 95 | NULL, 96 | NULL); -------------------------------------------------------------------------------- /fixtures/reports.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO report (id, 2 | created_date_time, 3 | modification_date_time, 4 | program_id, 5 | event_id, 6 | client_name, 7 | report_name, 8 | payload_descriptors, 9 | resources) 10 | VALUES ('report-1', 11 | '2024-07-25 08:31:10.776000 +00:00', 12 | '2024-07-25 08:31:10.776000 +00:00', 13 | 'program-1', 14 | 'event-1', 15 | 'some-client-maybe-vtn-name', 16 | NULL, 17 | NULL, 18 | '[]'), 19 | ('report-2', 20 | '2024-07-25 08:31:10.776000 +00:00', 21 | '2024-07-25 08:31:10.776000 +00:00', 22 | 'program-2', 23 | 'event-2', 24 | 'some-client-maybe-vtn-name', 25 | NULL, 26 | NULL, 27 | '[]') -------------------------------------------------------------------------------- /fixtures/resources.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO resource (id, 2 | created_date_time, 3 | modification_date_time, 4 | resource_name, 5 | ven_id, 6 | attributes, 7 | targets) 8 | VALUES ('resource-1', 9 | '2024-07-25 08:31:10.776000 +00:00', 10 | '2024-07-25 08:31:10.776000 +00:00', 11 | 'resource-1-name', 12 | 'ven-1', 13 | NULL, 14 | '[ 15 | { 16 | "type": "GROUP", 17 | "values": [ 18 | "group-1" 19 | ] 20 | } 21 | ]'), 22 | ('resource-2', 23 | '2024-07-25 08:31:10.776000 +00:00', 24 | '2024-07-25 08:31:10.776000 +00:00', 25 | 'resource-2-name', 26 | 'ven-2', 27 | NULL, 28 | '[ 29 | { 30 | "type": "GROUP", 31 | "values": [ 32 | "group-1" 33 | ] 34 | } 35 | ]'), 36 | ('resource-3', 37 | '2024-07-25 08:31:10.776000 +00:00', 38 | '2024-07-25 08:31:10.776000 +00:00', 39 | 'resource-3-name', 40 | 'ven-1', 41 | NULL, 42 | '[ 43 | { 44 | "type": "GROUP", 45 | "values": [ 46 | "group-2" 47 | ] 48 | } 49 | ]'), 50 | ('resource-4', 51 | '2024-07-25 08:31:10.776000 +00:00', 52 | '2024-07-25 08:31:10.776000 +00:00', 53 | 'resource-4-name', 54 | 'ven-2', 55 | NULL, 56 | NULL), 57 | ('resource-5', 58 | '2024-07-25 08:31:10.776000 +00:00', 59 | '2024-07-25 08:31:10.776000 +00:00', 60 | 'resource-5-name', 61 | 'ven-2', 62 | NULL, 63 | NULL); 64 | -------------------------------------------------------------------------------- /fixtures/test_user_credentials.sql: -------------------------------------------------------------------------------- 1 | -- create users 2 | INSERT INTO "user" (id, reference, description, created, modified) 3 | VALUES ('ven-manager', 4 | 'ven-manager', 5 | 'for automated test cases', 6 | now(), 7 | now()); 8 | 9 | INSERT INTO "user" (id, reference, description, created, modified) 10 | VALUES ('user-manager', 11 | 'user-manager', 12 | 'for automated test cases', 13 | now(), 14 | now()); 15 | 16 | INSERT INTO "user" (id, reference, description, created, modified) 17 | VALUES ('any-business', 18 | 'any-business', 19 | 'for automated test cases', 20 | now(), 21 | now()); 22 | 23 | INSERT INTO "user" (id, reference, description, created, modified) 24 | VALUES ('business-1-user', 25 | 'business-1-user', 26 | 'for automated test cases', 27 | now(), 28 | now()); 29 | 30 | INSERT INTO "user" (id, reference, description, created, modified) 31 | VALUES ('ven-1-user', 32 | 'ven-1-user', 33 | 'for automated test cases', 34 | now(), 35 | now()); 36 | 37 | -- associate roles to users 38 | INSERT INTO ven_manager (user_id) 39 | VALUES ('ven-manager'); 40 | INSERT INTO user_manager (user_id) 41 | VALUES ('user-manager'); 42 | INSERT INTO any_business_user (user_id) 43 | VALUES ('any-business'); 44 | 45 | INSERT INTO business (id) 46 | VALUES ('business-1'); 47 | INSERT INTO user_business (user_id, business_id) 48 | VALUES ('business-1-user', 'business-1'); 49 | 50 | INSERT INTO ven (id, created_date_time, modification_date_time, ven_name, attributes, targets) 51 | VALUES ('ven-1', 52 | now(), 53 | now(), 54 | 'ven-1-name', 55 | null, 56 | null); 57 | 58 | INSERT INTO user_ven (ven_id, user_id) 59 | VALUES ('ven-1', 'ven-1-user'); 60 | 61 | -- create login credentials 62 | INSERT INTO user_credentials (user_id, client_id, client_secret) 63 | VALUES ('ven-manager', 64 | 'ven-manager', 65 | --ven-manager 66 | '$argon2id$v=19$m=16,t=2,p=1$NGxoR0w0MG1oQVhTNTlWYw$NaIKUON6vrNcM2jXzqwX5Q'); 67 | 68 | INSERT INTO user_credentials (user_id, client_id, client_secret) 69 | VALUES ('user-manager', 70 | 'user-manager', 71 | --user-manager 72 | '$argon2id$v=19$m=16,t=2,p=1$eDRuSHFPUG16M09JNGo5WQ$LjaFZJVr2Qpna45k51yuhw'); 73 | 74 | INSERT INTO user_credentials (user_id, client_id, client_secret) 75 | VALUES ('any-business', 76 | 'any-business', 77 | --any-business 78 | '$argon2id$v=19$m=16,t=2,p=1$WWJENTVTbEpYZkFjdlhOUQ$UXEQI8OPlnbtXwStitu6Vw'); 79 | 80 | INSERT INTO user_credentials (user_id, client_id, client_secret) 81 | VALUES ('business-1-user', 82 | 'business-1', 83 | --business-1 84 | '$argon2id$v=19$m=16,t=2,p=1$bVlaZTI4QndaaERNa1Q5bg$0IjdRSjo601S9vHICOLRdQ'); 85 | 86 | INSERT INTO user_credentials (user_id, client_id, client_secret) 87 | VALUES ('ven-1-user', 88 | 'ven-1', 89 | --ven-1 90 | '$argon2id$v=19$m=16,t=2,p=1$WGpkRFZGampQM0N3S0ZZVw$lO3AhXZYKu/Wsk3Z1NE/Aw'); -------------------------------------------------------------------------------- /fixtures/users.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO "user" (id, reference, description, created, modified) 2 | VALUES ('admin', 'admin-ref', null, '2024-07-25 08:31:10.776000 +00:00', '2024-07-25 08:31:10.776000 +00:00'); 3 | 4 | INSERT INTO any_business_user (user_id) 5 | VALUES ('admin'); 6 | INSERT INTO user_manager (user_id) 7 | VALUES ('admin'); 8 | INSERT INTO ven_manager (user_id) 9 | VALUES ('admin'); 10 | 11 | INSERT INTO user_credentials (user_id, client_id, client_secret) 12 | VALUES ('admin', 'admin', '$argon2id$v=19$m=16,t=2,p=1$QmtwZnBPVnlIYkJTWUtHZg$lMxF0N+CeRa99UmzMaUKeg'); -- secret: admin 13 | 14 | INSERT INTO "user" (id, reference, description, created, modified) 15 | VALUES ('user-1', 'user-1-ref', 'desc', '2024-07-25 08:31:10.776000 +00:00', '2024-07-25 08:31:10.776000 +00:00'); 16 | 17 | INSERT INTO user_credentials (user_id, client_id, client_secret) 18 | VALUES ('user-1', 'user-1-client-id', 19 | '$argon2id$v=19$m=16,t=2,p=1$R04zbWxDNVhtVHB4aVJLag$mRpShTDhgZ9+bVNLa8GBgw'); -- secret: user-1 20 | 21 | -------------------------------------------------------------------------------- /fixtures/vens-programs.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO ven_program (program_id, ven_id) 2 | VALUES ('program-1', 'ven-1'); 3 | 4 | INSERT INTO ven_program (program_id, ven_id) 5 | VALUES ('program-1', 'ven-2'); 6 | 7 | INSERT INTO ven_program (program_id, ven_id) 8 | VALUES ('program-2', 'ven-2'); 9 | 10 | INSERT INTO ven_program (program_id, ven_id) 11 | VALUES ('program-3', 'ven-1'); -------------------------------------------------------------------------------- /fixtures/vens.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO ven (id, 2 | created_date_time, 3 | modification_date_time, 4 | ven_name, 5 | attributes, 6 | targets) 7 | VALUES ('ven-1', 8 | '2024-07-25 08:31:10.776000 +00:00', 9 | '2024-07-25 08:31:10.776000 +00:00', 10 | 'ven-1-name', 11 | NULL, 12 | '[ 13 | { 14 | "type": "GROUP", 15 | "values": [ 16 | "group-1" 17 | ] 18 | }, 19 | { 20 | "type": "PRIVATE_LABEL", 21 | "values": [ 22 | "private value" 23 | ] 24 | } 25 | ]'), 26 | ('ven-2', 27 | '2024-07-25 08:31:10.776000 +00:00', 28 | '2024-07-25 08:31:10.776000 +00:00', 29 | 'ven-2-name', 30 | NULL, 31 | NULL); 32 | 33 | INSERT INTO user_ven (ven_id, user_id) 34 | VALUES ('ven-1', 'user-1'); 35 | -------------------------------------------------------------------------------- /migrations/20240826084440_initial_scheme.sql: -------------------------------------------------------------------------------- 1 | create table business 2 | ( 3 | id text not null 4 | constraint business_pk primary key 5 | ); 6 | 7 | create table program 8 | ( 9 | id text not null 10 | constraint program_pk 11 | primary key, 12 | created_date_time timestamptz not null, 13 | modification_date_time timestamptz not null, 14 | 15 | program_name text not null, 16 | program_long_name text, 17 | retailer_name text, 18 | retailer_long_name text, 19 | program_type text, 20 | country text, 21 | principal_subdivision text, 22 | -- deliberately omitted: time_zone_offset 23 | interval_period jsonb, 24 | program_descriptions jsonb, 25 | binding_events boolean, 26 | local_price boolean, 27 | payload_descriptors jsonb, 28 | targets jsonb, 29 | business_id text references business (id) 30 | ); 31 | 32 | create unique index program_program_name_uindex 33 | on program (program_name); 34 | 35 | create table event 36 | ( 37 | id text not null 38 | constraint event_pk 39 | primary key, 40 | created_date_time timestamptz not null, 41 | modification_date_time timestamptz not null, 42 | 43 | program_id text not null references program (id), 44 | event_name text, 45 | priority bigint, 46 | report_descriptors jsonb, 47 | payload_descriptors jsonb, 48 | interval_period jsonb, 49 | intervals jsonb not null, 50 | targets jsonb 51 | ); 52 | 53 | create index event_event_name_index 54 | on event (event_name); 55 | 56 | 57 | create table report 58 | ( 59 | id text not null 60 | constraint report_pk 61 | primary key, 62 | created_date_time timestamptz not null, 63 | modification_date_time timestamptz not null, 64 | 65 | program_id text not null references program (id), 66 | event_id text not null references event (id), 67 | client_name text not null, 68 | report_name text, 69 | payload_descriptors jsonb, 70 | resources jsonb not null 71 | ); 72 | 73 | create unique index report_report_name_uindex 74 | on report (report_name); 75 | 76 | create table "user" 77 | ( 78 | id text primary key, 79 | reference text not null, 80 | description text, 81 | created timestamptz not null, 82 | modified timestamptz not null 83 | ); 84 | 85 | create table user_credentials 86 | ( 87 | user_id text not null references "user" (id) on delete cascade, 88 | client_id text primary key, 89 | client_secret text not null 90 | -- TODO maybe the credentials require their own role? 91 | ); 92 | 93 | create table ven 94 | ( 95 | id text not null 96 | constraint ven_pk 97 | primary key, 98 | created_date_time timestamptz not null, 99 | modification_date_time timestamptz not null, 100 | ven_name text not null, 101 | attributes jsonb, 102 | targets jsonb 103 | ); 104 | 105 | create unique index ven_ven_name_uindex 106 | on ven (ven_name); 107 | 108 | create table user_ven 109 | ( 110 | ven_id text not null references ven (id) on delete cascade, 111 | user_id text not null references "user" (id) on delete cascade 112 | ); 113 | 114 | create table resource 115 | ( 116 | id text not null 117 | constraint resource_pk 118 | primary key, 119 | created_date_time timestamptz not null, 120 | modification_date_time timestamptz not null, 121 | resource_name text not null, 122 | ven_id text not null references ven (id), -- TODO is this actually 'NOT NULL'? 123 | attributes jsonb, 124 | targets jsonb 125 | 126 | ); 127 | 128 | create unique index resource_ven_id_resource_name_uindex 129 | on resource (ven_id, resource_name); 130 | 131 | 132 | create table ven_program 133 | ( 134 | program_id text not null references program (id) on delete cascade, 135 | ven_id text not null references ven (id) on delete cascade, 136 | constraint ven_program_pk primary key (program_id, ven_id) 137 | ); 138 | 139 | 140 | create table user_business 141 | ( 142 | user_id text not null references "user" (id) on delete cascade, 143 | business_id text not null references business (id) on delete cascade 144 | ); 145 | 146 | create unique index uindex_user_business 147 | on user_business (user_id, business_id); 148 | 149 | create table ven_manager 150 | ( 151 | user_id text primary key references "user" (id) on delete cascade 152 | ); 153 | 154 | create table user_manager 155 | ( 156 | user_id text primary key references "user" (id) on delete cascade 157 | ); 158 | 159 | create table any_business_user 160 | ( 161 | user_id text primary key references "user" (id) on delete cascade 162 | ); -------------------------------------------------------------------------------- /openleadr-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openleadr-client" 3 | description = "OpenADR 3.0 client" 4 | readme = "README.md" 5 | version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | homepage.workspace = true 10 | publish.workspace = true 11 | rust-version.workspace = true 12 | keywords.workspace = true 13 | 14 | [dependencies] 15 | openleadr-wire.workspace = true 16 | 17 | serde.workspace = true 18 | serde_json.workspace = true 19 | 20 | reqwest.workspace = true 21 | axum.workspace = true 22 | tokio = { workspace = true, features = ["full"] } 23 | tracing.workspace = true 24 | http-body-util.workspace = true 25 | tower.workspace = true 26 | 27 | url.workspace = true 28 | chrono.workspace = true 29 | rangemap.workspace = true 30 | uuid.workspace = true 31 | async-trait.workspace = true 32 | 33 | [dev-dependencies] 34 | tokio = { workspace = true, features = ["full", "test-util"] } 35 | openleadr-vtn.workspace = true 36 | tokio-test.workspace = true 37 | mime.workspace = true 38 | sqlx.workspace = true 39 | serial_test.workspace = true 40 | dotenvy.workspace = true 41 | 42 | [package.metadata.cargo-udeps.ignore] 43 | # tokio-test is only used in the doc-tests and can therefore not be detected by cargo-udeps 44 | development = ["tokio-test"] -------------------------------------------------------------------------------- /openleadr-client/README.md: -------------------------------------------------------------------------------- 1 | ![maintenance-status](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg) 2 | ![codecov](https://codecov.io/gh/OpenLEADR/openleadr-rs/graph/badge.svg?token=BKQ0QW9G8H) 3 | ![Checks](https://github.com/OpenLEADR/openleadr-rs/actions/workflows/checks.yml/badge.svg?branch=main) 4 | ![Crates.io Version](https://img.shields.io/crates/v/openleadr-client) 5 | 6 | # OpenADR 3.0 VEN client library in Rust 7 | 8 | ![LF energy OpenLEADR logo](../openleadr-logo.svg) 9 | 10 | This is a client library to interact with an OpenADR 3.0 complaint VTN server. 11 | It mainly wraps the HTTP REST interface into an easy-to-use Rust API. 12 | 13 | The following contains information specific to the client library. 14 | If you are interested in information about the whole project, please visit the [project level Readme](../README.md). 15 | 16 | ### Basic usage 17 | For a basic example of how to use the client library, see the following. 18 | You can find detailed documentation of the library at [docs.rs](https://docs.rs/openleadr-client/latest/openleadr_client/). 19 | ```rust 20 | async fn main() { 21 | let credentials = ClientCredentials::new("client_id".to_string(), "client_secret".to_string()); 22 | let client = Client::with_url("https://your-vtn.com".try_into().unwrap(), Some(credentials)); 23 | 24 | let new_program = ProgramContent::new("example-program-name".to_string()); 25 | let example_program = client.create_program(new_program).await.unwrap(); 26 | 27 | let mut new_event = example_program.new_event(); 28 | new_event.priority = Priority::new(10); 29 | new_event.event_name = Some("Some descriptive name".to_string()); 30 | example_program.create_event(new_event).await.unwrap(); 31 | } 32 | ``` 33 | 34 | We plan to create a CLI binary using this library as well. 35 | See [#52](https://github.com/OpenLEADR/openleadr-rs/issues/52) for the current progress. -------------------------------------------------------------------------------- /openleadr-client/migrations: -------------------------------------------------------------------------------- 1 | ../migrations/ -------------------------------------------------------------------------------- /openleadr-client/src/bin/cli.rs: -------------------------------------------------------------------------------- 1 | use openleadr_client::ClientCredentials; 2 | use openleadr_wire::program::ProgramContent; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<(), Box> { 6 | let reqwest_client = reqwest::Client::new(); 7 | let client = openleadr_client::Client::with_details( 8 | "http://localhost:3000/".try_into()?, 9 | "http://localhost:8080/token".try_into()?, 10 | reqwest_client, 11 | Some(ClientCredentials::new( 12 | "admin".to_string(), 13 | "admin".to_string(), 14 | )), 15 | ); 16 | 17 | let _created_program = client.create_program(ProgramContent::new("name")).await?; 18 | // let created_program_1 = client.create_program(ProgramContent::new("name1")).await?; 19 | // let program = client.get_program_by_name("name").await?; 20 | // let created_event = program 21 | // .create_event(program.new_event().with_event_name("prices3").with_priority(0)) 22 | // .await?; 23 | // let events = program.get_event_list(Filter::none()).await?; 24 | // let reports = events[0].get_all_reports().await?; 25 | // let event = program.get_event(Target::Event("prices3")).await?; 26 | // dbg!(events); 27 | // dbg!(reports); 28 | 29 | // let programs: Vec = client.get_all_programs()?; 30 | // let programs = client.get_programs(TargetLabel::ProgramName, &["name"])?; 31 | 32 | // let program = client.get_program_by_id("id").await?; 33 | 34 | // let evt = program.send_event(Event { 35 | 36 | // })?; 37 | 38 | // let events = program.get_events(TargetLabel::EventName, &["name"], 0, 10)?; 39 | 40 | // program.get_event_by_name("prices").await?; 41 | 42 | // let events = program.get_all_events().await?; 43 | 44 | // // find the event you want, each event contains all relevant information to reconstruct periods 45 | // let event = events[0]; 46 | 47 | // for interval in event.intervals { 48 | // for payload in interval.payloads { // Iterator(); 54 | // } 55 | // } 56 | 57 | // send a report 58 | // event.send_report(Report { 59 | 60 | // }).await?; 61 | 62 | // program.on_event(|evt| { 63 | 64 | // })?; 65 | 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /openleadr-client/src/error.rs: -------------------------------------------------------------------------------- 1 | use reqwest::StatusCode; 2 | 3 | /// Errors that can occur using the [`Client`](crate::Client) 4 | #[derive(Debug)] 5 | #[allow(missing_docs)] 6 | pub enum Error { 7 | Reqwest(reqwest::Error), 8 | Serde(serde_json::Error), 9 | UrlParseError(url::ParseError), 10 | Problem(openleadr_wire::problem::Problem), 11 | AuthProblem(openleadr_wire::oauth::OAuthError), 12 | OAuthTokenNotBearer, 13 | ObjectNotFound, 14 | DuplicateObject, 15 | /// Error if you try 16 | /// to create an event underneath a program 17 | /// where the [`program_id`](crate::EventContent::program_id) 18 | /// in the [`EventContent`](crate::EventContent) 19 | /// does not match the program ID of the [`ProgramClient`](crate::ProgramClient), 20 | /// for example. 21 | InvalidParentObject, 22 | InvalidInterval, 23 | } 24 | 25 | impl Error { 26 | /// Checks if the [`Problem`](openleadr_wire::problem::Problem) response of the VTN is a 27 | /// `409 Conflict` HTTP status code. 28 | pub fn is_conflict(&self) -> bool { 29 | match self { 30 | Error::Problem(openleadr_wire::problem::Problem { status, .. }) => { 31 | *status == StatusCode::CONFLICT 32 | } 33 | _ => false, 34 | } 35 | } 36 | 37 | #[allow(missing_docs)] 38 | pub fn is_not_found(&self) -> bool { 39 | match self { 40 | Error::Problem(openleadr_wire::problem::Problem { status, .. }) => { 41 | *status == StatusCode::NOT_FOUND 42 | } 43 | _ => false, 44 | } 45 | } 46 | } 47 | 48 | impl From for Error { 49 | fn from(err: reqwest::Error) -> Self { 50 | Error::Reqwest(err) 51 | } 52 | } 53 | 54 | impl From for Error { 55 | fn from(err: serde_json::Error) -> Self { 56 | Error::Serde(err) 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(err: url::ParseError) -> Self { 62 | Error::UrlParseError(err) 63 | } 64 | } 65 | 66 | impl From for Error { 67 | fn from(err: openleadr_wire::problem::Problem) -> Self { 68 | Error::Problem(err) 69 | } 70 | } 71 | 72 | impl From for Error { 73 | fn from(err: openleadr_wire::oauth::OAuthError) -> Self { 74 | Error::AuthProblem(err) 75 | } 76 | } 77 | 78 | impl std::fmt::Display for Error { 79 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 80 | match self { 81 | Error::Reqwest(err) => write!(f, "Reqwest error: {err}"), 82 | Error::Serde(err) => write!(f, "Serde error: {err}"), 83 | Error::UrlParseError(err) => write!(f, "URL parse error: {err}"), 84 | Error::Problem(err) => write!(f, "OpenADR Problem: {err:?}"), 85 | Error::AuthProblem(err) => write!(f, "Authentication problem: {err:?}"), 86 | Error::ObjectNotFound => write!(f, "Object not found"), 87 | Error::DuplicateObject => write!(f, "Found more than one object matching the filter"), 88 | Error::InvalidParentObject => write!(f, "Invalid parent object"), 89 | Error::InvalidInterval => write!(f, "Invalid interval specified"), 90 | Error::OAuthTokenNotBearer => write!(f, "OAuth token received is not a Bearer token"), 91 | } 92 | } 93 | } 94 | 95 | impl std::error::Error for Error {} 96 | 97 | pub(crate) type Result = std::result::Result; 98 | -------------------------------------------------------------------------------- /openleadr-client/src/event.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::{ 4 | error::{Error, Result}, 5 | ClientRef, ReportClient, 6 | }; 7 | use openleadr_wire::{event::EventContent, report::ReportContent, Event, Report}; 8 | 9 | /// Client to manage the data of a specific event and the reports contained in that event 10 | /// 11 | /// Can be created by a [`ProgramClient`](crate::ProgramClient) 12 | /// ```no_run 13 | /// # use openleadr_client::{Client, Filter}; 14 | /// # use openleadr_wire::event::Priority; 15 | /// let client = Client::with_url("https://your-vtn.com".try_into().unwrap(), None); 16 | /// # tokio_test::block_on(async { 17 | /// let program = client.get_program_by_id(&"program-1".parse().unwrap()).await.unwrap(); 18 | /// 19 | /// // retrieve all events in that specific program 20 | /// let mut events = program.get_event_list(Filter::none()).await.unwrap(); 21 | /// let mut event = events.remove(0); 22 | /// 23 | /// // Set event priority to maximum 24 | /// event.content_mut().priority = Priority::MAX; 25 | /// event.update().await.unwrap() 26 | /// # }) 27 | /// ``` 28 | #[derive(Debug, Clone)] 29 | pub struct EventClient { 30 | client: Arc, 31 | data: Event, 32 | } 33 | 34 | impl EventClient { 35 | pub(super) fn from_event(client: Arc, event: Event) -> Self { 36 | Self { 37 | client, 38 | data: event, 39 | } 40 | } 41 | 42 | /// Get the id of the event 43 | pub fn id(&self) -> &openleadr_wire::event::EventId { 44 | &self.data.id 45 | } 46 | 47 | /// Get the time the event was created on the VTN 48 | pub fn created_date_time(&self) -> chrono::DateTime { 49 | self.data.created_date_time 50 | } 51 | 52 | /// Get the time the event was last modified on the VTN 53 | pub fn modification_date_time(&self) -> chrono::DateTime { 54 | self.data.modification_date_time 55 | } 56 | 57 | /// Read the data of the event 58 | pub fn content(&self) -> &EventContent { 59 | &self.data.content 60 | } 61 | 62 | /// Modify the data of the event. 63 | /// Make sure to call [`update`](Self::update) 64 | /// after your modifications to store them on the VTN 65 | pub fn content_mut(&mut self) -> &mut EventContent { 66 | &mut self.data.content 67 | } 68 | 69 | /// Stores any modifications made to the event content at the server 70 | /// and refreshes the locally stored data with the returned VTN data 71 | pub async fn update(&mut self) -> Result<()> { 72 | self.data = self 73 | .client 74 | .put(&format!("events/{}", self.id()), &self.data.content) 75 | .await?; 76 | Ok(()) 77 | } 78 | 79 | /// Delete the event from the VTN 80 | pub async fn delete(self) -> Result { 81 | self.client.delete(&format!("events/{}", self.id())).await 82 | } 83 | 84 | /// Create a new report object 85 | pub fn new_report(&self, client_name: String) -> ReportContent { 86 | ReportContent { 87 | program_id: self.content().program_id.clone(), 88 | event_id: self.id().clone(), 89 | client_name, 90 | report_name: None, 91 | payload_descriptors: None, 92 | resources: vec![], 93 | } 94 | } 95 | 96 | /// Create a new report on the VTN. 97 | /// The content should be created with [`EventClient::new_report`] 98 | /// to automatically insert the correct program ID and event ID 99 | pub async fn create_report(&self, report_data: ReportContent) -> Result { 100 | if report_data.program_id != self.content().program_id { 101 | return Err(Error::InvalidParentObject); 102 | } 103 | 104 | if &report_data.event_id != self.id() { 105 | return Err(Error::InvalidParentObject); 106 | } 107 | 108 | let report = self.client.post("events", &report_data).await?; 109 | Ok(ReportClient::from_report(self.client.clone(), report)) 110 | } 111 | 112 | async fn get_reports_req( 113 | &self, 114 | client_name: Option<&str>, 115 | skip: usize, 116 | limit: usize, 117 | ) -> Result> { 118 | let skip_str = skip.to_string(); 119 | let limit_str = limit.to_string(); 120 | 121 | let mut query = vec![ 122 | ("programID", self.content().program_id.as_str()), 123 | ("eventID", self.id().as_str()), 124 | ("skip", &skip_str), 125 | ("limit", &limit_str), 126 | ]; 127 | 128 | if let Some(client_name) = client_name { 129 | query.push(("clientName", client_name)); 130 | } 131 | 132 | let reports: Vec = self.client.get("reports", &query).await?; 133 | Ok(reports 134 | .into_iter() 135 | .map(|report| ReportClient::from_report(self.client.clone(), report)) 136 | .collect()) 137 | } 138 | 139 | /// Get all reports from the VTN, possibly filtered by `client_name`, trying to paginate whenever possible 140 | pub async fn get_report_list(&self, client_name: Option<&str>) -> Result> { 141 | self.client 142 | .iterate_pages(|skip, limit| self.get_reports_req(client_name, skip, limit)) 143 | .await 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /openleadr-client/src/program.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::{Error, Result}, 3 | Client, EventClient, EventContent, Filter, PaginationOptions, ProgramContent, ProgramId, 4 | Timeline, 5 | }; 6 | use openleadr_wire::{ 7 | event::{EventInterval, Priority}, 8 | Program, 9 | }; 10 | 11 | /// A client for interacting with the data in a specific program and the events 12 | /// contained in the program. 13 | #[derive(Debug, Clone)] 14 | pub struct ProgramClient { 15 | client: Client, 16 | data: Program, 17 | } 18 | 19 | impl ProgramClient { 20 | pub(super) fn from_program(client: Client, program: Program) -> Self { 21 | Self { 22 | client, 23 | data: program, 24 | } 25 | } 26 | 27 | /// Get the id of the program 28 | pub fn id(&self) -> &ProgramId { 29 | &self.data.id 30 | } 31 | 32 | /// Get the time the program was created on the VTN 33 | pub fn created_date_time(&self) -> chrono::DateTime { 34 | self.data.created_date_time 35 | } 36 | 37 | /// Get the time the program was last modified on the VTN 38 | pub fn modification_date_time(&self) -> chrono::DateTime { 39 | self.data.modification_date_time 40 | } 41 | 42 | /// Read the data of the program 43 | pub fn content(&self) -> &ProgramContent { 44 | &self.data.content 45 | } 46 | 47 | /// Modify the data of the program. 48 | /// Make sure to call [`update`](Self::update) 49 | /// after your modifications to store them on the VTN 50 | pub fn content_mut(&mut self) -> &mut ProgramContent { 51 | &mut self.data.content 52 | } 53 | 54 | /// Stores any modifications made to the program content at the server 55 | /// and refreshes the locally stored data with the returned VTN data 56 | pub async fn update(&mut self) -> Result<()> { 57 | self.data = self 58 | .client 59 | .client_ref 60 | .put(&format!("programs/{}", self.id()), &self.data.content) 61 | .await?; 62 | Ok(()) 63 | } 64 | 65 | /// Delete the program from the VTN 66 | pub async fn delete(self) -> Result { 67 | self.client 68 | .client_ref 69 | .delete(&format!("programs/{}", self.id())) 70 | .await 71 | } 72 | 73 | /// Create a new event on the VTN. 74 | /// The content should be created with [`ProgramClient::new_event`] 75 | /// to automatically insert the correct program ID 76 | pub async fn create_event(&self, event_data: EventContent) -> Result { 77 | if &event_data.program_id != self.id() { 78 | return Err(Error::InvalidParentObject); 79 | } 80 | let event = self.client.client_ref.post("events", &event_data).await?; 81 | Ok(EventClient::from_event( 82 | self.client.client_ref.clone(), 83 | event, 84 | )) 85 | } 86 | 87 | /// Create a new event object within the program 88 | pub fn new_event(&self, intervals: Vec) -> EventContent { 89 | EventContent { 90 | program_id: self.id().clone(), 91 | event_name: None, 92 | priority: Priority::UNSPECIFIED, 93 | targets: None, 94 | report_descriptors: None, 95 | payload_descriptors: None, 96 | interval_period: None, 97 | intervals, 98 | } 99 | } 100 | 101 | /// Low-level operation that gets a list of events for this program from the VTN 102 | /// with the given query parameters. 103 | /// 104 | /// To automatically iterate pages, use [`self.get_event_list`] 105 | pub async fn get_events_request( 106 | &self, 107 | filter: Filter<'_, impl AsRef>, 108 | pagination: PaginationOptions, 109 | ) -> Result> { 110 | self.client 111 | .get_events(Some(self.id()), filter, pagination) 112 | .await 113 | } 114 | 115 | /// Get a list of events from the VTN with the given query parameters 116 | pub async fn get_event_list( 117 | &self, 118 | filter: Filter<'_, impl AsRef + Clone>, 119 | ) -> Result> { 120 | self.client.get_event_list(Some(self.id()), filter).await 121 | } 122 | 123 | /// Retrieves the events for this program from the VTN and tries to build a [`Timeline`] from it. 124 | pub async fn get_timeline( 125 | &self, 126 | filter: Filter<'_, impl AsRef + Clone>, 127 | ) -> Result { 128 | let events = self.get_event_list(filter).await?; 129 | let events = events.iter().map(|e| e.content()).collect(); 130 | Timeline::from_events(&self.data, events).ok_or(Error::InvalidInterval) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /openleadr-client/src/report.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use openleadr_wire::{report::ReportContent, Report}; 4 | 5 | use crate::{error::Result, ClientRef}; 6 | 7 | /// Client to manage the data of a specific report 8 | /// 9 | /// Can be created by a [`EventClient`](crate::EventClient) 10 | /// ```no_run 11 | /// # use openleadr_client::{Client, Filter}; 12 | /// # use openleadr_wire::event::Priority; 13 | /// let client = Client::with_url("https://your-vtn.com".try_into().unwrap(), None); 14 | /// # tokio_test::block_on(async { 15 | /// let event = client.get_event_by_id(&"event-1".parse().unwrap()).await.unwrap(); 16 | /// 17 | /// // retrieve all reports in that specific event, optionally filtered by the client name 18 | /// let mut reports = event.get_report_list(Some("client-name")).await.unwrap(); 19 | /// let mut report = reports.remove(0); 20 | /// 21 | /// // change report name 22 | /// report.content_mut().report_name = Some("new-report-name".to_string()); 23 | /// report.update().await.unwrap() 24 | /// # }) 25 | /// ``` 26 | #[derive(Debug, Clone)] 27 | pub struct ReportClient { 28 | client: Arc, 29 | data: Report, 30 | } 31 | 32 | impl ReportClient { 33 | pub(super) fn from_report(client: Arc, report: Report) -> Self { 34 | Self { 35 | client, 36 | data: report, 37 | } 38 | } 39 | 40 | /// Get the id of the report 41 | pub fn id(&self) -> &openleadr_wire::report::ReportId { 42 | &self.data.id 43 | } 44 | 45 | /// Get the time the report was created on the VTN 46 | pub fn created_date_time(&self) -> &chrono::DateTime { 47 | &self.data.created_date_time 48 | } 49 | 50 | /// Get the time the report was last modified on the VTN 51 | pub fn modification_date_time(&self) -> &chrono::DateTime { 52 | &self.data.modification_date_time 53 | } 54 | 55 | /// Read the data of the report 56 | pub fn content(&self) -> &ReportContent { 57 | &self.data.content 58 | } 59 | 60 | /// Modify the data of the report. 61 | /// Make sure to call [`update`](Self::update) 62 | /// after your modifications to store them on the VTN 63 | pub fn content_mut(&mut self) -> &mut ReportContent { 64 | &mut self.data.content 65 | } 66 | 67 | /// Stores any modifications made to the report content at the server 68 | /// and refreshes the locally stored data with the returned VTN data 69 | pub async fn update(&mut self) -> Result<()> { 70 | let res = self 71 | .client 72 | .put(&format!("reports/{}", self.id()), &self.data.content) 73 | .await?; 74 | self.data = res; 75 | Ok(()) 76 | } 77 | 78 | /// Delete the report from the VTN 79 | pub async fn delete(self) -> Result<()> { 80 | self.client.delete(&format!("reports/{}", self.id())).await 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /openleadr-client/src/resource.rs: -------------------------------------------------------------------------------- 1 | use crate::{ClientRef, Result}; 2 | use chrono::{DateTime, Utc}; 3 | use openleadr_wire::{ 4 | resource::{Resource, ResourceContent, ResourceId}, 5 | ven::VenId, 6 | }; 7 | use std::sync::Arc; 8 | 9 | /// A client 10 | /// for interacting with the data in a specific resource 11 | /// stored as a child element of a VEN on the VTN. 12 | /// 13 | /// To retrieve or create a resource, refer to the [`VenClient`](crate::VenClient). 14 | #[derive(Debug, Clone)] 15 | pub struct ResourceClient { 16 | client: Arc, 17 | ven_id: VenId, 18 | data: Resource, 19 | } 20 | 21 | impl ResourceClient { 22 | pub(super) fn from_resource(client: Arc, ven_id: VenId, resource: Resource) -> Self { 23 | Self { 24 | client, 25 | ven_id, 26 | data: resource, 27 | } 28 | } 29 | 30 | /// Get the resource ID 31 | pub fn id(&self) -> &ResourceId { 32 | &self.data.id 33 | } 34 | 35 | /// Get the time the resource was created on the VTN 36 | pub fn created_date_time(&self) -> DateTime { 37 | self.data.created_date_time 38 | } 39 | 40 | /// Get the time the resource was last updated on the VTN 41 | pub fn modification_date_time(&self) -> DateTime { 42 | self.data.modification_date_time 43 | } 44 | 45 | /// Read the content of the resource 46 | pub fn content(&self) -> &ResourceContent { 47 | &self.data.content 48 | } 49 | 50 | /// Modify the data of the resource. 51 | /// Make sure to call [`update`](Self::update) 52 | /// after your modifications to store them on the VTN. 53 | pub fn content_mut(&mut self) -> &mut ResourceContent { 54 | &mut self.data.content 55 | } 56 | 57 | /// Stores any modifications made to the resource content at the VTN 58 | /// and refreshes the data stored locally with the returned VTN data 59 | pub async fn update(&mut self) -> Result<()> { 60 | self.data = self 61 | .client 62 | .put( 63 | &format!("vens/{}/resources/{}", self.ven_id, self.id()), 64 | &self.data.content, 65 | ) 66 | .await?; 67 | Ok(()) 68 | } 69 | 70 | /// Delete the resource from the VTN 71 | pub async fn delete(self) -> Result { 72 | self.client 73 | .delete(&format!("vens/{}/resources/{}", self.ven_id, self.id())) 74 | .await 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /openleadr-client/src/target.rs: -------------------------------------------------------------------------------- 1 | use openleadr_wire::target::TargetType; 2 | 3 | /// Target for a query to the VTN 4 | #[derive(Copy, Clone, Debug)] 5 | pub enum Target<'a> { 6 | /// Target by a specific program name 7 | Program(&'a str), 8 | 9 | /// Target by a list of program names 10 | Programs(&'a [&'a str]), 11 | 12 | /// Target by a specific event name 13 | Event(&'a str), 14 | 15 | /// Target by a list of event names 16 | Events(&'a [&'a str]), 17 | 18 | /// Target by a specific VEN name 19 | VEN(&'a str), 20 | 21 | /// Target by a list of VEN names 22 | VENs(&'a [&'a str]), 23 | 24 | /// Target by a specific group name 25 | Group(&'a str), 26 | 27 | /// Target by a list of group names 28 | Groups(&'a [&'a str]), 29 | 30 | /// Target by a specific resource name 31 | Resource(&'a str), 32 | 33 | /// Target by a list of resource names 34 | Resources(&'a [&'a str]), 35 | 36 | /// Target by a specific service area 37 | ServiceArea(&'a str), 38 | 39 | /// Target by a list of service areas 40 | ServiceAreas(&'a [&'a str]), 41 | 42 | /// Target by a specific power service location 43 | PowerServiceLocation(&'a str), 44 | 45 | /// Target by a list of power service locations 46 | PowerServiceLocations(&'a [&'a str]), 47 | 48 | /// Target using some other kind of privately defined target type, using a single target value 49 | Other(&'a str, &'a str), 50 | 51 | /// Target using some other kind of privately defined target type, with a list of values 52 | Others(&'a str, &'a [&'a str]), 53 | } 54 | 55 | impl Target<'_> { 56 | /// Get the target label for this specific target 57 | pub fn target_label(&self) -> TargetType { 58 | match self { 59 | Target::Program(_) | Target::Programs(_) => TargetType::ProgramName, 60 | Target::Event(_) | Target::Events(_) => TargetType::EventName, 61 | Target::VEN(_) | Target::VENs(_) => TargetType::VENName, 62 | Target::Group(_) | Target::Groups(_) => TargetType::Group, 63 | Target::Resource(_) | Target::Resources(_) => TargetType::ResourceName, 64 | Target::ServiceArea(_) | Target::ServiceAreas(_) => TargetType::ServiceArea, 65 | Target::PowerServiceLocation(_) | Target::PowerServiceLocations(_) => { 66 | TargetType::PowerServiceLocation 67 | } 68 | Target::Other(p, _) | Target::Others(p, _) => TargetType::Private(p.to_string()), 69 | } 70 | } 71 | 72 | /// Get the list of target values for this specific target 73 | pub fn target_values(&self) -> &[&str] { 74 | match self { 75 | Target::Program(v) => std::slice::from_ref(v), 76 | Target::Programs(v) => v, 77 | Target::Event(v) => std::slice::from_ref(v), 78 | Target::Events(v) => v, 79 | Target::VEN(v) => std::slice::from_ref(v), 80 | Target::VENs(v) => v, 81 | Target::Group(v) => std::slice::from_ref(v), 82 | Target::Groups(v) => v, 83 | Target::Resource(v) => std::slice::from_ref(v), 84 | Target::Resources(v) => v, 85 | Target::ServiceArea(v) => std::slice::from_ref(v), 86 | Target::ServiceAreas(v) => v, 87 | Target::PowerServiceLocation(v) => std::slice::from_ref(v), 88 | Target::PowerServiceLocations(v) => v, 89 | Target::Other(_, v) => std::slice::from_ref(v), 90 | Target::Others(_, v) => v, 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /openleadr-client/src/ven.rs: -------------------------------------------------------------------------------- 1 | use crate::{resource::ResourceClient, ClientRef, Error, Result}; 2 | use chrono::{DateTime, Utc}; 3 | use openleadr_wire::{ 4 | resource::{Resource, ResourceContent, ResourceId}, 5 | ven::{VenContent, VenId}, 6 | Ven, 7 | }; 8 | use std::sync::Arc; 9 | 10 | /// A client for interacting with the data in a specific VEN and the resources contained in the VEN. 11 | #[derive(Debug, Clone)] 12 | pub struct VenClient { 13 | client: Arc, 14 | data: Ven, 15 | } 16 | 17 | impl VenClient { 18 | pub(super) fn from_ven(client: Arc, data: Ven) -> Self { 19 | Self { client, data } 20 | } 21 | 22 | /// Get the VEN ID 23 | pub fn id(&self) -> &VenId { 24 | &self.data.id 25 | } 26 | 27 | /// Get the time the VEN was created on the VTN 28 | pub fn created_date_time(&self) -> DateTime { 29 | self.data.created_date_time 30 | } 31 | 32 | /// Get the time the VEN was last modified on the VTN 33 | pub fn modification_date_time(&self) -> DateTime { 34 | self.data.modification_date_time 35 | } 36 | 37 | /// Read the content of the VEN 38 | pub fn content(&self) -> &VenContent { 39 | &self.data.content 40 | } 41 | 42 | /// Modify the content of the VEN. 43 | /// Make sure to call [`update`](Self::update) 44 | /// after your modifications to store them on the VTN. 45 | pub fn content_mut(&mut self) -> &mut VenContent { 46 | &mut self.data.content 47 | } 48 | 49 | /// Stores any modifications made to the VEN content at the VTN 50 | /// and refreshes the data stored locally with the returned VTN data 51 | pub async fn update(&mut self) -> Result<()> { 52 | self.data = self 53 | .client 54 | .put(&format!("vens/{}", self.id()), &self.data.content) 55 | .await?; 56 | Ok(()) 57 | } 58 | 59 | /// Delete the VEN from the VTN. 60 | /// 61 | /// Depending on the VTN implementation, 62 | /// you may need to delete all associated resources before you can delete the VEN 63 | pub async fn delete(self) -> Result { 64 | self.client.delete(&format!("vens/{}", self.id())).await 65 | } 66 | 67 | /// Create a resource as a child of this VEN 68 | pub async fn create_resource(&self, resource: ResourceContent) -> Result { 69 | let resource = self 70 | .client 71 | .post(&format!("vens/{}/resources", self.id()), &resource) 72 | .await?; 73 | Ok(ResourceClient::from_resource( 74 | Arc::clone(&self.client), 75 | self.id().clone(), 76 | resource, 77 | )) 78 | } 79 | 80 | async fn get_resources_req( 81 | &self, 82 | resource_name: Option<&str>, 83 | skip: usize, 84 | limit: usize, 85 | ) -> Result> { 86 | let skip_str = skip.to_string(); 87 | let limit_str = limit.to_string(); 88 | 89 | let mut query: Vec<(&str, &str)> = vec![("skip", &skip_str), ("limit", &limit_str)]; 90 | 91 | if let Some(resource_name) = resource_name { 92 | query.push(("resourceName", resource_name)); 93 | } 94 | 95 | let resources: Vec = self 96 | .client 97 | .get(&format!("/vens/{}/resources", self.id()), &query) 98 | .await?; 99 | Ok(resources 100 | .into_iter() 101 | .map(|resource| { 102 | ResourceClient::from_resource(Arc::clone(&self.client), self.id().clone(), resource) 103 | }) 104 | .collect()) 105 | } 106 | 107 | /// Get all resources stored as children of this VEN. 108 | /// 109 | /// The client automatically tries to iterate pages where necessary. 110 | pub async fn get_all_resources( 111 | &self, 112 | resource_name: Option<&str>, 113 | ) -> Result> { 114 | self.client 115 | .iterate_pages(|skip, limit| self.get_resources_req(resource_name, skip, limit)) 116 | .await 117 | } 118 | 119 | /// Get a resource by its ID 120 | pub async fn get_resource_by_id(&self, id: &ResourceId) -> Result { 121 | let resource = self 122 | .client 123 | .get(&format!("vens/{}/resources/{}", self.id(), id), &[]) 124 | .await?; 125 | Ok(ResourceClient::from_resource( 126 | Arc::clone(&self.client), 127 | self.id().clone(), 128 | resource, 129 | )) 130 | } 131 | 132 | /// Get VEN by name from VTN. 133 | /// According to the spec, a [`resource_name`](ResourceContent::resource_name) must be unique per VEN. 134 | pub async fn get_resource_by_name(&self, name: &str) -> Result { 135 | let mut resources: Vec = self 136 | .client 137 | .get( 138 | &format!("vens/{}/resources", self.id()), 139 | &[("resourceName", name)], 140 | ) 141 | .await?; 142 | match resources[..] { 143 | [] => Err(Error::ObjectNotFound), 144 | [_] => Ok(ResourceClient::from_resource( 145 | Arc::clone(&self.client), 146 | self.id().clone(), 147 | resources.remove(0), 148 | )), 149 | [..] => Err(Error::DuplicateObject), 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /openleadr-client/tests/basic-read.rs: -------------------------------------------------------------------------------- 1 | use openleadr_client::Filter; 2 | use openleadr_wire::program::ProgramContent; 3 | use sqlx::PgPool; 4 | 5 | mod common; 6 | 7 | #[sqlx::test(fixtures("users"))] 8 | async fn basic_create_read(db: PgPool) -> Result<(), openleadr_client::Error> { 9 | let client = common::setup_client(db).await; 10 | 11 | client 12 | .create_program(ProgramContent::new("test-prog")) 13 | .await?; 14 | 15 | let programs = client.get_program_list(Filter::none()).await?; 16 | assert_eq!(programs.len(), 1); 17 | assert_eq!(programs[0].content().program_name, "test-prog"); 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /openleadr-client/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use axum::body::Body; 3 | use http_body_util::BodyExt; 4 | use openleadr_client::{Client, ClientCredentials, HttpClient, ProgramClient}; 5 | use openleadr_vtn::{data_source::PostgresStorage, jwt::AuthRole, state::AppState}; 6 | use openleadr_wire::program::ProgramContent; 7 | use reqwest::{Method, RequestBuilder, Response}; 8 | use sqlx::PgPool; 9 | use std::{env::VarError, ops::Deref, sync::Arc}; 10 | use tower::{Service, ServiceExt}; 11 | use url::Url; 12 | 13 | fn default_credentials(auth_role: AuthRole) -> ClientCredentials { 14 | let (id, secr) = match auth_role { 15 | AuthRole::UserManager => ("user-manager", "user-manager"), 16 | AuthRole::VenManager => ("ven-manager", "ven-manager"), 17 | AuthRole::Business(_) => ("business-1", "business-1"), 18 | AuthRole::AnyBusiness => ("any-business", "any-business"), 19 | AuthRole::VEN(_) => ("ven-1", "ven-1"), 20 | }; 21 | 22 | ClientCredentials::new(id.to_string(), secr.to_string()) 23 | } 24 | 25 | #[derive(Debug)] 26 | pub struct MockClientRef { 27 | router: Arc>, 28 | } 29 | 30 | impl MockClientRef { 31 | pub fn new(router: axum::Router) -> Self { 32 | MockClientRef { 33 | router: Arc::new(tokio::sync::Mutex::new(router)), 34 | } 35 | } 36 | 37 | pub fn into_client(self, auth: Option) -> Client { 38 | Client::with_http_client( 39 | "https://example.com/".parse().unwrap(), 40 | "https://example.com/auth/token".parse().unwrap(), 41 | Box::new(self), 42 | auth, 43 | ) 44 | } 45 | } 46 | 47 | #[async_trait] 48 | impl HttpClient for MockClientRef { 49 | fn request_builder(&self, method: Method, url: Url) -> RequestBuilder { 50 | reqwest::Client::new().request(method, url) 51 | } 52 | 53 | async fn send(&self, req: RequestBuilder) -> reqwest::Result { 54 | let request = axum::http::Request::try_from(req.build()?)?; 55 | 56 | let response = 57 | ServiceExt::>::ready(&mut *self.router.lock().await) 58 | .await 59 | .unwrap() 60 | .call(request) 61 | .await 62 | .unwrap(); 63 | 64 | let (parts, body) = response.into_parts(); 65 | let body = body.collect().await.unwrap().to_bytes(); 66 | let body = reqwest::Body::from(body); 67 | let response = axum::http::Response::from_parts(parts, body); 68 | 69 | Ok(response.into()) 70 | } 71 | } 72 | 73 | pub struct TestContext { 74 | pub client: Client, 75 | } 76 | 77 | impl Deref for TestContext { 78 | type Target = Client; 79 | fn deref(&self) -> &Self::Target { 80 | &self.client 81 | } 82 | } 83 | 84 | #[allow(unused)] 85 | pub async fn setup(auth_role: AuthRole) -> TestContext { 86 | dotenvy::dotenv().unwrap(); 87 | match std::env::var("OPENLEADR_RS_VTN_URL") { 88 | Ok(url) => match url.parse() { 89 | Ok(url) => TestContext { 90 | client: setup_url_client(url), 91 | }, 92 | Err(e) => panic!("Could not parse URL: {e}"), 93 | }, 94 | Err(VarError::NotPresent) => match std::env::var("DATABASE_URL") { 95 | Ok(db_url) => { 96 | let db = PgPool::connect(&db_url).await.unwrap(); 97 | local_vtn_test_client(db, auth_role).await 98 | } 99 | Err(_) => panic!("Must either set DATABASE_URL or OPENLEADR_RS_VTN_URL env var"), 100 | }, 101 | Err(VarError::NotUnicode(e)) => panic!("Could not parse URL: {e:?}"), 102 | } 103 | } 104 | 105 | async fn local_vtn_test_client(db: PgPool, auth_role: AuthRole) -> TestContext { 106 | let cred = default_credentials(auth_role); 107 | let storage = PostgresStorage::new(db).unwrap(); 108 | 109 | let router = AppState::new(storage).await.into_router(); 110 | TestContext { 111 | client: MockClientRef::new(router).into_client(Some(cred)), 112 | } 113 | } 114 | 115 | // FIXME make this function independent of the storage backend 116 | pub async fn setup_mock_client(db: PgPool) -> Client { 117 | // let auth_info = AuthInfo::bl_admin(); 118 | let client_credentials = ClientCredentials::new("admin".to_string(), "admin".to_string()); 119 | 120 | let storage = PostgresStorage::new(db).unwrap(); 121 | // storage.auth.try_write().unwrap().push(auth_info); 122 | 123 | let app_state = AppState::new(storage).await; 124 | 125 | MockClientRef::new(app_state.into_router()).into_client(Some(client_credentials)) 126 | } 127 | 128 | pub fn setup_url_client(url: Url) -> Client { 129 | Client::with_url( 130 | url, 131 | Some(ClientCredentials::new( 132 | "admin".to_string(), 133 | "admin".to_string(), 134 | )), 135 | ) 136 | } 137 | 138 | pub async fn setup_client(db: PgPool) -> Client { 139 | match std::env::var("OPENADR_VTN_URL") { 140 | Ok(url) => match url.parse() { 141 | Ok(url) => setup_url_client(url), 142 | Err(e) => panic!("Could not parse URL: {e}"), 143 | }, 144 | Err(VarError::NotPresent) => setup_mock_client(db).await, 145 | Err(VarError::NotUnicode(e)) => panic!("Could not parse URL: {e:?}"), 146 | } 147 | } 148 | 149 | #[allow(unused)] 150 | pub async fn setup_program_client(program_name: impl ToString, db: PgPool) -> ProgramClient { 151 | let client = setup_client(db).await; 152 | 153 | let program_content = ProgramContent { 154 | program_name: program_name.to_string(), 155 | program_long_name: Some("program_long_name".to_string()), 156 | retailer_name: Some("retailer_name".to_string()), 157 | retailer_long_name: Some("retailer_long_name".to_string()), 158 | program_type: None, 159 | country: None, 160 | principal_subdivision: None, 161 | time_zone_offset: None, 162 | interval_period: None, 163 | program_descriptions: None, 164 | binding_events: None, 165 | local_price: None, 166 | payload_descriptors: None, 167 | targets: None, 168 | }; 169 | 170 | client.create_program(program_content).await.unwrap() 171 | } 172 | -------------------------------------------------------------------------------- /openleadr-client/tests/fixtures: -------------------------------------------------------------------------------- 1 | ../../fixtures/ -------------------------------------------------------------------------------- /openleadr-client/tests/resources.rs: -------------------------------------------------------------------------------- 1 | use crate::common::setup; 2 | use openleadr_vtn::jwt::AuthRole; 3 | use openleadr_wire::{ 4 | resource::ResourceContent, 5 | target::{TargetEntry, TargetMap, TargetType}, 6 | values_map::{Value, ValueType, ValuesMap}, 7 | ven::VenContent, 8 | }; 9 | use serial_test::serial; 10 | 11 | mod common; 12 | 13 | #[tokio::test] 14 | #[serial] 15 | async fn crud() { 16 | let ctx = setup(AuthRole::VenManager).await; 17 | 18 | // create new VEN 19 | let new = VenContent::new("ven-test".to_string(), None, None, None); 20 | let ven = ctx.create_ven(new).await.unwrap(); 21 | 22 | // Create 23 | let new = ResourceContent { 24 | resource_name: "test-resource".to_string(), 25 | attributes: None, 26 | targets: None, 27 | }; 28 | let created_resource = ven.create_resource(new.clone()).await.unwrap(); 29 | assert_eq!(created_resource.content().resource_name, "test-resource"); 30 | 31 | // Create with the same name fails for the same ven 32 | { 33 | let err = ven.create_resource(new.clone()).await.unwrap_err(); 34 | assert!(err.is_conflict()); 35 | } 36 | 37 | // Create with the same name succeeds for a different ven 38 | { 39 | let new_ven2 = VenContent::new("ven-test2".to_string(), None, None, None); 40 | let ven2 = ctx.create_ven(new_ven2).await.unwrap(); 41 | 42 | let resource = ven2.create_resource(new).await.unwrap(); 43 | 44 | // Cleanup 45 | resource.delete().await.unwrap(); 46 | ven2.delete().await.unwrap(); 47 | } 48 | 49 | // Retrieve all 50 | { 51 | let resources = ven.get_all_resources(None).await.unwrap(); 52 | assert!(resources 53 | .iter() 54 | .any(|r| r.content().resource_name == "test-resource")); 55 | } 56 | 57 | // Retrieve one by name 58 | { 59 | let resource2 = ven 60 | .create_resource(ResourceContent { 61 | resource_name: "test-resource2".to_string(), 62 | attributes: None, 63 | targets: None, 64 | }) 65 | .await 66 | .unwrap(); 67 | let get_resource = ven.get_resource_by_name("test-resource").await.unwrap(); 68 | assert_eq!(get_resource.content(), created_resource.content()); 69 | resource2.delete().await.unwrap(); 70 | } 71 | 72 | // Retrieve one by ID 73 | let mut get_resource = ven.get_resource_by_id(created_resource.id()).await.unwrap(); 74 | assert_eq!(get_resource.content(), created_resource.content()); 75 | 76 | // Update 77 | { 78 | let updated_name = "test-resource-updated".to_string(); 79 | let updated_attributes = Some(vec![ValuesMap { 80 | value_type: ValueType("PRICE".to_string()), 81 | values: vec![Value::Number(123.12)], 82 | }]); 83 | let updated_targets = Some(TargetMap(vec![TargetEntry { 84 | label: TargetType::Group, 85 | values: vec!["group-1".to_string()], 86 | }])); 87 | 88 | get_resource.content_mut().resource_name = updated_name.clone(); 89 | get_resource.content_mut().attributes = updated_attributes.clone(); 90 | get_resource.content_mut().targets = updated_targets.clone(); 91 | get_resource.update().await.unwrap(); 92 | 93 | assert_eq!(get_resource.content().resource_name, updated_name); 94 | assert_eq!(get_resource.content().attributes, updated_attributes); 95 | assert_eq!(get_resource.content().targets, updated_targets); 96 | 97 | let get_resource2 = ven 98 | .get_resource_by_name("test-resource-updated") 99 | .await 100 | .unwrap(); 101 | assert_eq!(get_resource2.content().resource_name, updated_name); 102 | assert_eq!(get_resource2.content().attributes, updated_attributes); 103 | assert_eq!(get_resource2.content().targets, updated_targets); 104 | 105 | assert_eq!( 106 | get_resource2.modification_date_time(), 107 | get_resource.modification_date_time() 108 | ); 109 | assert_ne!( 110 | created_resource.modification_date_time(), 111 | get_resource.modification_date_time() 112 | ); 113 | assert_eq!( 114 | get_resource2.created_date_time(), 115 | created_resource.created_date_time() 116 | ) 117 | } 118 | 119 | // Delete 120 | { 121 | let id = created_resource.id().clone(); 122 | created_resource.delete().await.unwrap(); 123 | let err = ven.get_resource_by_id(&id).await.unwrap_err(); 124 | assert!(err.is_not_found()) 125 | } 126 | 127 | // Cleanup 128 | { 129 | ven.delete().await.unwrap(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /openleadr-client/tests/ven.rs: -------------------------------------------------------------------------------- 1 | use crate::common::setup; 2 | use openleadr_client::Filter; 3 | use openleadr_vtn::jwt::AuthRole; 4 | use openleadr_wire::{ 5 | target::{TargetEntry, TargetMap, TargetType}, 6 | values_map::{Value, ValueType, ValuesMap}, 7 | ven::VenContent, 8 | }; 9 | use serial_test::serial; 10 | 11 | mod common; 12 | 13 | #[tokio::test] 14 | #[serial] 15 | async fn crud() { 16 | let ctx = setup(AuthRole::VenManager).await; 17 | 18 | // cleanup potentially clashing VEN 19 | { 20 | if let Ok(old_ven) = ctx.get_ven_by_name("ven-test").await { 21 | assert_eq!(old_ven.content().ven_name, "ven-test"); 22 | old_ven.delete().await.unwrap(); 23 | } 24 | } 25 | 26 | // Create 27 | let ven = VenContent::new("test-ven".to_string(), None, None, None); 28 | let create_ven = ctx.create_ven(ven.clone()).await.unwrap(); 29 | assert_eq!(create_ven.content().ven_name, "test-ven"); 30 | 31 | // Create with the same name fails 32 | { 33 | let err = ctx.create_ven(ven).await.unwrap_err(); 34 | assert!(err.is_conflict()); 35 | } 36 | 37 | // Retrieve all 38 | { 39 | let vens = ctx.get_ven_list(Filter::none()).await.unwrap(); 40 | assert!(vens.iter().any(|v| v.content().ven_name == "test-ven")); 41 | } 42 | 43 | // Retrieve one by ID 44 | { 45 | let get_ven_id = ctx.get_ven_by_id(create_ven.id()).await.unwrap(); 46 | assert_eq!(get_ven_id.content(), create_ven.content()); 47 | } 48 | 49 | // Retrieve one by name 50 | let mut get_ven = ctx.get_ven_by_name("test-ven").await.unwrap(); 51 | assert_eq!(get_ven.content(), create_ven.content()); 52 | assert_eq!(get_ven.content().ven_name, "test-ven"); 53 | 54 | // Update 55 | { 56 | let updated_name = "ven-test-update".to_string(); 57 | let updated_attributes = Some(vec![ValuesMap { 58 | value_type: ValueType("PRICE".to_string()), 59 | values: vec![Value::Number(123.12)], 60 | }]); 61 | let updated_targets = Some(TargetMap(vec![TargetEntry { 62 | label: TargetType::Group, 63 | values: vec!["group-1".to_string()], 64 | }])); 65 | 66 | get_ven.content_mut().ven_name = updated_name.clone(); 67 | get_ven.content_mut().attributes = updated_attributes.clone(); 68 | get_ven.content_mut().targets = updated_targets.clone(); 69 | get_ven.update().await.unwrap(); 70 | 71 | assert_eq!(get_ven.content().ven_name, updated_name); 72 | assert_eq!(get_ven.content().attributes, updated_attributes); 73 | assert_eq!(get_ven.content().targets, updated_targets); 74 | 75 | let get_ven2 = ctx.get_ven_by_name("ven-test-update").await.unwrap(); 76 | assert_eq!(get_ven2.content().ven_name, updated_name); 77 | assert_eq!(get_ven2.content().attributes, updated_attributes); 78 | assert_eq!(get_ven2.content().targets, updated_targets); 79 | 80 | assert_eq!( 81 | get_ven2.modification_date_time(), 82 | get_ven.modification_date_time() 83 | ); 84 | assert_ne!( 85 | create_ven.modification_date_time(), 86 | get_ven.modification_date_time() 87 | ); 88 | assert_eq!(get_ven2.created_date_time(), create_ven.created_date_time()) 89 | } 90 | 91 | // Delete 92 | get_ven.delete().await.unwrap(); 93 | } 94 | -------------------------------------------------------------------------------- /openleadr-vtn/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openleadr-vtn" 3 | description = "OpenADR 3.0 VTN server" 4 | readme = "README.md" 5 | version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | homepage.workspace = true 10 | publish.workspace = true 11 | rust-version.workspace = true 12 | keywords.workspace = true 13 | 14 | [dependencies] 15 | openleadr-wire.workspace = true 16 | 17 | serde.workspace = true 18 | serde_json.workspace = true 19 | serde_with.workspace = true 20 | 21 | reqwest.workspace = true 22 | axum.workspace = true 23 | axum-extra.workspace = true 24 | tokio = { workspace = true, features = ["full"] } 25 | tower-http.workspace = true 26 | tower.workspace = true 27 | 28 | tracing.workspace = true 29 | tracing-subscriber.workspace = true 30 | 31 | url.workspace = true 32 | uuid.workspace = true 33 | jsonwebtoken.workspace = true 34 | base64.workspace = true 35 | rand.workspace = true 36 | validator.workspace = true 37 | mime.workspace = true 38 | http-body-util.workspace = true 39 | async-trait.workspace = true 40 | 41 | chrono.workspace = true 42 | thiserror.workspace = true 43 | 44 | sqlx = { workspace = true, optional = true } 45 | argon2 = { workspace = true, optional = true } 46 | dotenvy = { workspace = true, optional = true } 47 | 48 | [dev-dependencies] 49 | tokio = { workspace = true, features = ["full", "test-util"] } 50 | serial_test.workspace = true 51 | 52 | [features] 53 | default = ["postgres", "live-db-test", "internal-oauth"] 54 | live-db-test = ["postgres", "internal-oauth"] 55 | postgres = ["sqlx/postgres", "dep:dotenvy", "dep:argon2"] 56 | internal-oauth = [] -------------------------------------------------------------------------------- /openleadr-vtn/README.md: -------------------------------------------------------------------------------- 1 | ![maintenance-status](https://img.shields.io/badge/maintenance-actively--developed-brightgreen.svg) 2 | ![codecov](https://codecov.io/gh/OpenLEADR/openleadr-rs/graph/badge.svg?token=BKQ0QW9G8H) 3 | ![Checks](https://github.com/OpenLEADR/openleadr-rs/actions/workflows/checks.yml/badge.svg?branch=main) 4 | ![Crates.io Version](https://img.shields.io/crates/v/openleadr-vtn) 5 | 6 | # OpenADR 3.0 VTN server in Rust 7 | 8 | ![LF energy OpenLEADR logo](../openleadr-logo.svg) 9 | 10 | This crate contains an OpenADR VTN implementation. 11 | 12 | The following contains information specific to the VTN application, i.e., the server. 13 | If you are interested in information about the whole project, please visit the [project level Readme](../README.md). 14 | 15 | ## Getting started 16 | Your machine needs a recent version of Rust installed. 17 | Please refer to the [official installation website](https://rustup.rs/) for the setup. 18 | To apply the Database migrations, you also need the sqlx-cli installed. 19 | Simply run `cargo install sqlx-cli`. 20 | 21 | All the following commands are executed in the root directory of the Git repository. 22 | 23 | ### Database setup 24 | 25 | First, start up a postgres database. For example, using docker compose: 26 | 27 | ```bash 28 | docker compose up -d db 29 | ``` 30 | 31 | Run the [migrations](https://github.com/launchbadge/sqlx/blob/main/sqlx-cli/README.md): 32 | 33 | ```bash 34 | cargo sqlx migrate run 35 | ``` 36 | 37 | ### How to use 38 | 39 | Running the VTN using cargo: 40 | 41 | ```bash 42 | RUST_LOG=trace cargo run --bin openleadr-vtn 43 | ``` 44 | 45 | Running the VTN using docker-compose: 46 | 47 | ```bash 48 | docker compose up -d 49 | ``` 50 | 51 | ### Internal vs. external OAuth provider 52 | The VTN implementation does feature an implementation of an OAuth provider including user management APIs 53 | to allow for an easy setup. 54 | The OpenADR specification does not require this feature but mentions that there must exist some OAuth provider somewhere. 55 | Generally, the idea of OAuth is to decouple the authorization from the resource server, here the VTN. 56 | Therefore, the OAuth provider feature is optional. 57 | You can either disable it during compile time or runtime. 58 | 59 | **During runtime** 60 | The OAuth configuration of the VTN is done via the following environment variables: 61 | - `OAUTH_TYPE` (allowed values: `INTERNAL`, `EXTERNAL`. Defaults to `INTERNAL`) 62 | - `OAUTH_BASE64_SECRET` (must be at least 256 bit long. Required if `OAUTH_KEY_TYPE` is `HMAC`) 63 | - `OAUTH_KEY_TYPE`(allows values: `HMAC`, `RSA`, `EC`, `ED`. Defaults to `HMAC`) 64 | - `OAUTH_PEM` (path to a PEM encoded public key file. Either `OAUTH_PEM` or `OAUTH_JWKS_LOCATION` is required for all `OAUTH_KEY_TYPE`s, except `HMAC`) 65 | - `OAUTH_JWKS_LOCATION` (path to the OAUTH server well known JWKS endpoint. Either `OAUTH_PEM` or `OAUTH_JWKS_LOCATION` is required for all `OAUTH_KEY_TYPE`s, except `HMAC`) 66 | - `OAUTH_VALID_AUDIENCES` (specifies the list of valid audiences for token validation, ensuring that the token is intended for the correct recipient. Required when `OAUTH_TYPE` is `EXTERNAL`. Optional and defaults to an empty list when `OAUTH_TYPE` is `INTERNAL`, which will fail validation if an `aud` claim is present in the decoded access token.) 67 | 68 | The internal OAuth provider does only support `HMAC`. 69 | 70 | **During compiletime** 71 | If you already know that you don't need the internal OAuth feature, 72 | you can disable it during compilation with the feature flag `internal-oauth`, which is enabled by default. 73 | Therefore, run 74 | ```bash 75 | cargo build/run --bin openleadr-vtn --no-default-features --features=postgres [--release] 76 | ``` 77 | 78 | ### Note on prepared SQL 79 | 80 | This workspace uses SQLX macro to type check SQL statements. 81 | In order to build the crate without a running SQL server (such as in the docker), SQLX must be run in offline mode. 82 | In this mode type checking is done via a cached variant of the DB (the .sqlx directory). 83 | For this to work as intended, each time a change is made to SQL schemas or queries, please run 84 | 85 | ```bash 86 | cargo sqlx prepare --workspace 87 | ``` 88 | 89 | This will update the cached SQL in the `.sqlx` directory which should be committed to GitHub. 90 | 91 | ### Invalidating the docker build cache 92 | 93 | To expedite the slow cargo release builds, the Dockerfile uses a multi-stage build. 94 | If changes have been made and are not being reflected in the binary running inside docker, try 95 | 96 | ```bash 97 | docker compose up --force-recreate --build --no-deps vtn 98 | ``` 99 | 100 | This will force a rebuild 101 | -------------------------------------------------------------------------------- /openleadr-vtn/build.rs: -------------------------------------------------------------------------------- 1 | // generated by `sqlx migrate build-script` 2 | fn main() { 3 | // trigger recompilation when a new migration is added 4 | println!("cargo:rerun-if-changed=migrations"); 5 | } 6 | -------------------------------------------------------------------------------- /openleadr-vtn/migrations: -------------------------------------------------------------------------------- 1 | ../migrations/ -------------------------------------------------------------------------------- /openleadr-vtn/src/api/auth.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "internal-oauth")] 2 | use crate::{api::ValidatedForm, data_source::AuthSource, jwt::JwtManager}; 3 | #[cfg(feature = "internal-oauth")] 4 | use axum::extract::State; 5 | #[cfg(feature = "internal-oauth")] 6 | use axum_extra::headers::{authorization::Basic, Authorization}; 7 | #[cfg(feature = "internal-oauth")] 8 | use serde::Deserialize; 9 | #[cfg(feature = "internal-oauth")] 10 | use std::sync::Arc; 11 | #[cfg(feature = "internal-oauth")] 12 | use validator::Validate; 13 | 14 | use crate::error::AppError; 15 | use axum::{ 16 | http::{header::AUTHORIZATION, HeaderMap, Response, StatusCode}, 17 | response::IntoResponse, 18 | Json, 19 | }; 20 | use axum_extra::headers::{authorization::Bearer, Header}; 21 | use openleadr_wire::oauth::{OAuthError, OAuthErrorType}; 22 | use reqwest::header; 23 | 24 | #[cfg(feature = "internal-oauth")] 25 | use tracing::trace; 26 | 27 | #[derive(Debug, Deserialize, Validate)] 28 | #[cfg(feature = "internal-oauth")] 29 | pub struct AccessTokenRequest { 30 | grant_type: String, 31 | // TODO: handle scope 32 | // scope: Option, 33 | client_id: Option, 34 | client_secret: Option, 35 | } 36 | 37 | #[derive(Debug)] 38 | pub struct ResponseOAuthError(pub OAuthError); 39 | 40 | impl IntoResponse for ResponseOAuthError { 41 | fn into_response(self) -> Response { 42 | match self.0.error { 43 | OAuthErrorType::InvalidClient => ( 44 | StatusCode::UNAUTHORIZED, 45 | [(header::WWW_AUTHENTICATE, r#"Basic realm="VTN""#)], 46 | Json(self.0), 47 | ) 48 | .into_response(), 49 | OAuthErrorType::ServerError => { 50 | (StatusCode::INTERNAL_SERVER_ERROR, Json(self.0)).into_response() 51 | } 52 | OAuthErrorType::OAuthNotEnabled => AppError::NotFound.into_response(), 53 | _ => (StatusCode::BAD_REQUEST, Json(self.0)).into_response(), 54 | } 55 | } 56 | } 57 | 58 | impl From for ResponseOAuthError { 59 | fn from(_: jsonwebtoken::errors::Error) -> Self { 60 | ResponseOAuthError( 61 | OAuthError::new(OAuthErrorType::ServerError) 62 | .with_description("Could not issue a new token".to_string()), 63 | ) 64 | } 65 | } 66 | 67 | impl From for ResponseOAuthError { 68 | fn from(err: OAuthError) -> Self { 69 | ResponseOAuthError(err) 70 | } 71 | } 72 | 73 | #[derive(Debug, serde::Serialize)] 74 | pub struct AccessTokenResponse { 75 | access_token: String, 76 | token_type: &'static str, 77 | expires_in: u64, 78 | #[serde(skip_serializing_if = "Option::is_none")] 79 | scope: Option, 80 | } 81 | 82 | impl IntoResponse for AccessTokenResponse { 83 | fn into_response(self) -> Response { 84 | IntoResponse::into_response((StatusCode::OK, Json(self))) 85 | } 86 | } 87 | 88 | /// RFC 6749 client credentials grant flow 89 | #[cfg(feature = "internal-oauth")] 90 | pub(crate) async fn token( 91 | State(auth_source): State>, 92 | State(jwt_manager): State>, 93 | headers: HeaderMap, 94 | ValidatedForm(request): ValidatedForm, 95 | ) -> Result { 96 | if request.grant_type != "client_credentials" { 97 | return Err(OAuthError::new(OAuthErrorType::UnsupportedGrantType) 98 | .with_description("Only client_credentials grant type is supported".to_string()) 99 | .into()); 100 | } 101 | 102 | let mut auth_header = None; 103 | if let Some(header) = headers.get(AUTHORIZATION) { 104 | if let Ok(basic_auth) = Authorization::::decode(&mut [header].into_iter()) { 105 | auth_header = Some(( 106 | basic_auth.username().to_string(), 107 | basic_auth.password().to_string(), 108 | )) 109 | } else if Authorization::::decode(&mut [header].into_iter()).is_ok() { 110 | trace!("login request contained Bearer token which got ignored") 111 | } 112 | } 113 | 114 | let auth_body = request 115 | .client_id 116 | .as_ref() 117 | .map(|client_id| { 118 | ( 119 | client_id.as_str(), 120 | request.client_secret.as_deref().unwrap_or(""), 121 | ) 122 | }) 123 | .or_else(|| request.client_secret.as_ref().map(|cr| ("", cr.as_str()))); 124 | 125 | if auth_header.is_some() && auth_body.is_some() { 126 | return Err(OAuthError::new(OAuthErrorType::InvalidRequest) 127 | .with_description("Both header and body authentication provided".to_string()) 128 | .into()); 129 | } 130 | 131 | let Some((client_id, client_secret)) = 132 | auth_body.or(auth_header.as_ref().map(|(a, b)| (a.as_str(), b.as_str()))) 133 | else { 134 | return Err(OAuthError::new(OAuthErrorType::InvalidClient) 135 | .with_description( 136 | "No valid authentication data provided, client_id and client_secret required" 137 | .to_string(), 138 | ) 139 | .into()); 140 | }; 141 | 142 | // check that the client_id and client_secret are valid 143 | let Some(user) = auth_source 144 | .check_credentials(client_id, client_secret) 145 | .await 146 | else { 147 | return Err(OAuthError::new(OAuthErrorType::InvalidClient) 148 | .with_description("Invalid client_id or client_secret".to_string()) 149 | .into()); 150 | }; 151 | 152 | let expiration = std::time::Duration::from_secs(3600 * 24 * 30); 153 | let token = jwt_manager.create(expiration, user.client_id, user.roles)?; 154 | 155 | Ok(AccessTokenResponse { 156 | access_token: token, 157 | token_type: "Bearer", 158 | expires_in: expiration.as_secs(), 159 | scope: None, 160 | }) 161 | } 162 | -------------------------------------------------------------------------------- /openleadr-vtn/src/api/fixtures: -------------------------------------------------------------------------------- 1 | ../../../fixtures/ -------------------------------------------------------------------------------- /openleadr-vtn/src/data_source/postgres/fixtures: -------------------------------------------------------------------------------- 1 | ../../../../fixtures/ -------------------------------------------------------------------------------- /openleadr-vtn/src/data_source/postgres/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "internal-oauth")] 2 | use crate::data_source::{postgres::user::PgAuthSource, AuthSource}; 3 | 4 | use crate::{ 5 | data_source::{ 6 | postgres::{ 7 | event::PgEventStorage, program::PgProgramStorage, report::PgReportStorage, 8 | ven::PgVenStorage, 9 | }, 10 | DataSource, EventCrud, ProgramCrud, ReportCrud, ResourceCrud, VenCrud, 11 | }, 12 | error::AppError, 13 | jwt::{BusinessIds, Claims}, 14 | }; 15 | use async_trait::async_trait; 16 | use dotenvy::dotenv; 17 | use openleadr_wire::target::{TargetMap, TargetType}; 18 | use resource::PgResourceStorage; 19 | use serde::Serialize; 20 | use sqlx::{migrate::MigrateError, postgres::PgPoolOptions, PgPool}; 21 | use std::sync::Arc; 22 | use tracing::{error, info, trace}; 23 | 24 | use super::Migrate; 25 | 26 | mod event; 27 | mod program; 28 | mod report; 29 | mod resource; 30 | #[cfg(feature = "internal-oauth")] 31 | mod user; 32 | mod ven; 33 | 34 | #[derive(Clone)] 35 | pub struct PostgresStorage { 36 | db: PgPool, 37 | } 38 | 39 | impl DataSource for PostgresStorage { 40 | fn programs(&self) -> Arc { 41 | Arc::::new(self.db.clone().into()) 42 | } 43 | 44 | fn reports(&self) -> Arc { 45 | Arc::::new(self.db.clone().into()) 46 | } 47 | 48 | fn events(&self) -> Arc { 49 | Arc::::new(self.db.clone().into()) 50 | } 51 | 52 | fn vens(&self) -> Arc { 53 | Arc::::new(self.db.clone().into()) 54 | } 55 | 56 | fn resources(&self) -> Arc { 57 | Arc::::new(self.db.clone().into()) 58 | } 59 | 60 | #[cfg(feature = "internal-oauth")] 61 | fn auth(&self) -> Arc { 62 | Arc::::new(self.db.clone().into()) 63 | } 64 | 65 | /// Verify the connection pool is open and has at least one connection 66 | fn connection_active(&self) -> bool { 67 | !self.db.is_closed() && self.db.size() > 0 68 | } 69 | } 70 | 71 | #[async_trait] 72 | impl Migrate for PostgresStorage { 73 | async fn migrate(&self) -> Result<(), MigrateError> { 74 | sqlx::migrate!("./migrations").run(&self.db).await 75 | } 76 | } 77 | 78 | impl PostgresStorage { 79 | pub fn new(db: PgPool) -> Result { 80 | Ok(Self { db }) 81 | } 82 | 83 | pub async fn from_env() -> Result { 84 | dotenv().ok(); 85 | let db_url = std::env::var("DATABASE_URL") 86 | .expect("Missing DATABASE_URL env var even though the 'postgres' feature is active"); 87 | 88 | let db = PgPoolOptions::new() 89 | .min_connections(1) 90 | .connect(&db_url) 91 | .await?; 92 | 93 | let connect_options = db.connect_options(); 94 | let safe_db_url = format!( 95 | "{}:{}/{}", 96 | connect_options.get_host(), 97 | connect_options.get_port(), 98 | connect_options.get_database().unwrap_or_default() 99 | ); 100 | 101 | Self::new(db) 102 | .inspect_err(|err| error!(?err, "could not connect to Postgres database")) 103 | .inspect(|_| { 104 | info!( 105 | "Successfully connected to Postgres backend at {}", 106 | safe_db_url 107 | ) 108 | }) 109 | } 110 | } 111 | 112 | fn to_json_value(v: Option) -> Result, AppError> { 113 | v.map(|v| serde_json::to_value(v).map_err(AppError::SerdeJsonBadRequest)) 114 | .transpose() 115 | } 116 | 117 | #[tracing::instrument(level = "trace")] 118 | fn extract_vens(targets: Option) -> (Option, Option>) { 119 | if let Some(TargetMap(targets)) = targets { 120 | let (vens, targets): (Vec<_>, Vec<_>) = targets 121 | .into_iter() 122 | .partition(|t| t.label == TargetType::VENName); 123 | 124 | let vens = vens 125 | .into_iter() 126 | .map(|t| t.values[0].clone()) 127 | .collect::>(); 128 | 129 | let targets = if targets.is_empty() { 130 | None 131 | } else { 132 | Some(TargetMap(targets)) 133 | }; 134 | let vens = if vens.is_empty() { None } else { Some(vens) }; 135 | 136 | trace!(?targets, ?vens); 137 | (targets, vens) 138 | } else { 139 | (None, None) 140 | } 141 | } 142 | 143 | fn extract_business_id(user: &Claims) -> Result, AppError> { 144 | match user.business_ids() { 145 | BusinessIds::Specific(ids) => { 146 | if ids.len() == 1 { 147 | Ok(Some(ids[0].clone())) 148 | } else { 149 | Err(AppError::BadRequest("Cannot infer business id from user"))? 150 | } 151 | } 152 | BusinessIds::Any => Ok(None), 153 | } 154 | } 155 | 156 | fn extract_business_ids(user: &Claims) -> Option> { 157 | match user.business_ids() { 158 | BusinessIds::Specific(ids) => Some(ids), 159 | BusinessIds::Any => None, 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /openleadr-vtn/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | pub mod data_source; 3 | mod error; 4 | pub mod jwt; 5 | pub mod state; 6 | -------------------------------------------------------------------------------- /openleadr-vtn/src/main.rs: -------------------------------------------------------------------------------- 1 | use tokio::{net::TcpListener, signal}; 2 | use tracing::{error, info, warn}; 3 | use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; 4 | 5 | #[cfg(feature = "postgres")] 6 | use openleadr_vtn::data_source::PostgresStorage; 7 | use openleadr_vtn::{data_source::Migrate, state::AppState}; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | tracing_subscriber::registry() 12 | .with(fmt::layer().with_file(true).with_line_number(true)) 13 | .with(EnvFilter::from_default_env()) 14 | .init(); 15 | 16 | let addr = "0.0.0.0:3000"; 17 | let listener = TcpListener::bind(addr).await.unwrap(); 18 | info!("listening on http://{}", listener.local_addr().unwrap()); 19 | 20 | #[cfg(feature = "postgres")] 21 | let storage = PostgresStorage::from_env().await.unwrap(); 22 | 23 | #[cfg(not(feature = "postgres"))] 24 | compile_error!( 25 | "No storage backend selected. Please enable the `postgres` feature flag during compilation" 26 | ); 27 | 28 | if let Err(e) = storage.migrate().await { 29 | warn!("Database migration failed: {}", e); 30 | } 31 | 32 | let state = AppState::new(storage).await; 33 | if let Err(e) = axum::serve(listener, state.into_router()) 34 | .with_graceful_shutdown(shutdown_signal()) 35 | .await 36 | { 37 | error!("webserver crashed: {}", e); 38 | } 39 | } 40 | 41 | async fn shutdown_signal() { 42 | let ctrl_c = async { 43 | signal::ctrl_c() 44 | .await 45 | .expect("failed to install Ctrl+C handler"); 46 | }; 47 | 48 | #[cfg(unix)] 49 | let terminate = async { 50 | signal::unix::signal(signal::unix::SignalKind::terminate()) 51 | .expect("failed to install signal handler") 52 | .recv() 53 | .await; 54 | }; 55 | 56 | #[cfg(not(unix))] 57 | let terminate = std::future::pending::<()>(); 58 | 59 | tokio::select! { 60 | _ = ctrl_c => {}, 61 | _ = terminate => {}, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /openleadr-vtn/tests/assets/public-rsa.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArMesPLlgYK4E90MwDl9E 3 | 3InByTtCXQrQBg/wN8C3KpwP1QyOxGMcnQsNSJP2iCZe1dD3m6gTQ9SeFyzU0Ws3 4 | FeAewvjZULE1uEI77OJrrT/8f7T3PjKlAVdknFPXFqWgUT/gJclbHSfoXIR+33Ii 5 | s2SGcp761IaMT4apXMDD5xUf6qnIjVVumlhsYMc0Gf+jhv9rwWN7ywBrkWTRZCNa 6 | gM3gXunJs+5acztlMCHEsImCofQUsEyo2t+AoZ8O0XAQtJSWf2kbcz+XP94stJ/4 7 | 3Il9xDXZppoHPL/hi5pNdbBmMEzrC6z+z07FdKo37pjBlu7eNzHUcssUaRmhQZXd 8 | HQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /openleadr-wire/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openleadr-wire" 3 | description = "Encode and decode OpenADR 3.0 messages that go over the wire" 4 | readme = "../README.md" 5 | version.workspace = true 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | homepage.workspace = true 10 | publish.workspace = true 11 | rust-version.workspace = true 12 | keywords.workspace = true 13 | 14 | [dependencies] 15 | serde.workspace = true 16 | chrono.workspace = true 17 | serde_with.workspace = true 18 | iso8601-duration.workspace = true 19 | thiserror.workspace = true 20 | http.workspace = true 21 | validator.workspace = true 22 | iso_currency.workspace = true 23 | 24 | [dev-dependencies] 25 | serde_json.workspace = true 26 | quickcheck.workspace = true 27 | -------------------------------------------------------------------------------- /openleadr-wire/src/interval.rs: -------------------------------------------------------------------------------- 1 | //! Descriptions of temporal periods 2 | 3 | use crate::{values_map::ValuesMap, Duration}; 4 | use chrono::{DateTime, Utc}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_with::skip_serializing_none; 7 | 8 | /// An object defining a temporal window and a list of valuesMaps. if intervalPeriod present may set 9 | /// temporal aspects of interval or override event.intervalPeriod. 10 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct Interval { 13 | /// A client generated number assigned an interval object. Not a sequence number. 14 | pub id: i32, 15 | /// Defines default start and durations of intervals. 16 | #[serde(skip_serializing_if = "Option::is_none")] 17 | pub interval_period: Option, 18 | /// A list of valuesMap objects. 19 | pub payloads: Vec, 20 | } 21 | 22 | impl Interval { 23 | pub fn new(id: i32, payloads: Vec) -> Self { 24 | Self { 25 | id, 26 | interval_period: None, 27 | payloads, 28 | } 29 | } 30 | } 31 | 32 | /// Defines temporal aspects of intervals. A duration of default null indicates infinity. A 33 | /// randomizeStart of default null indicates no randomization. 34 | #[skip_serializing_none] 35 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct IntervalPeriod { 38 | /// The start time of an interval or set of intervals. 39 | #[serde(with = "crate::serde_rfc3339")] 40 | pub start: DateTime, 41 | /// The duration of an interval or set of intervals. 42 | pub duration: Option, 43 | /// Indicates a randomization time that may be applied to start. 44 | pub randomize_start: Option, 45 | } 46 | 47 | impl IntervalPeriod { 48 | pub fn new(start: DateTime) -> Self { 49 | Self { 50 | start, 51 | duration: None, 52 | randomize_start: None, 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /openleadr-wire/src/oauth.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 2 | #[serde(rename_all = "snake_case")] 3 | pub enum OAuthErrorType { 4 | OAuthNotEnabled, 5 | InvalidRequest, 6 | InvalidClient, 7 | InvalidGrant, 8 | // UnauthorizedClient, 9 | UnsupportedGrantType, 10 | // InvalidScope, 11 | ServerError, 12 | NoAvailableKeys, 13 | } 14 | 15 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 16 | pub struct OAuthError { 17 | pub error: OAuthErrorType, 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub error_description: Option, 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | pub error_uri: Option, 22 | } 23 | 24 | impl OAuthError { 25 | pub fn new(error: OAuthErrorType) -> Self { 26 | Self { 27 | error, 28 | error_description: None, 29 | error_uri: None, 30 | } 31 | } 32 | 33 | pub fn with_description(mut self, description: String) -> Self { 34 | self.error_description = Some(description); 35 | self 36 | } 37 | 38 | pub fn with_uri(mut self, uri: String) -> Self { 39 | self.error_uri = Some(uri); 40 | self 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /openleadr-wire/src/problem.rs: -------------------------------------------------------------------------------- 1 | use http::StatusCode; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_with::skip_serializing_none; 4 | 5 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] 6 | pub struct ProblemUri(String); 7 | 8 | impl Default for ProblemUri { 9 | fn default() -> Self { 10 | Self("about:blank".to_string()) 11 | } 12 | } 13 | 14 | /// Reusable error response. From . 15 | #[skip_serializing_none] 16 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] 17 | #[serde(rename_all = "camelCase")] 18 | pub struct Problem { 19 | /// An absolute URI that identifies the problem type. 20 | /// When dereferenced, it SHOULD provide human-readable documentation for the problem type 21 | /// (e.g., using HTML). 22 | #[serde(default)] 23 | pub r#type: ProblemUri, 24 | /// A short, summary of the problem type. 25 | /// Written in english and readable for engineers 26 | /// (usually not suited for non-technical stakeholders and not localized); 27 | /// example: Service Unavailable. 28 | pub title: Option, 29 | /// The HTTP status code generated by the origin server for this occurrence of the problem. 30 | #[serde(with = "status_code_serialization")] 31 | pub status: StatusCode, 32 | /// A human-readable explanation specific to this occurrence of the problem. 33 | pub detail: Option, 34 | /// An absolute URI that identifies the specific occurrence of the problem. 35 | /// It may or may not yield further information if dereferenced. 36 | pub instance: Option, 37 | } 38 | 39 | mod status_code_serialization { 40 | use super::*; 41 | 42 | use serde::{de::Unexpected, Deserializer, Serializer}; 43 | 44 | pub fn serialize(code: &StatusCode, serializer: S) -> Result 45 | where 46 | S: Serializer, 47 | { 48 | serializer.serialize_u16(code.as_u16()) 49 | } 50 | 51 | pub fn deserialize<'de, D>(deserializer: D) -> Result 52 | where 53 | D: Deserializer<'de>, 54 | { 55 | u16::deserialize(deserializer).and_then(|code| { 56 | StatusCode::from_u16(code).map_err(|_| { 57 | serde::de::Error::invalid_value( 58 | Unexpected::Unsigned(code as u64), 59 | &"Valid http status code", 60 | ) 61 | }) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /openleadr-wire/src/resource.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_with::skip_serializing_none; 4 | use std::{fmt::Display, str::FromStr}; 5 | use validator::Validate; 6 | 7 | use crate::{target::TargetMap, values_map::ValuesMap, ven::VenId, Identifier, IdentifierError}; 8 | 9 | /// A resource is an energy device or system subject to control by a VEN. 10 | #[skip_serializing_none] 11 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct Resource { 14 | /// URL safe VTN assigned object ID. 15 | pub id: ResourceId, 16 | /// datetime in ISO 8601 format 17 | #[serde(with = "crate::serde_rfc3339")] 18 | pub created_date_time: DateTime, 19 | /// datetime in ISO 8601 format 20 | #[serde(with = "crate::serde_rfc3339")] 21 | pub modification_date_time: DateTime, 22 | /// URL safe VTN assigned object ID. 23 | #[serde(rename = "venID")] 24 | pub ven_id: VenId, 25 | #[serde(flatten)] 26 | #[validate(nested)] 27 | pub content: ResourceContent, 28 | } 29 | 30 | #[skip_serializing_none] 31 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)] 32 | #[serde(rename_all = "camelCase", tag = "objectType", rename = "RESOURCE")] 33 | pub struct ResourceContent { 34 | /// User generated identifier, resource may be configured with identifier out-of-band. 35 | #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")] 36 | pub resource_name: String, 37 | /// A list of valuesMap objects describing attributes. 38 | pub attributes: Option>, 39 | /// A list of valuesMap objects describing target criteria. 40 | pub targets: Option, 41 | } 42 | 43 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash, Eq)] 44 | pub struct ResourceId(pub(crate) Identifier); 45 | 46 | impl Display for ResourceId { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | write!(f, "{}", self.0) 49 | } 50 | } 51 | 52 | impl ResourceId { 53 | pub fn as_str(&self) -> &str { 54 | self.0.as_str() 55 | } 56 | 57 | pub fn new(identifier: &str) -> Option { 58 | Some(Self(identifier.parse().ok()?)) 59 | } 60 | } 61 | 62 | impl FromStr for ResourceId { 63 | type Err = IdentifierError; 64 | 65 | fn from_str(s: &str) -> Result { 66 | Ok(Self(s.parse()?)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /openleadr-wire/src/target.rs: -------------------------------------------------------------------------------- 1 | //! Types to filter resources 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::fmt::{Display, Formatter}; 5 | 6 | #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] 7 | pub struct TargetMap(pub Vec); 8 | 9 | // TODO: Handle strong typing of values 10 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 11 | pub struct TargetEntry { 12 | #[serde(rename = "type")] 13 | pub label: TargetType, 14 | pub values: Vec, 15 | } 16 | 17 | #[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)] 18 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 19 | pub enum TargetType { 20 | /// A Power Service Location is a utility named specific location in 21 | /// geography or the distribution system, usually the point of service to a 22 | /// customer site. 23 | PowerServiceLocation, 24 | /// A Service Area is a utility named geographic region. 25 | ServiceArea, 26 | /// Targeting a specific group (string). 27 | Group, 28 | /// Targeting a specific resource (string). 29 | ResourceName, 30 | /// Targeting a specific VEN (string). 31 | #[serde(rename = "VEN_NAME")] 32 | VENName, 33 | /// Targeting a specific event (string). 34 | EventName, 35 | /// Targeting a specific program (string). 36 | ProgramName, 37 | /// An application specific privately defined target. 38 | #[serde(untagged)] 39 | #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")] 40 | Private(String), 41 | } 42 | 43 | impl TargetType { 44 | pub fn as_str(&self) -> &str { 45 | match self { 46 | TargetType::PowerServiceLocation => "POWER_SERVICE_LOCATION", 47 | TargetType::ServiceArea => "SERVICE_AREA", 48 | TargetType::Group => "GROUP", 49 | TargetType::ResourceName => "RESOURCE_NAME", 50 | TargetType::VENName => "VEN_NAME", 51 | TargetType::EventName => "EVENT_NAME", 52 | TargetType::ProgramName => "PROGRAM_NAME", 53 | TargetType::Private(s) => s.as_str(), 54 | } 55 | } 56 | } 57 | 58 | impl Display for TargetType { 59 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 60 | f.write_str(self.as_str()) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | 68 | #[test] 69 | fn test_target_serialization() { 70 | assert_eq!( 71 | serde_json::to_string(&TargetType::EventName).unwrap(), 72 | r#""EVENT_NAME""# 73 | ); 74 | assert_eq!( 75 | serde_json::to_string(&TargetType::Private(String::from("something else"))).unwrap(), 76 | r#""something else""# 77 | ); 78 | assert_eq!( 79 | serde_json::from_str::(r#""VEN_NAME""#).unwrap(), 80 | TargetType::VENName 81 | ); 82 | assert_eq!( 83 | serde_json::from_str::(r#""something else""#).unwrap(), 84 | TargetType::Private(String::from("something else")) 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /openleadr-wire/src/values_map.rs: -------------------------------------------------------------------------------- 1 | //! Helper types to realize type values relations 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// ValuesMap : Represents one or more values associated with a type. E.g. a type of PRICE contains a single float value. 6 | 7 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 8 | pub struct ValuesMap { 9 | /// Enumerated or private string signifying the nature of values. E.G. \"PRICE\" indicates value is to be interpreted as a currency. 10 | #[serde(rename = "type")] 11 | pub value_type: ValueType, 12 | /// A list of data points. Most often a singular value such as a price. 13 | pub values: Vec, 14 | } 15 | 16 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 17 | pub struct ValueType( 18 | #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")] pub String, 19 | ); 20 | 21 | #[derive(Clone, Debug, Serialize, Deserialize)] 22 | #[serde(untagged)] 23 | pub enum Value { 24 | Integer(i64), 25 | Number(f64), 26 | Boolean(bool), 27 | Point(Point), 28 | String(String), 29 | } 30 | 31 | impl PartialEq for Value { 32 | fn eq(&self, other: &Self) -> bool { 33 | match (self, other) { 34 | (Self::Integer(s), Self::Integer(o)) => s == o, 35 | (Self::Boolean(s), Self::Boolean(o)) => s == o, 36 | (Self::Point(s), Self::Point(o)) => s == o, 37 | (Self::String(s), Self::String(o)) => s == o, 38 | (Self::Number(s), Self::Number(o)) if s.is_nan() && o.is_nan() => true, 39 | (Self::Number(s), Self::Number(o)) => s == o, 40 | _ => false, 41 | } 42 | } 43 | } 44 | 45 | impl Eq for Value {} 46 | 47 | #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 48 | pub struct Point { 49 | /// A value on an x axis. 50 | pub x: f32, 51 | /// A value on a y axis. 52 | pub y: f32, 53 | } 54 | 55 | impl Point { 56 | pub fn new(x: f32, y: f32) -> Self { 57 | Self { x, y } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /openleadr-wire/src/ven.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_with::skip_serializing_none; 4 | use std::{fmt::Display, str::FromStr}; 5 | use validator::Validate; 6 | 7 | use crate::{ 8 | resource::Resource, target::TargetMap, values_map::ValuesMap, Identifier, IdentifierError, 9 | }; 10 | 11 | /// Ven represents a client with the ven role. 12 | #[skip_serializing_none] 13 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)] 14 | #[serde(rename_all = "camelCase")] 15 | pub struct Ven { 16 | /// URL safe VTN assigned object ID. 17 | pub id: VenId, 18 | /// datetime in ISO 8601 format 19 | #[serde(with = "crate::serde_rfc3339")] 20 | pub created_date_time: DateTime, 21 | /// datetime in ISO 8601 format 22 | #[serde(with = "crate::serde_rfc3339")] 23 | pub modification_date_time: DateTime, 24 | 25 | #[serde(flatten)] 26 | #[validate(nested)] 27 | pub content: VenContent, 28 | } 29 | 30 | #[skip_serializing_none] 31 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate)] 32 | #[serde(rename_all = "camelCase", tag = "objectType", rename = "VEN")] 33 | pub struct VenContent { 34 | /// User generated identifier, may be VEN identifier provisioned during program enrollment. 35 | #[serde(deserialize_with = "crate::string_within_range_inclusive::<1, 128, _>")] 36 | pub ven_name: String, 37 | /// A list of valuesMap objects describing attributes. 38 | pub attributes: Option>, 39 | /// A list of valuesMap objects describing target criteria. 40 | pub targets: Option, 41 | /// A list of resource objects representing end-devices or systems. 42 | resources: Option>, 43 | } 44 | 45 | impl VenContent { 46 | pub fn new( 47 | ven_name: String, 48 | attributes: Option>, 49 | targets: Option, 50 | resources: Option>, 51 | ) -> Self { 52 | Self { 53 | ven_name, 54 | attributes, 55 | targets, 56 | resources, 57 | } 58 | } 59 | 60 | pub fn resources(&self) -> Option<&[Resource]> { 61 | self.resources.as_deref() 62 | } 63 | } 64 | 65 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Hash, Eq, PartialOrd, Ord)] 66 | pub struct VenId(pub(crate) Identifier); 67 | 68 | impl Display for VenId { 69 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 70 | write!(f, "{}", self.0) 71 | } 72 | } 73 | 74 | impl VenId { 75 | pub fn as_str(&self) -> &str { 76 | self.0.as_str() 77 | } 78 | 79 | pub fn new(identifier: &str) -> Option { 80 | Some(Self(identifier.parse().ok()?)) 81 | } 82 | } 83 | 84 | impl FromStr for VenId { 85 | type Err = IdentifierError; 86 | 87 | fn from_str(s: &str) -> Result { 88 | Ok(Self(s.parse()?)) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/dyn-price.oadr.yaml: -------------------------------------------------------------------------------- 1 | programs: 2 | - id: dp101 3 | programName: Dynamic Pricing 101 4 | 5 | events: 6 | - id: dp101-e0 7 | programID: dp101 8 | payloadDescriptors: 9 | - payloadType: PRICE 10 | currency: EUR 11 | units: KWH 12 | intervalPeriod: 13 | start: 2024-01-01T00:00Z 14 | duration: PT1H 15 | intervals: 16 | - id: 0 17 | payloads: 18 | - type: PRICE 19 | values: [ 0.42 ] 20 | - id: 1 21 | payloads: 22 | - type: PRICE 23 | values: [ 0.43 ] 24 | - id: 2 25 | payloads: 26 | - type: PRICE 27 | values: [ 0.44 ] 28 | - id: 3 29 | payloads: 30 | - type: PRICE 31 | values: [ 0.45 ] 32 | - id: 4 33 | payloads: 34 | - type: PRICE 35 | values: [ 0.46 ] 36 | - id: 5 37 | payloads: 38 | - type: PRICE 39 | values: [ 0.47 ] 40 | - id: 6 41 | payloads: 42 | - type: PRICE 43 | values: [ 0.48 ] 44 | - id: 7 45 | payloads: 46 | - type: PRICE 47 | values: [ 0.49 ] 48 | - id: 8 49 | payloads: 50 | - type: PRICE 51 | values: [ 0.50 ] 52 | - id: 9 53 | payloads: 54 | - type: PRICE 55 | values: [ 0.49 ] 56 | - id: 10 57 | payloads: 58 | - type: PRICE 59 | values: [ 0.48 ] 60 | - id: 11 61 | payloads: 62 | - type: PRICE 63 | values: [ 0.47 ] 64 | - id: 12 65 | payloads: 66 | - type: PRICE 67 | values: [ 0.46 ] 68 | - id: 13 69 | payloads: 70 | - type: PRICE 71 | values: [ 0.45 ] 72 | - id: 14 73 | payloads: 74 | - type: PRICE 75 | values: [ 0.44 ] 76 | - id: 15 77 | payloads: 78 | - type: PRICE 79 | values: [ 0.43 ] 80 | - id: 16 81 | payloads: 82 | - type: PRICE 83 | values: [ 0.42 ] 84 | - id: 17 85 | payloads: 86 | - type: PRICE 87 | values: [ 0.41 ] 88 | - id: 18 89 | payloads: 90 | - type: PRICE 91 | values: [ 0.40 ] 92 | - id: 19 93 | payloads: 94 | - type: PRICE 95 | values: [ 0.41 ] 96 | - id: 20 97 | payloads: 98 | - type: PRICE 99 | values: [ 0.42 ] 100 | - id: 21 101 | payloads: 102 | - type: PRICE 103 | values: [ 0.43 ] 104 | - id: 22 105 | payloads: 106 | - type: PRICE 107 | values: [ 0.44 ] 108 | - id: 23 109 | payloads: 110 | - type: PRICE 111 | values: [ 0.45 ] 112 | - id: dp101-e1 113 | programID: dp101 114 | payloadDescriptors: 115 | - payloadType: PRICE 116 | currency: EUR 117 | units: KWH 118 | intervalPeriod: 119 | start: 2024-01-02T00:00Z 120 | duration: PT1H 121 | intervals: 122 | - id: 0 123 | payloads: 124 | - type: PRICE 125 | values: [ 0.42 ] 126 | - id: 1 127 | payloads: 128 | - type: PRICE 129 | values: [ 0.43 ] 130 | - id: 2 131 | payloads: 132 | - type: PRICE 133 | values: [ 0.44 ] 134 | - id: 3 135 | payloads: 136 | - type: PRICE 137 | values: [ 0.45 ] 138 | - id: 4 139 | payloads: 140 | - type: PRICE 141 | values: [ 0.46 ] 142 | - id: 5 143 | payloads: 144 | - type: PRICE 145 | values: [ 0.47 ] 146 | - id: 6 147 | payloads: 148 | - type: PRICE 149 | values: [ 0.48 ] 150 | - id: 7 151 | payloads: 152 | - type: PRICE 153 | values: [ 0.49 ] 154 | - id: 8 155 | payloads: 156 | - type: PRICE 157 | values: [ 0.50 ] 158 | - id: 9 159 | payloads: 160 | - type: PRICE 161 | values: [ 0.49 ] 162 | - id: 10 163 | payloads: 164 | - type: PRICE 165 | values: [ 0.48 ] 166 | - id: 11 167 | payloads: 168 | - type: PRICE 169 | values: [ 0.47 ] 170 | - id: 12 171 | payloads: 172 | - type: PRICE 173 | values: [ 0.46 ] 174 | - id: 13 175 | payloads: 176 | - type: PRICE 177 | values: [ 0.45 ] 178 | - id: 14 179 | payloads: 180 | - type: PRICE 181 | values: [ 0.44 ] 182 | - id: 15 183 | payloads: 184 | - type: PRICE 185 | values: [ 0.43 ] 186 | - id: 16 187 | payloads: 188 | - type: PRICE 189 | values: [ 0.42 ] 190 | - id: 17 191 | payloads: 192 | - type: PRICE 193 | values: [ 0.41 ] 194 | - id: 18 195 | payloads: 196 | - type: PRICE 197 | values: [ 0.40 ] 198 | - id: 19 199 | payloads: 200 | - type: PRICE 201 | values: [ 0.41 ] 202 | - id: 20 203 | payloads: 204 | - type: PRICE 205 | values: [ 0.42 ] 206 | - id: 21 207 | payloads: 208 | - type: PRICE 209 | values: [ 0.43 ] 210 | - id: 22 211 | payloads: 212 | - type: PRICE 213 | values: [ 0.44 ] 214 | - id: 23 215 | payloads: 216 | - type: PRICE 217 | values: [ 0.45 ] -------------------------------------------------------------------------------- /tests/load-sched.oadr.yaml: -------------------------------------------------------------------------------- 1 | # Based on User Guide 8.2 2 | 3 | programs: 4 | - id: ls101 5 | programName: Load Shedding 101 6 | events: 7 | - id: ls101-e0 8 | programID: ls101 9 | intervalPeriod: 10 | start: 2024-01-01T13:37Z 11 | duration: PT4H 12 | payloadDescriptors: 13 | - payloadType: SIMPLE 14 | intervals: 15 | - id: 0 16 | payloads: 17 | - type: SIMPLE 18 | values: [ 1 ] 19 | -------------------------------------------------------------------------------- /tests/state-of-charge.oadr.yaml: -------------------------------------------------------------------------------- 1 | # Based on User Guide 8.6 2 | programs: 3 | - id: sof101 4 | programName: State of Charge 101 5 | 6 | events: 7 | - id: sof101-e1 8 | programID: sof101 9 | intervalPeriod: 10 | start: 2024-01-01T00:00Z 11 | reportDescriptors: 12 | - payloadType: STORAGE_USABLE_CAPACITY 13 | units: KWH 14 | - payloadType: STORAGE_CHARGE_LEVEL 15 | units: PERCENT 16 | - payloadType: STORAGE_MAX_DISCHARGE_POWER 17 | units: KW 18 | - payloadType: STORAGE_MAX_CHARGE_POWER 19 | units: KW 20 | intervals: [ ] 21 | 22 | reports: 23 | - reportName: State of Charge 24 | programID: sof101 25 | eventID: sod101-e1 26 | clientName: bat0 27 | resources: 28 | - resourceName: AGGREGATED_REPORT 29 | intervalPeriod: 30 | start: 2024-01-01T00:00Z 31 | duration: PT0S 32 | intervals: 33 | - id: 0 34 | payloads: 35 | - type: STORAGE_USABLE_CAPACITY 36 | values: [ 100 ] 37 | - type: STORAGE_CHARGE_LEVEL 38 | values: [ 42 ] 39 | - type: STORAGE_MAX_DISCHARGE_POWER 40 | values: [ 25 ] 41 | - type: STORAGE_MAX_CHARGE_POWER 42 | values: [ 15 ] -------------------------------------------------------------------------------- /vtn.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.85-alpine AS base 2 | 3 | # Install build dependencies 4 | RUN apk add --no-cache alpine-sdk openssl-dev openssl-libs-static 5 | 6 | FROM base AS builder 7 | 8 | ADD . /app 9 | WORKDIR /app 10 | COPY . . 11 | 12 | # Don't depend on live sqlx during build use cached .sqlx 13 | RUN SQLX_OFFLINE=true cargo build --release --bin openleadr-vtn 14 | RUN cp /app/target/release/openleadr-vtn /app/openleadr-vtn 15 | 16 | FROM alpine:latest AS final 17 | 18 | # Install OpenSSL 19 | RUN apk add --no-cache openssl-libs-static curl 20 | 21 | # create a non root user to run the binary 22 | ARG user=nonroot 23 | ARG group=nonroot 24 | ARG uid=2000 25 | ARG gid=2000 26 | RUN addgroup -g ${gid} ${group} && \ 27 | adduser -u ${uid} -G ${group} -s /bin/sh -D ${user} 28 | 29 | EXPOSE 3000 30 | 31 | # get the pre-built binary from builder so that we don't have to re-build every time 32 | COPY --from=1 --chown=nonroot:nonroot /app/openleadr-vtn/openleadr-vtn /home/nonroot/openleadr-vtn 33 | RUN chmod 777 /home/nonroot/openleadr-vtn 34 | 35 | USER $user 36 | 37 | ENTRYPOINT ["./home/nonroot/openleadr-vtn"] 38 | --------------------------------------------------------------------------------