diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 570cb2e4..be227cb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,11 +2,6 @@ name: Continuous integration on: workflow_dispatch: - push: - branches: - - main - paths: - - "**.rs" pull_request: merge_group: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 00000000..4abe56c3 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,183 @@ +name: Continuous integration + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + check: + name: check + runs-on: ubuntu-latest + steps: + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + + - name: Checkout Sources + uses: actions/checkout@v3 + + - name: Setup cache + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-make + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-make + + - name: Run check + run: cargo make check + + clippy: + name: clippy + runs-on: ubuntu-latest + steps: + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + components: clippy + profile: minimal + + - name: Checkout Sources + uses: actions/checkout@v3 + + - name: Setup cache + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-make + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-make + + - name: Run clippy + run: cargo make clippy + + test: + needs: [ clippy, check ] + name: tests + runs-on: ubuntu-latest + steps: + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + profile: minimal + + - name: Checkout Sources + uses: actions/checkout@v3 + + - name: Setup cache + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-make + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-make + + - name: Tests + run: cargo make test + + docs-lint: + name: lint docs + runs-on: ubuntu-latest + steps: + - uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + + - name: Checkout Sources + uses: actions/checkout@v3 + + - name: Setup cache + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-make + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-make + + - name: Run docs lint + uses: actions-rs/cargo@v1 + with: + command: make + args: docs_lint + + build-docs: + name: build docs + runs-on: ubuntu-latest + steps: + - uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + + - name: Checkout Sources + uses: actions/checkout@v3 + + - name: Setup cache + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-make + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-make + + - name: Build docs + uses: actions-rs/cargo@v1 + with: + command: make + args: docs_build + + - name: Archive artifact + run: | + tar \ + --dereference --hard-dereference \ + -cvf "$RUNNER_TEMP/pages.tar" \ + docs/ + + - name: Upload artifact + id: upload-artifact + uses: actions/upload-artifact@v4 + with: + name: pages + path: ${{ runner.temp }}/pages.tar + retention-days: 1 + if-no-files-found: error + + deploy-docs: + needs: [build-docs] + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }}/nightly + steps: + - name: Deploy to pages + id: deployment + uses: actions/deploy-pages@v4 + with: + artifact_name: "pages" diff --git a/Cargo.toml b/Cargo.toml index b88ff971..021bed09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,8 +3,19 @@ name = "feedback-fusion" version = "0.1.0" edition = "2021" license = "MIT" +default-run = "main" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "main" +path = "src/main.rs" + +[[bin]] +name = "docs" +path = "src/docs.rs" + +[[bin]] +name = "bindings" +path = "src/bindings.rs" [dependencies] aliri = "0.6.2" @@ -35,6 +46,7 @@ tower = { version = "0.4.13", features = ["limit", "buffer"] } tower-http = { version = "0.4.4", features = ["trace"] } tracing = "0.1.39" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +ts-rs = { version = "7.0.0", optional = true } typed-builder = "0.18.0" utoipa = { version = "4.1.0", features = ["yaml", "chrono"] } validator = { version = "0.16", features = ["derive"] } @@ -48,10 +60,11 @@ test-log = "0.2.14" [features] default = ["all-databases"] -docs = [] all-databases = ["postgres", "mysql"] postgres = ["rbdc-pg"] mysql = ["rbdc-mysql"] test = [] +docs = [] +bindings = ["ts-rs"] diff --git a/Makefile.toml b/Makefile.toml index 0fd0016e..aa7af01c 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -12,7 +12,7 @@ args = ["i"] [tasks.docs_generate] dependencies = ["docs_init"] command = "cargo" -args = ["run", "--features", "docs"] +args = ["run", "--bin", "docs", "--features", "docs"] [tasks.docs_preview] dependencies = ["docs_generate"] @@ -21,11 +21,20 @@ args = ["redocly", "preview-docs", "target/openapi.yaml"] [tasks.check] command = "cargo" -args = ["check"] +args = ["check", "--bin", "main"] [tasks.clippy] command = "cargo" -args = ["clippy", "--features", "postgres", "--", "-D", "warnings"] +args = [ + "clippy", + "--bin", + "main", + "--features", + "postgres", + "--", + "-D", + "warnings", +] [tasks.oidc-server-mock] script = "docker compose -f testing/oidc-mock/docker-compose.yaml up -d" @@ -35,15 +44,13 @@ script = "docker compose -f testing/oidc-mock/docker-compose.yaml up -d" script = "docker run --name postgres -e POSTGRES_PASSWORD=password -e POSTGRES_USERNAME=postgres -p 5150:5432 -d postgres && sleep 1" [tasks.postgres_tests] -env = { DATABASE = "POSTGRES", POSTGRES_USERNAME = "postgres", POSTGRES_PASSWORD = "password", POSTGRES_ENDPOINT = "localhost:5150", POSTGRES_DATABASE = "postgres", "OIDC_DISCOVERY_URL" = "http://localhost:5151", OIDC_CLIENT_ID = "client", OIDC_CLIENT_SECRET = "secret", RUST_LOG = "DEBUG", OIDC_SCOPE = "api:feedback-fusion" } +env = { DATABASE = "POSTGRES", POSTGRES_USERNAME = "postgres", POSTGRES_PASSWORD = "password", POSTGRES_ENDPOINT = "localhost:5150", POSTGRES_DATABASE = "postgres", "OIDC_DISCOVERY_URL" = "http://localhost:5151", OIDC_CLIENT_ID = "client", OIDC_CLIENT_SECRET = "secret", OIDC_SCOPE = "api:feedback-fusion" } command = "cargo" args = [ "test", "--no-default-features", "--features", "postgres,test", - "--test", - "http_tests", "--", "--nocapture", "--test-threads=1", @@ -78,4 +85,21 @@ args = [ [tasks.docs_build] dependencies = ["docs_lint"] command = "npx" -args = ["redocly", "build-docs", "target/openapi.yaml", "-o", "docs/index.html"] +args = ["redocly", "build-docs", "target/openapi.yaml", "-o", "docs/rest/index.html"] + +[tasks.generate_bindings] +command = "cargo" +args = [ + "run", + "--bin", + "bindings", + "--no-default-features", + "--features", + "bindings,postgres", +] + +[tasks.export_bindings] +script = '''for f in bindings/*.ts; do [ "$f" != "bindings/index.ts" ] && echo "export * from './$(basename "$f" .ts)';" ; done > bindings/index.ts''' + +[tasks.bindings] +run_task = { name = ["generate_bindings", "export_bindings"] } diff --git a/bindings/CreateFeedbackPromptFieldRequest.ts b/bindings/CreateFeedbackPromptFieldRequest.ts new file mode 100644 index 00000000..8c34a9ab --- /dev/null +++ b/bindings/CreateFeedbackPromptFieldRequest.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FeedbackPromptInputOptions } from "./FeedbackPromptInputOptions"; +import type { FeedbackPromptInputType } from "./FeedbackPromptInputType"; + +export interface CreateFeedbackPromptFieldRequest { title: string, type: FeedbackPromptInputType, options: FeedbackPromptInputOptions, } \ No newline at end of file diff --git a/bindings/CreateFeedbackPromptRequest.ts b/bindings/CreateFeedbackPromptRequest.ts new file mode 100644 index 00000000..8bf886a8 --- /dev/null +++ b/bindings/CreateFeedbackPromptRequest.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface CreateFeedbackPromptRequest { title: string, active: boolean, } \ No newline at end of file diff --git a/bindings/CreateFeedbackTargetRequest.ts b/bindings/CreateFeedbackTargetRequest.ts new file mode 100644 index 00000000..91fe6703 --- /dev/null +++ b/bindings/CreateFeedbackTargetRequest.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface CreateFeedbackTargetRequest { name: string, description: string | null, } \ No newline at end of file diff --git a/bindings/FeedbackPrompt.ts b/bindings/FeedbackPrompt.ts new file mode 100644 index 00000000..f43f71b2 --- /dev/null +++ b/bindings/FeedbackPrompt.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface FeedbackPrompt { id: string, title: string, target: string, active: boolean, updated_at: Date, created_at: Date, } \ No newline at end of file diff --git a/bindings/FeedbackPromptField.ts b/bindings/FeedbackPromptField.ts new file mode 100644 index 00000000..bf80ff39 --- /dev/null +++ b/bindings/FeedbackPromptField.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FeedbackPromptInputOptions } from "./FeedbackPromptInputOptions"; +import type { FeedbackPromptInputType } from "./FeedbackPromptInputType"; + +export interface FeedbackPromptField { id: string, title: string, prompt: string, type: FeedbackPromptInputType, options: FeedbackPromptInputOptions, updated_at: Date, created_at: Date, } \ No newline at end of file diff --git a/bindings/FeedbackPromptFieldData.ts b/bindings/FeedbackPromptFieldData.ts new file mode 100644 index 00000000..b23860fd --- /dev/null +++ b/bindings/FeedbackPromptFieldData.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RatingResponse } from "./RatingResponse"; +import type { TextResponse } from "./TextResponse"; + +export type FeedbackPromptFieldData = TextResponse | RatingResponse; \ No newline at end of file diff --git a/bindings/FeedbackPromptFieldResponse.ts b/bindings/FeedbackPromptFieldResponse.ts new file mode 100644 index 00000000..873b63f7 --- /dev/null +++ b/bindings/FeedbackPromptFieldResponse.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FeedbackPromptFieldData } from "./FeedbackPromptFieldData"; + +export interface FeedbackPromptFieldResponse { id: string, response: string, field: string, data: FeedbackPromptFieldData, } \ No newline at end of file diff --git a/bindings/FeedbackPromptInputOptions.ts b/bindings/FeedbackPromptInputOptions.ts new file mode 100644 index 00000000..2ce99d95 --- /dev/null +++ b/bindings/FeedbackPromptInputOptions.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { RatingOptions } from "./RatingOptions"; +import type { TextOptions } from "./TextOptions"; + +export type FeedbackPromptInputOptions = TextOptions | RatingOptions; \ No newline at end of file diff --git a/bindings/FeedbackPromptInputType.ts b/bindings/FeedbackPromptInputType.ts new file mode 100644 index 00000000..ec98c973 --- /dev/null +++ b/bindings/FeedbackPromptInputType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type FeedbackPromptInputType = "text" | "rating"; \ No newline at end of file diff --git a/bindings/FeedbackPromptResponse.ts b/bindings/FeedbackPromptResponse.ts new file mode 100644 index 00000000..20ff4821 --- /dev/null +++ b/bindings/FeedbackPromptResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface FeedbackPromptResponse { id: string, prompt: string, created_at: Date, } \ No newline at end of file diff --git a/bindings/FeedbackTarget.ts b/bindings/FeedbackTarget.ts new file mode 100644 index 00000000..4c4e7db6 --- /dev/null +++ b/bindings/FeedbackTarget.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface FeedbackTarget { id: string, name: string, description: string | null, updated_at: Date, created_at: Date, } \ No newline at end of file diff --git a/bindings/GetFeedbackPromptResponsesResponseWrapper.ts b/bindings/GetFeedbackPromptResponsesResponseWrapper.ts new file mode 100644 index 00000000..7b119d1f --- /dev/null +++ b/bindings/GetFeedbackPromptResponsesResponseWrapper.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FeedbackPromptFieldResponse } from "./FeedbackPromptFieldResponse"; + +export type GetFeedbackPromptResponsesResponseWrapper = Record>; \ No newline at end of file diff --git a/bindings/Page.ts b/bindings/Page.ts new file mode 100644 index 00000000..200d55e4 --- /dev/null +++ b/bindings/Page.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FeedbackPromptField } from "./FeedbackPromptField"; + +export interface Page { records: Array, total: number, page_no: number, } \ No newline at end of file diff --git a/bindings/PutFeedbackPromptFieldRequest.ts b/bindings/PutFeedbackPromptFieldRequest.ts new file mode 100644 index 00000000..bbe80555 --- /dev/null +++ b/bindings/PutFeedbackPromptFieldRequest.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FeedbackPromptInputOptions } from "./FeedbackPromptInputOptions"; + +export interface PutFeedbackPromptFieldRequest { title: string | null, options: FeedbackPromptInputOptions | null, } \ No newline at end of file diff --git a/bindings/PutFeedbackPromptRequest.ts b/bindings/PutFeedbackPromptRequest.ts new file mode 100644 index 00000000..f401b179 --- /dev/null +++ b/bindings/PutFeedbackPromptRequest.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface PutFeedbackPromptRequest { title: string | null, active: boolean | null, } \ No newline at end of file diff --git a/bindings/PutFeedbackTargetRequest.ts b/bindings/PutFeedbackTargetRequest.ts new file mode 100644 index 00000000..15247839 --- /dev/null +++ b/bindings/PutFeedbackTargetRequest.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface PutFeedbackTargetRequest { name: string | null, description: string | null, } \ No newline at end of file diff --git a/bindings/RatingOptions.ts b/bindings/RatingOptions.ts new file mode 100644 index 00000000..5f1690f5 --- /dev/null +++ b/bindings/RatingOptions.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface RatingOptions { description: string, max: number, } \ No newline at end of file diff --git a/bindings/RatingResponse.ts b/bindings/RatingResponse.ts new file mode 100644 index 00000000..f9f61fc7 --- /dev/null +++ b/bindings/RatingResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface RatingResponse { data: number, } \ No newline at end of file diff --git a/bindings/SubmitFeedbackPromptResponseRequest.ts b/bindings/SubmitFeedbackPromptResponseRequest.ts new file mode 100644 index 00000000..79e02bfd --- /dev/null +++ b/bindings/SubmitFeedbackPromptResponseRequest.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FeedbackPromptFieldData } from "./FeedbackPromptFieldData"; + +export interface SubmitFeedbackPromptResponseRequest { responses: Record, } \ No newline at end of file diff --git a/bindings/TextOptions.ts b/bindings/TextOptions.ts new file mode 100644 index 00000000..e658810b --- /dev/null +++ b/bindings/TextOptions.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface TextOptions { description: string, placeholder: string, } \ No newline at end of file diff --git a/bindings/TextResponse.ts b/bindings/TextResponse.ts new file mode 100644 index 00000000..bfdfefe3 --- /dev/null +++ b/bindings/TextResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface TextResponse { data: string, } \ No newline at end of file diff --git a/bindings/index.ts b/bindings/index.ts new file mode 100644 index 00000000..5162dc46 --- /dev/null +++ b/bindings/index.ts @@ -0,0 +1,21 @@ +export * from './CreateFeedbackPromptFieldRequest'; +export * from './CreateFeedbackPromptRequest'; +export * from './CreateFeedbackTargetRequest'; +export * from './FeedbackPrompt'; +export * from './FeedbackPromptField'; +export * from './FeedbackPromptFieldData'; +export * from './FeedbackPromptFieldResponse'; +export * from './FeedbackPromptInputOptions'; +export * from './FeedbackPromptInputType'; +export * from './FeedbackPromptResponse'; +export * from './FeedbackTarget'; +export * from './GetFeedbackPromptResponsesResponseWrapper'; +export * from './Page'; +export * from './PutFeedbackPromptFieldRequest'; +export * from './PutFeedbackPromptRequest'; +export * from './PutFeedbackTargetRequest'; +export * from './RatingOptions'; +export * from './RatingResponse'; +export * from './SubmitFeedbackPromptResponseRequest'; +export * from './TextOptions'; +export * from './TextResponse'; diff --git a/lib/core/.gitignore b/lib/core/.gitignore new file mode 100644 index 00000000..a9180ed0 --- /dev/null +++ b/lib/core/.gitignore @@ -0,0 +1,132 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp. + +dist/ diff --git a/lib/core/package.json b/lib/core/package.json new file mode 100644 index 00000000..a4552102 --- /dev/null +++ b/lib/core/package.json @@ -0,0 +1,36 @@ +{ + "name": "@onelitefeathernet/feedback-fusion-core", + "version": "0.0.0", + "description": "Core lib containing the http client in order to interact with the feedback-fusion backend", + "main": "dist/lib/core/src/index.js", + "types": "dist/lib/core/src/index.d.ts", + "scripts": { + "tsc": "tsc" + }, + "files": [ + "dist/" + ], + "exports": { + ".": { + "types": "./dist/lib/core/src/index.d.ts", + "default": "./dist/lib/core/src/index.js" + }, + "./locales/*": { + "types": "./dist/lib/core/src/locales/*.d.ts", + "default": "./dist/lib/core/src/locales/*.js" + } + }, + "repository": { + "type": "git", + "url": "git+https://github.com/OneLiteFeatherNET/feedback-fusion.git" + }, + "author": "OneLiteFeatherNET", + "license": "MIT", + "bugs": { + "url": "https://github.com/OneLiteFeatherNET/feedback-fusion/issues" + }, + "homepage": "https://github.com/OneLiteFeatherNET/feedback-fusion#readme", + "devDependencies": { + "typescript": "^5.3.3" + } +} diff --git a/lib/core/pnpm-lock.yaml b/lib/core/pnpm-lock.yaml new file mode 100644 index 00000000..99bd229e --- /dev/null +++ b/lib/core/pnpm-lock.yaml @@ -0,0 +1,18 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + typescript: + specifier: ^5.3.3 + version: 5.3.3 + +packages: + + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} + hasBin: true + dev: true diff --git a/lib/core/src/config.ts b/lib/core/src/config.ts new file mode 100644 index 00000000..2647b85d --- /dev/null +++ b/lib/core/src/config.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 OneLiteFeatherNET + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { FeedbackFusionClient } from "./"; +import en from "./locales/en"; + +interface BaseConfig { + baseURL: string; + target: string; +} + +interface LocaleData { + locale: string; + translation: Object[]; +} + +interface ThemeOptions { + [key: string]: { + text: string; + subtitle: string; + sheet: string; + primary: string; + inactive: string; + success: string; + error: string; + }; +} + +export interface FeedbackFusionConfigurationOptions extends BaseConfig { + locales?: LocaleData[]; + defaultLocale?: string; + defaultTheme?: string; + themes?: ThemeOptions; +} + +export interface FeedbackFusionConfig extends BaseConfig { + locales: { [key: string]: { translation: Object } }; + defaultLocale: string; + defaultTheme: string; + themes: ThemeOptions; +} + +const defaultThemes = { + dark: { + text: "#FFFFF5", + subtitle: "#757575", + sheet: "#212121", + primary: "#3498db", + inactive: "#757575", + success: "#4caf50", + error: "#d33d3d", + }, +}; + +export function patchConfig( + config: FeedbackFusionConfigurationOptions, +): FeedbackFusionConfig { + // default themes + config.themes = Object.assign(defaultThemes, config.themes || {}); + config.defaultTheme = config.defaultTheme || "dark"; + + // default locales + if (!config.defaultLocale) { + config.defaultLocale = "en"; + + if (!config.locales?.find((locale: LocaleData) => locale.locale === "en")) { + if (!config.locales) config.locales = []; + // @ts-ignore + config.locales.push(en); + } + } + + // transform the locales + const locales: any = {}; + config.locales!.forEach((locale: LocaleData) => + locales[locale.locale] = { translation: locale.translation } + ); + config.locales = locales; + + // @ts-ignore + return config as FeedbackFusionConfig; +} + +export interface FeedbackFusionState { + config: FeedbackFusionConfig; + client: FeedbackFusionClient; +} diff --git a/lib/core/src/index.ts b/lib/core/src/index.ts new file mode 100644 index 00000000..3d58a0de --- /dev/null +++ b/lib/core/src/index.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 OneLiteFeatherNET + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { + FeedbackPromptResponse, + SubmitFeedbackPromptResponseRequest, + FeedbackPromptField, + Page, + FeedbackPrompt, +} from "../../../bindings"; + +export class FeedbackFusionClient { + public baseURL: string; + + public constructor(baseURL: string, target: String) { + this.baseURL = `${baseURL}/v1/target/${target}`; + } + + /** + * Submit a new response to the prompt + */ + public async submitResponse( + prompt: String, + responses: SubmitFeedbackPromptResponseRequest, + ): Promise { + return await fetch(`${this.baseURL}/prompt/${prompt}/response`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(responses), + }) + .then((response) => response.json() as Promise) + .then((response: FeedbackPromptResponse) => response); + // TODO: handle errors + } + + /** + * Fetch a specific prompt + */ + public async getPrompt(prompt: string): Promise { + return await fetch(`${this.baseURL}/prompt/${prompt}`) + .then((response) => response.json() as Promise) + .then((response: FeedbackPrompt) => response) + } + + /** + * Fetch a page of fields + */ + public async getFields(prompt: string, page = 1, page_size = 20): Promise> { + return await fetch(`${this.baseURL}/prompt/${prompt}/fetch?page=${page}&page_size=${page_size}`) + .then((response) => response.json() as Promise>) + .then((response: Page) => response) + } +} + +export * from "./config"; + +// export bindings generated via ts-rs +export * from "../../../bindings"; diff --git a/lib/core/src/locales/en.ts b/lib/core/src/locales/en.ts new file mode 100644 index 00000000..1213138f --- /dev/null +++ b/lib/core/src/locales/en.ts @@ -0,0 +1,11 @@ +export default { + locale: "en", + translation: { + loading: "Loading...", + page: "page {{ current }} of {{ total }}", + submit: "submit", + finished: "Thank you for participating in our survery!", + close: "close", + error: "An error occurred while processing your request" + }, +}; diff --git a/lib/core/tsconfig.json b/lib/core/tsconfig.json new file mode 100644 index 00000000..5a64472c --- /dev/null +++ b/lib/core/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "baseUrl": ".", + "declaration": true, + "outDir": "./dist", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "moduleResolution": "node" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/lib/vue/.gitignore b/lib/vue/.gitignore new file mode 100644 index 00000000..d078af2a --- /dev/null +++ b/lib/vue/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +!docs/ +docs/.vitepress/cache/ diff --git a/lib/vue/README.md b/lib/vue/README.md new file mode 100644 index 00000000..ef72fd52 --- /dev/null +++ b/lib/vue/README.md @@ -0,0 +1,18 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/lib/vue/src/components/Prompt.vue b/lib/vue/src/components/Prompt.vue new file mode 100644 index 00000000..24939df0 --- /dev/null +++ b/lib/vue/src/components/Prompt.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/lib/vue/src/index.ts b/lib/vue/src/index.ts new file mode 100644 index 00000000..2ad85987 --- /dev/null +++ b/lib/vue/src/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 OneLiteFeatherNET + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { + FeedbackFusionClient, + FeedbackFusionConfigurationOptions, + patchConfig, +} from "@onelitefeathernet/feedback-fusion-core"; +import i18next from "i18next"; +import { App } from "vue"; +import Prompt from "./components/Prompt.vue" + +export const FeedbackFusion = { + install(Vue: App, config: FeedbackFusionConfigurationOptions) { + const patchedConfig = patchConfig(config); + + i18next.init({ + lng: config.defaultLocale, + resources: config.locales as any + }); + + Vue.provide("feedbackFusionState", { + config: patchedConfig, + client: new FeedbackFusionClient(patchedConfig.baseURL, patchedConfig.target), + }); + + Vue.component("FeedbackFusionPrompt", Prompt); + }, +}; diff --git a/lib/vue/tsconfig.json b/lib/vue/tsconfig.json new file mode 100644 index 00000000..26c94bd9 --- /dev/null +++ b/lib/vue/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/lib/vue/tsconfig.node.json b/lib/vue/tsconfig.node.json new file mode 100644 index 00000000..b5a34318 --- /dev/null +++ b/lib/vue/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/lib/vue/vite.config.ts b/lib/vue/vite.config.ts new file mode 100644 index 00000000..f848727e --- /dev/null +++ b/lib/vue/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; +import vue from "@vitejs/plugin-vue"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + build: { + lib: { + entry: resolve(__dirname, "src/index.ts"), + name: "FeedbackFusion", + fileName: "feedback-fusion-vue", + }, + rollupOptions: { + external: ["vue"], + output: { + globals: { + vue: "Vue", + }, + }, + }, + }, +}); diff --git a/src/bindings.rs b/src/bindings.rs new file mode 100644 index 00000000..b45a40a6 --- /dev/null +++ b/src/bindings.rs @@ -0,0 +1,78 @@ +//SPDX-FileCopyrightText: 2024 OneLiteFeatherNet +//SPDX-License-Identifier: MIT + +//MIT License + +// Copyright (c) 2024 OneLiteFeatherNet + +//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +//associated documentation files (the "Software"), to deal in the Software without restriction, +//including without limitation the rights to use, copy, modify, merge, publish, distribute, +//sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: + +//The above copyright notice and this permission notice (including the next paragraph) shall be +//included in all copies or substantial portions of the Software. + +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +//NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +//NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +//DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#[cfg(feature = "bindings")] +use crate::{database::schema::feedback::*, routes::v1::{*, prompt::*, response::*}}; +#[cfg(feature = "bindings")] +use ts_rs::TS; + +pub mod database; +pub mod error; +pub mod routes; +pub mod state; +pub mod config; +pub mod prelude; + +#[cfg(feature = "bindings")] +macro_rules! export { + ($($path:path $(,)?)*) => { + $( + <$path>::export().unwrap(); + )* + }; +} + +#[cfg(feature = "bindings")] +#[derive(serde::Serialize, TS)] +pub struct Page { + records: Vec, + total: u16, + page_no: u16, +} + +pub fn main() { + #[cfg(feature = "bindings")] + export!( + FeedbackTarget, + PutFeedbackTargetRequest, + FeedbackPrompt, + PutFeedbackPromptRequest, + FeedbackPromptField, + PutFeedbackPromptFieldRequest, + FeedbackPromptInputType, + FeedbackPromptField, + FeedbackPromptInputOptions, + TextOptions, + RatingOptions, + FeedbackPromptResponse, + FeedbackPromptFieldResponse, + FeedbackPromptFieldData, + CreateFeedbackTargetRequest, + CreateFeedbackPromptRequest, + CreateFeedbackPromptFieldRequest, + GetFeedbackPromptResponsesResponseWrapper, + SubmitFeedbackPromptResponseRequest, + TextResponse, + RatingResponse, + Page + ); +} diff --git a/src/config.rs b/src/config.rs index eebe8011..0f22d4d7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -21,6 +21,14 @@ * */ +use crate::prelude::*; + +lazy_static! { + pub static ref CONFIG: Config = envy::from_env::().unwrap(); + pub static ref DATABASE_CONFIG: DatabaseConfiguration = + DatabaseConfiguration::extract().unwrap(); +} + #[derive(Deserialize, Debug, Clone, Getters)] #[get = "pub"] pub struct Config { @@ -34,7 +42,7 @@ pub struct Config { #[serde(default = "default_oidc_scope_read")] oidc_scope_read: String, #[serde(default = "default_oidc_audience")] - oidc_audience: String + oidc_audience: String, } #[inline] @@ -42,14 +50,21 @@ fn default_global_rate_limit() -> u64 { 10 } #[inline] -fn default_oidc_scope_admin() -> String { "api:feedback-fusion".to_owned() } +fn default_oidc_scope_admin() -> String { + "api:feedback-fusion".to_owned() +} #[inline] -fn default_oidc_scope_write() -> String { "feedback-fusion:write".to_owned() } +fn default_oidc_scope_write() -> String { + "feedback-fusion:write".to_owned() +} #[inline] -fn default_oidc_scope_read() -> String { "feedback-fusion:read".to_owned() } +fn default_oidc_scope_read() -> String { + "feedback-fusion:read".to_owned() +} #[inline] -fn default_oidc_audience() -> String { "feedback-fusion".to_owned() } - +fn default_oidc_audience() -> String { + "feedback-fusion".to_owned() +} diff --git a/src/database/migration.rs b/src/database/migration.rs index c1d03991..299da181 100644 --- a/src/database/migration.rs +++ b/src/database/migration.rs @@ -21,6 +21,7 @@ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use rbatis::rbdc::DateTime; +use crate::prelude::*; #[derive(Deserialize, Serialize, Clone)] pub struct Migration { diff --git a/src/database/mod.rs b/src/database/mod.rs index 979431d4..22ccb68e 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -163,7 +163,7 @@ macro_rules! impl_select_page_wrapper { let limit = page_request.page_size(); let offset = page_request.offset(); - match $crate::DATABASE_CONFIG.deref() { + match $crate::config::DATABASE_CONFIG.deref() { #[cfg(feature = "postgres")] $crate::database::DatabaseConfiguration::Postgres(_) => Self::$ident(executor, page_request, $($arg,)* format!(" LIMIT {} OFFSET {} ", limit, offset).as_str()).await, #[allow(unreachable_patterns)] diff --git a/src/database/schema/feedback/input.rs b/src/database/schema/feedback/input.rs index 0e661257..df367475 100644 --- a/src/database/schema/feedback/input.rs +++ b/src/database/schema/feedback/input.rs @@ -21,13 +21,14 @@ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use crate::prelude::*; -use rbatis::rbdc::{DateTime, JsonV}; +use rbatis::rbdc::DateTime; use super::FeedbackPromptInputType; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] #[serde(untagged)] #[serde(rename_all = "lowercase")] +#[cfg_attr(feature = "bindings", derive(TS))] pub enum FeedbackPromptInputOptions { Text(TextOptions), Rating(RatingOptions), @@ -45,6 +46,7 @@ impl PartialEq for FeedbackPromptInputType { #[derive(Deserialize, Serialize, Clone, Debug, PartialEq, TypedBuilder, ToSchema, Validate)] #[builder(field_defaults(setter(into)))] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct TextOptions { #[validate(length(max = 255))] description: String, @@ -54,6 +56,7 @@ pub struct TextOptions { #[derive(Deserialize, Serialize, Clone, Debug, PartialEq, TypedBuilder, ToSchema, Validate)] #[builder(field_defaults(setter(into)))] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct RatingOptions { #[validate(length(max = 255))] description: String, @@ -67,12 +70,14 @@ pub struct RatingOptions { #[get = "pub"] #[get_mut = "pub"] #[builder(field_defaults(setter(into)))] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct FeedbackPromptResponse { #[builder(default_code = r#"nanoid::nanoid!()"#)] id: String, prompt: String, #[derivative(PartialEq = "ignore")] #[builder(default)] + #[cfg_attr(feature = "bindings", ts(type = "Date"))] created_at: DateTime, } @@ -85,19 +90,24 @@ impl_select_page_wrapper!(FeedbackPromptResponse {select_page_by_prompt(prompt: #[get = "pub"] #[get_mut = "pub"] #[builder(field_defaults(setter(into)))] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct FeedbackPromptFieldResponse { #[builder(default_code = r#"nanoid::nanoid!()"#)] id: String, response: String, field: String, + #[cfg(not(feature = "bindings"))] #[schema(value_type = FeedbackPromptFieldData)] data: JsonV, + #[cfg(feature = "bindings")] + data: FeedbackPromptFieldData, } crud!(FeedbackPromptFieldResponse {}); #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)] #[serde(untagged)] +#[cfg_attr(feature = "bindings", derive(TS))] pub enum FeedbackPromptFieldData { Text(TextResponse), Rating(RatingResponse), @@ -114,11 +124,13 @@ impl PartialEq for FeedbackPromptInputType { } #[derive(Deserialize, Serialize, Clone, Debug, ToSchema, PartialEq)] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct TextResponse { data: String, } #[derive(Deserialize, Serialize, Clone, Debug, ToSchema, PartialEq)] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct RatingResponse { data: u8, } diff --git a/src/database/schema/feedback/prompt.rs b/src/database/schema/feedback/prompt.rs index 91827506..2d27c2d4 100644 --- a/src/database/schema/feedback/prompt.rs +++ b/src/database/schema/feedback/prompt.rs @@ -21,7 +21,7 @@ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use crate::prelude::*; -use rbatis::rbdc::{DateTime, JsonV}; +use rbatis::rbdc::DateTime; use super::input::FeedbackPromptInputOptions; @@ -41,6 +41,7 @@ use super::input::FeedbackPromptInputOptions; #[get = "pub"] #[set = "pub"] #[builder(field_defaults(setter(into)))] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct FeedbackPrompt { #[builder(default_code = r#"nanoid::nanoid!()"#)] id: String, @@ -51,8 +52,10 @@ pub struct FeedbackPrompt { active: bool, #[derivative(PartialEq = "ignore")] #[builder(default)] + #[cfg_attr(feature = "bindings", ts(type = "Date"))] updated_at: DateTime, #[derivative(PartialEq = "ignore")] + #[cfg_attr(feature = "bindings", ts(type = "Date"))] #[builder(default)] created_at: DateTime, } @@ -63,6 +66,7 @@ impl_select_page_wrapper!(FeedbackPrompt {select_page_by_target(target: &str) => #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, ToSchema)] #[serde(rename_all = "lowercase")] +#[cfg_attr(feature = "bindings", derive(TS))] pub enum FeedbackPromptInputType { Text, Rating, @@ -84,6 +88,7 @@ pub enum FeedbackPromptInputType { #[get = "pub"] #[set = "pub"] #[builder(field_defaults(setter(into)))] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct FeedbackPromptField { #[builder(default_code = r#"nanoid::nanoid!()"#)] id: String, @@ -91,12 +96,17 @@ pub struct FeedbackPromptField { title: String, prompt: String, r#type: FeedbackPromptInputType, + #[cfg(not(feature = "bindings"))] #[schema(value_type = FeedbackPromptInputOptions)] options: JsonV, + #[cfg(feature = "bindings")] + options: FeedbackPromptInputOptions, #[builder(default)] #[derivative(PartialEq = "ignore")] + #[cfg_attr(feature = "bindings", ts(type = "Date"))] updated_at: DateTime, #[derivative(PartialEq = "ignore")] + #[cfg_attr(feature = "bindings", ts(type = "Date"))] #[builder(default)] created_at: DateTime, } diff --git a/src/database/schema/feedback/target.rs b/src/database/schema/feedback/target.rs index 1d0aa85b..e64e5e8a 100644 --- a/src/database/schema/feedback/target.rs +++ b/src/database/schema/feedback/target.rs @@ -28,6 +28,7 @@ use crate::prelude::*; #[get = "pub"] #[set = "pub"] #[builder(field_defaults(setter(into)))] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct FeedbackTarget { #[builder(default_code = r#"nanoid::nanoid!()"#)] id: String, @@ -37,9 +38,11 @@ pub struct FeedbackTarget { description: Option, #[derivative(PartialEq = "ignore")] #[builder(default)] + #[cfg_attr(feature = "bindings", ts(type = "Date"))] updated_at: DateTime, #[derivative(PartialEq = "ignore")] #[builder(default)] + #[cfg_attr(feature = "bindings", ts(type = "Date"))] created_at: DateTime } diff --git a/src/docs.rs b/src/docs.rs index 526bab69..b5725377 100644 --- a/src/docs.rs +++ b/src/docs.rs @@ -25,7 +25,10 @@ use crate::{ routes::v1::{prompt::*, response::*, *}, }; use std::{fs, path::Path}; -use utoipa::{OpenApi, ToSchema, Modify, openapi::security::{SecurityScheme, OpenIdConnect}}; +use utoipa::{ + openapi::security::{OpenIdConnect, SecurityScheme}, + Modify, OpenApi, ToSchema, +}; #[derive(ToSchema)] #[aliases( @@ -34,13 +37,21 @@ use utoipa::{OpenApi, ToSchema, Modify, openapi::security::{SecurityScheme, Open FeedbackPromptFieldPage = Page )] +#[allow(unused)] pub struct Page ToSchema<'a>> { records: Vec, - total: u64, - page_no: u64, + total: u16, + page_no: u16, } -pub fn generate() { +pub mod database; +pub mod error; +pub mod routes; +pub mod state; +pub mod config; +pub mod prelude; + +pub fn main() { #[derive(OpenApi)] #[openapi( paths( @@ -49,6 +60,7 @@ pub fn generate() { put_target, delete_target, post_prompt, + get_prompt, get_prompts, put_prompt, delete_prompt, @@ -104,7 +116,9 @@ pub fn generate() { if let Some(components) = openapi.components.as_mut() { components.add_security_scheme( "oidc", - SecurityScheme::OpenIdConnect(OpenIdConnect::new("https://your-oidc-provider.tld")) + SecurityScheme::OpenIdConnect(OpenIdConnect::new( + "https://your-oidc-provider.tld", + )), ) } } diff --git a/src/error.rs b/src/error.rs index 6849b9f4..4f320b33 100644 --- a/src/error.rs +++ b/src/error.rs @@ -21,10 +21,10 @@ * */ +use crate::prelude::*; use axum::{ http::StatusCode, response::{IntoResponse, Response}, - Json, }; use thiserror::Error; use validator::ValidationErrors; diff --git a/src/main.rs b/src/main.rs index 23bef39f..b52b157c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,97 +21,62 @@ */ #![allow(clippy::too_many_arguments)] -#[macro_use] -extern crate derivative; -#[macro_use] -extern crate getset; -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate paste; -#[macro_use] -extern crate rbatis; -#[macro_use] -extern crate serde; -#[macro_use] -extern crate tracing; -#[macro_use] -extern crate typed_builder; -#[macro_use] -extern crate utoipa; -#[macro_use] -extern crate validator; - -use crate::{config::Config, database::DatabaseConfiguration, prelude::*}; -use axum::{error_handling::HandleErrorLayer, http::StatusCode, BoxError, Router, Server}; +use crate::prelude::*; +use axum::{error_handling::HandleErrorLayer, http::StatusCode, BoxError, Server}; use std::{net::SocketAddr, time::Duration}; use tower::{buffer::BufferLayer, limit::RateLimitLayer, ServiceBuilder}; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -lazy_static! { - pub static ref CONFIG: Config = envy::from_env::().unwrap(); - pub static ref DATABASE_CONFIG: DatabaseConfiguration = - DatabaseConfiguration::extract().unwrap(); -} - pub mod config; pub mod database; pub mod error; +pub mod prelude; pub mod routes; pub mod state; -#[cfg(feature = "docs")] -pub mod docs; - #[tokio::main] async fn main() { - #[cfg(feature = "docs")] - docs::generate(); - - #[cfg(not(feature = "docs"))] - { - // init the tracing subscriber with the `RUST_LOG` env filter - tracing_subscriber::registry() - .with(tracing_subscriber::EnvFilter::from_default_env()) - .with(tracing_subscriber::fmt::layer()) - .init(); - - // init config - lazy_static::initialize(&CONFIG); - lazy_static::initialize(&DATABASE_CONFIG); - - let (sender, receiver) = kanal::oneshot_async::<()>(); - let address = SocketAddr::from(([0, 0, 0, 0], 8000)); - - // connect to the database - let connection = DATABASE_CONFIG.connect().await.unwrap(); - let connection = DatabaseConnection::from(connection); - - tokio::spawn(async move { - Server::bind(&address) - .serve(router(connection).await.into_make_service()) - .with_graceful_shutdown(async move { - receiver.recv().await.ok(); - }) - .await - .unwrap(); - }); - info!("Listening for incoming requests"); - - match tokio::signal::ctrl_c().await { - Ok(()) => {} - Err(error) => { - error!("Unable to listen for the shutdown signal: {}", error); - } + // init the tracing subscriber with the `RUST_LOG` env filter + tracing_subscriber::registry() + .with(tracing_subscriber::EnvFilter::from_default_env()) + .with(tracing_subscriber::fmt::layer()) + .init(); + + // init config + lazy_static::initialize(&CONFIG); + lazy_static::initialize(&DATABASE_CONFIG); + + let (sender, receiver) = kanal::oneshot_async::<()>(); + let address = SocketAddr::from(([0, 0, 0, 0], 8000)); + + // connect to the database + let connection = DATABASE_CONFIG.connect().await.unwrap(); + let connection = DatabaseConnection::from(connection); + + tokio::spawn(async move { + Server::bind(&address) + .serve(router(connection).await.into_make_service()) + .with_graceful_shutdown(async move { + receiver.recv().await.ok(); + }) + .await + .unwrap(); + }); + info!("Listening for incoming requests"); + + match tokio::signal::ctrl_c().await { + Ok(()) => {} + Err(error) => { + error!("Unable to listen for the shutdown signal: {}", error); } - - info!("Received shutdown signal... shutting down..."); - sender.send(()).await.unwrap(); } + + info!("Received shutdown signal... shutting down..."); + sender.send(()).await.unwrap(); } -pub(crate) async fn router(connection: DatabaseConnection) -> Router { +async fn router(connection: DatabaseConnection) -> Router { let state = FeedbackFusionState::new(connection); routes::router(state).await.layer( @@ -128,25 +93,6 @@ pub(crate) async fn router(connection: DatabaseConnection) -> Router { *CONFIG.global_rate_limit(), Duration::from_secs(1), )) - .layer(TraceLayer::new_for_http()) + .layer(TraceLayer::new_for_http()), ) } - -pub mod prelude { - pub use crate::{ - config::*, - database::DatabaseConnection, - database_request, - error::*, - impl_select_page_wrapper, - routes::{oidc::*, *}, - state::FeedbackFusionState, - CONFIG, DATABASE_CONFIG, - }; - pub use axum::{ - extract::{Json, Query, State}, - routing::*, - Router, - }; - pub use rbatis::{plugin::page::Page, rbdc::JsonV, IPageRequest}; -} diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 00000000..8e4a41d4 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,52 @@ +//SPDX-FileCopyrightText: 2024 OneLiteFeatherNet +//SPDX-License-Identifier: MIT + +//MIT License + +// Copyright (c) 2024 OneLiteFeatherNet + +//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +//associated documentation files (the "Software"), to deal in the Software without restriction, +//including without limitation the rights to use, copy, modify, merge, publish, distribute, +//sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +//furnished to do so, subject to the following conditions: + +//The above copyright notice and this permission notice (including the next paragraph) shall be +//included in all copies or substantial portions of the Software. + +//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +//NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +//NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +//DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +pub use crate::{ + config::*, + database::{DatabaseConfiguration, DatabaseConnection}, + database_request, + error::*, + impl_select_page_wrapper, + routes::{oidc::*, *}, + state::FeedbackFusionState, +}; +pub use axum::{ + extract::{Json, Query, State}, + routing::*, + Router, +}; +pub use derivative::Derivative; +pub use getset::{Getters, MutGetters, Setters}; +pub use lazy_static::lazy_static; +pub use paste::paste; +pub use rbatis::{ + crud, impl_insert, impl_select, impl_select_page, impled, plugin::page::Page, py_sql, + rbdc::JsonV, IPageRequest, +}; +pub use serde::{Deserialize, Serialize}; +pub use tracing::{debug, error, info, info_span, warn}; +pub use typed_builder::TypedBuilder; +pub use utoipa::{IntoParams, ToSchema}; +pub use validator::Validate; + +#[cfg(feature = "bindings")] +pub use ts_rs::TS; diff --git a/src/routes/v1/mod.rs b/src/routes/v1/mod.rs index b577d941..300ef930 100644 --- a/src/routes/v1/mod.rs +++ b/src/routes/v1/mod.rs @@ -46,6 +46,7 @@ pub async fn router(state: FeedbackFusionState) -> (Router, Router) { ) .with_state(state.clone()), Router::new() + .route("/target/:target/prompt/:prompt", get(prompt::get_prompt)) .route("/target/:target/prompt/:prompt/fetch", get(prompt::fetch)) .route( "/target/:target/prompt/:prompt/response", @@ -56,6 +57,7 @@ pub async fn router(state: FeedbackFusionState) -> (Router, Router) { } #[derive(ToSchema, Deserialize, Debug, Clone, Validate)] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct CreateFeedbackTargetRequest { #[validate(length(max = 255))] name: String, @@ -134,6 +136,7 @@ pub async fn get_target( } #[derive(Clone, Debug, Deserialize, ToSchema, Validate)] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct PutFeedbackTargetRequest { #[validate(length(max = 255))] name: Option, diff --git a/src/routes/v1/prompt.rs b/src/routes/v1/prompt.rs index 91bcbe08..faf6d357 100644 --- a/src/routes/v1/prompt.rs +++ b/src/routes/v1/prompt.rs @@ -39,6 +39,7 @@ pub async fn router(state: FeedbackFusionState) -> Router { } #[derive(ToSchema, Deserialize, Debug, Clone, Validate)] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct CreateFeedbackPromptRequest { #[validate(length(max = 255))] title: String, @@ -69,6 +70,25 @@ pub async fn post_prompt( Ok((StatusCode::CREATED, Json(prompt))) } +/// GET /v1/target/:target/prompt/:prompt +#[utoipa::path(get, path = "/v1/target/:target/prompt/:prompt", responses( + (status = 200, body = FeedbackPrompt) +), tag = "FeedbackTargetPrompt", security(()))] +pub async fn get_prompt( + State(state): State, + Path((_, prompt)): Path<(String, String)>, +) -> Result> { + let prompt: Option = + database_request!(FeedbackPrompt::select_by_id(state.connection(), prompt.as_str()).await?); + + match prompt { + Some(prompt) => Ok(Json(prompt)), + None => Err(FeedbackFusionError::BadRequest( + "invalid prompt".to_string(), + )), + } +} + /// GET /v1/target/:target/prompt #[utoipa::path(get, path = "/v1/target/:target/prompt", params(Pagination), responses( (status = 200, body = FeedbackPromptPage) @@ -92,6 +112,7 @@ pub async fn get_prompts( } #[derive(Deserialize, Debug, Clone, ToSchema, Validate)] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct PutFeedbackPromptRequest { #[validate(length(max = 255))] title: Option, @@ -139,6 +160,7 @@ pub async fn delete_prompt( } #[derive(Debug, Clone, ToSchema, Deserialize, Validate)] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct CreateFeedbackPromptFieldRequest { #[validate(length(max = 255))] title: String, @@ -165,10 +187,14 @@ pub async fn post_field( }; // build the field + #[cfg(not(feature = "bindings"))] + let options = JsonV(data.options); + #[cfg(feature = "bindings")] + let options = data.options; let field = FeedbackPromptField::builder() .title(data.title) .r#type(data.r#type) - .options(JsonV(data.options)) + .options(options) .prompt(prompt) .build(); database_request!(FeedbackPromptField::insert(state.connection(), &field).await?); @@ -234,6 +260,7 @@ pub async fn get_fields( } #[derive(Debug, Clone, Deserialize, Validate, ToSchema)] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct PutFeedbackPromptFieldRequest { #[validate(length(max = 255))] title: Option, @@ -272,6 +299,7 @@ pub async fn put_field( field.set_title(data.title.unwrap_or(field.title().to_string())); if let Some(options) = data.options { if field.r#type().eq(&options) { + #[cfg(not(feature = "bindings"))] field.set_options(JsonV(options)); } } diff --git a/src/routes/v1/response.rs b/src/routes/v1/response.rs index 196da718..06f3165e 100644 --- a/src/routes/v1/response.rs +++ b/src/routes/v1/response.rs @@ -40,6 +40,7 @@ pub async fn router(state: FeedbackFusionState) -> Router { } #[derive(Deserialize, Clone, Debug, ToSchema)] +#[cfg_attr(feature = "bindings", derive(TS))] pub struct SubmitFeedbackPromptResponseRequest { responses: HashMap, } @@ -78,11 +79,14 @@ pub async fn post_response( .iter() .any(|f| field.eq(f.id()) && f.r#type().eq(&value)) { + #[cfg(not(feature = "bindings"))] + let value = JsonV(value); + Some( FeedbackPromptFieldResponse::builder() .response(response.id().as_str()) .field(field) - .data(JsonV(value)) + .data(value) .build(), ) } else { @@ -105,6 +109,8 @@ pub async fn post_response( pub type GetFeedbackPromptResponsesResponse = HashMap>; #[derive(ToSchema)] +#[cfg_attr(feature = "bindings", derive(TS))] +#[allow(unused)] pub struct GetFeedbackPromptResponsesResponseWrapper( HashMap>, ); diff --git a/tests/http_tests.rs b/tests/http_tests.rs index f0dd162f..a5e556a9 100644 --- a/tests/http_tests.rs +++ b/tests/http_tests.rs @@ -251,6 +251,32 @@ async fn test_prompt_endpoints() { assert_eq!(&prompt, data.records.first().unwrap()); } + // test get specific prompt + { + let response = client + .get(format!( + "{}/v1/target/{}/prompt/invalid", + HTTP_ENDPOINT, &target.id + )) + .send() + .await + .unwrap(); + assert_eq!(StatusCode::BAD_REQUEST, response.status()); + + let response = client + .get(format!( + "{}/v1/target/{}/prompt/{}", + HTTP_ENDPOINT, &target.id, &prompt.id + )) + .send() + .await + .unwrap(); + assert_eq!(StatusCode::OK, response.status()); + + let data = response.json::().await.unwrap(); + assert_eq!(&prompt, &data); + } + // test put { let response = client