diff --git a/integrator-default-profile/samples/customer-order-api/Ballerina.toml b/integrator-default-profile/samples/customer-order-api/Ballerina.toml new file mode 100644 index 00000000..21c82069 --- /dev/null +++ b/integrator-default-profile/samples/customer-order-api/Ballerina.toml @@ -0,0 +1,27 @@ +[package] +org = "wso2" +name = "customer_order_api" +version = "0.1.0" +distribution = "2201.13.3" +title = "Customer Order API" + +[build-options] +sticky = true + +[[tool.persist]] +id = "customer_order_api.dbpersist" +targetModule = "customer_order_api.dbpersist" +filePath = "persist/db/model.bal" +options.datastore = "postgresql" +options.eagerLoading = true +options.withInitParams = true + +[[dependency]] +org = "ballerina" +name = "tool.persist" +version = "1.9.2" + +[[platform.java21.dependency]] +groupId = "io.ballerina.stdlib" +artifactId = "persist.sql-native" +version = "1.7.2" diff --git a/integrator-default-profile/samples/customer-order-api/Dependencies.toml b/integrator-default-profile/samples/customer-order-api/Dependencies.toml new file mode 100644 index 00000000..fd0b9b8f --- /dev/null +++ b/integrator-default-profile/samples/customer-order-api/Dependencies.toml @@ -0,0 +1,441 @@ +# AUTO-GENERATED FILE. DO NOT MODIFY. + +# This file is auto-generated by Ballerina for managing dependency versions. +# It should not be modified by hand. + +[ballerina] +dependencies-toml-version = "2" +distribution-version = "2201.13.3" + +[[package]] +org = "ballerina" +name = "auth" +version = "2.14.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "cache" +version = "3.10.0" +dependencies = [ + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "constraint" +version = "1.7.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "crypto" +version = "2.10.1" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "data.jsondata" +version = "1.1.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "file" +version = "1.12.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "os"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "http" +version = "2.16.1" +dependencies = [ + {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "constraint"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "data.jsondata"}, + {org = "ballerina", name = "file"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "jwt"}, + {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.decimal"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.regexp"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "mime"}, + {org = "ballerina", name = "oauth2"}, + {org = "ballerina", name = "observe"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] +modules = [ + {org = "ballerina", packageName = "http", moduleName = "http"}, + {org = "ballerina", packageName = "http", moduleName = "http.httpscerr"} +] + +[[package]] +org = "ballerina" +name = "io" +version = "1.8.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"} +] + +[[package]] +org = "ballerina" +name = "jballerina.java" +version = "0.0.0" +modules = [ + {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "jwt" +version = "2.15.1" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "lang.string"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] + +[[package]] +org = "ballerina" +name = "lang.decimal" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.int" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" + +[[package]] +org = "ballerina" +name = "lang.regexp" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.runtime" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.string" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.regexp"} +] + +[[package]] +org = "ballerina" +name = "lang.transaction" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "lang.value" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "log" +version = "2.17.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "observe"} +] + +[[package]] +org = "ballerina" +name = "mime" +version = "2.12.1" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "log"} +] + +[[package]] +org = "ballerina" +name = "oauth2" +version = "2.15.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "url"} +] + +[[package]] +org = "ballerina" +name = "observe" +version = "1.7.1" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "os" +version = "1.10.1" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "persist" +version = "1.7.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "persist", moduleName = "persist"} +] + +[[package]] +org = "ballerina" +name = "sql" +version = "1.19.0" +dependencies = [ + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerina", packageName = "sql", moduleName = "sql"} +] + +[[package]] +org = "ballerina" +name = "task" +version = "2.11.1" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "uuid"} +] + +[[package]] +org = "ballerina" +name = "time" +version = "2.8.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] +modules = [ + {org = "ballerina", packageName = "time", moduleName = "time"} +] + +[[package]] +org = "ballerina" +name = "url" +version = "2.6.1" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + +[[package]] +org = "ballerina" +name = "uuid" +version = "1.10.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "time"} +] + +[[package]] +org = "ballerinai" +name = "observe" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "observe"} +] + +[[package]] +org = "ballerinai" +name = "transaction" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.transaction"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "uuid"} +] +modules = [ + {org = "ballerinai", packageName = "transaction", moduleName = "transaction"} +] + +[[package]] +org = "ballerinax" +name = "cdc" +version = "1.3.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "data.jsondata"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerinai", name = "observe"} +] + +[[package]] +org = "ballerinax" +name = "persist.sql" +version = "1.7.3" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "persist"}, + {org = "ballerina", name = "sql"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerinax", packageName = "persist.sql", moduleName = "persist.sql"} +] + +[[package]] +org = "ballerinax" +name = "postgresql" +version = "1.18.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "sql"}, + {org = "ballerina", name = "time"}, + {org = "ballerinax", name = "cdc"} +] +modules = [ + {org = "ballerinax", packageName = "postgresql", moduleName = "postgresql"} +] + +[[package]] +org = "ballerinax" +name = "postgresql.driver" +version = "1.6.3" +modules = [ + {org = "ballerinax", packageName = "postgresql.driver", moduleName = "postgresql.driver"} +] + +[[package]] +org = "wso2" +name = "customer_order_api" +version = "0.1.0" +dependencies = [ + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "persist"}, + {org = "ballerina", name = "sql"}, + {org = "ballerina", name = "time"}, + {org = "ballerinai", name = "transaction"}, + {org = "ballerinax", name = "persist.sql"}, + {org = "ballerinax", name = "postgresql"}, + {org = "ballerinax", name = "postgresql.driver"} +] +modules = [ + {org = "wso2", packageName = "customer_order_api", moduleName = "customer_order_api"}, + {org = "wso2", packageName = "customer_order_api", moduleName = "customer_order_api.dbpersist"} +] + +[[tool]] +id = "persist" +org = "ballerina" +name = "tool.persist" +version = "1.9.2" + diff --git a/integrator-default-profile/samples/customer-order-api/README.md b/integrator-default-profile/samples/customer-order-api/README.md new file mode 100644 index 00000000..aa52f91a --- /dev/null +++ b/integrator-default-profile/samples/customer-order-api/README.md @@ -0,0 +1,144 @@ +# Customer Order API + +## Description + +A minimal integration sample showing how to use a database `persist` client from an HTTP service. The database models a small e-commerce schema (customers, products, orders, order items), and the service exposes a REST API that performs basic CRUD and a cross-entity insert via the generated persist client. + +The goal is to be small enough to read end-to-end. There is no auth, pagination, OpenAPI spec, or validation beyond what the sample needs to make sense. + +## What this sample demonstrates + +- Defining entities with `@sql:` annotations in `persist/db/model.bal` +- Generating a typed persist client (`bal persist generate`) and using it from an HTTP service +- Collection operations — `get`, `post`, `put`, `delete` — on the generated client +- Fetching a single record by primary key +- Filtering a collection with a `whereClause` (customers → their orders) +- Creating an `Order` first and then inserting its `OrderItem[]` rows in a follow-up call, wrapped in a single `transaction` block +- Reusing the generated record types directly as request/response payloads + +## Prerequisites + +- Ballerina 2201.13.3 or newer +- Docker (for the Postgres container) or a local Postgres 16 instance + +## Usage Instructions + +### 1. Start Postgres + +```bash +docker compose up -d +``` + +This starts Postgres on `localhost:5432`, creates the `db_persist` database, runs the schema in `db/init/01_schema.sql`, and seeds sample rows from `db/init/02_seed.sql`. + +### 2. Configure the DB password + +Create a `Config.toml` in the sample root with the password for the bundled Postgres container: + +```toml +[wso2.customer_order_api] +dbPassword = "postgres" +``` + +Replace `wso2` with the `org` value from your `Ballerina.toml` if you have changed it. Override `dbHost`, `dbPort`, `dbUser`, or `dbDatabase` in the same section if you are pointing at a different DB. + +### 3. Run the service + +```bash +bal run +``` + +The service listens on `http://localhost:9090/api/v1`. + +### Deploy on the **WSO2 Integration Platform** + +1. Deploy this integration on the **WSO2 Integration Platform** as an **Integration as API**. +2. Host a Postgres database for the application. You can create one directly on the **WSO2 Integration Platform** — in the [**Console**](https://console.devant.dev), select your **Organization** and go to the **Databases** section under the **Admin** tab. +3. Run `db/init/01_schema.sql` (and optionally `db/init/02_seed.sql`) against the database to initialize the schema and sample data. +4. In the **Overview** section of the newly created integration, configure `dbHost`, `dbPort`, `dbUser`, `dbPassword`, and `dbDatabase` before deploying. +5. Once deployed, click on **"Test"** to try out the API. + +## How It Works + +- The service listens on port `9090` under the `/api/v1` base path. +- On startup, a typed `persist` client is created from the entities defined in `persist/db/model.bal` and pointed at the configured Postgres instance. +- Each resource function maps directly to an operation on the generated client — for example, `GET /customers` returns `customerDb->/customers.get()`. +- Creating an order is wrapped in a `transaction` block: the `Order` row is inserted first to obtain its primary key, then the `OrderItem[]` rows are inserted with that key and the order total (computed from current product prices) is persisted in the same commit. +- Requests for records that do not exist return `404 Not Found`; invalid order payloads (empty items, non-positive quantity, unknown customer or product) return `400 Bad Request`. + +## API + +All endpoints are under `/api/v1`. + +### Customers + +```bash +# List +curl http://localhost:9090/api/v1/customers + +# Get one +curl http://localhost:9090/api/v1/customers/1 + +# Create +curl -X POST http://localhost:9090/api/v1/customers \ + -H 'Content-Type: application/json' \ + -d '{"name": "Eve Patel", "email": "eve@example.com"}' + +# Update (any subset of fields) +curl -X PUT http://localhost:9090/api/v1/customers/1 \ + -H 'Content-Type: application/json' \ + -d '{"name": "Alice J."}' + +# Delete +curl -X DELETE http://localhost:9090/api/v1/customers/1 + +# Orders for a customer +curl http://localhost:9090/api/v1/customers/1/orders +``` + +### Products + +```bash +curl http://localhost:9090/api/v1/products +``` + +### Orders + +Create an order with one or more line items. The service looks up each product, uses its current price as the unit price, and computes the order total. + +```bash +curl -X POST http://localhost:9090/api/v1/orders \ + -H 'Content-Type: application/json' \ + -d '{ + "customerId": 1, + "status": "pending", + "items": [ + {"productId": 1, "quantity": 2}, + {"productId": 4, "quantity": 1} + ] + }' +``` + +`status` is optional and defaults to `"pending"`. + +## Regenerating the persist client + +If you edit `persist/db/model.bal`, regenerate the client: + +```bash +bal persist generate +``` + +`bal build` also runs this automatically. + +## Ideas to extend this sample + +- Add a `GET /orders/{id}` that returns the order with its items (`OrderWithRelations`) +- Decrement `Product.stock` when an order is placed +- Add pagination via `limitClause` and `orderByClause` +- Add a scheduled job that produces a daily sales summary + +## References + +- [Ballerina `persist` library](https://ballerina.io/learn/persist-your-data-with-ballerina/) +- [Develop an Integration as an API](https://wso2.com/devant/docs/quick-start-guides/develop-an-integration-as-an-api/) on the WSO2 Integration Platform diff --git a/integrator-default-profile/samples/customer-order-api/config.bal b/integrator-default-profile/samples/customer-order-api/config.bal new file mode 100644 index 00000000..d7366517 --- /dev/null +++ b/integrator-default-profile/samples/customer-order-api/config.bal @@ -0,0 +1,5 @@ +configurable string dbHost = "localhost"; +configurable int dbPort = 5432; +configurable string dbUser = "postgres"; +configurable string dbPassword = ?; +configurable string dbDatabase = "db_persist"; diff --git a/integrator-default-profile/samples/customer-order-api/connections.bal b/integrator-default-profile/samples/customer-order-api/connections.bal new file mode 100644 index 00000000..0a96bce5 --- /dev/null +++ b/integrator-default-profile/samples/customer-order-api/connections.bal @@ -0,0 +1,3 @@ +import customer_order_api.dbpersist; + +final dbpersist:Client customerDb = check new (dbHost, dbPort, dbUser, dbPassword, dbDatabase); diff --git a/integrator-default-profile/samples/customer-order-api/db/init/01_schema.sql b/integrator-default-profile/samples/customer-order-api/db/init/01_schema.sql new file mode 100644 index 00000000..d679f3b5 --- /dev/null +++ b/integrator-default-profile/samples/customer-order-api/db/init/01_schema.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS customers ( + "id" SERIAL PRIMARY KEY, + "name" VARCHAR(120) NOT NULL, + "email" VARCHAR(160) NOT NULL UNIQUE, + "createdAt" TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS products ( + "id" SERIAL PRIMARY KEY, + "sku" VARCHAR(60) NOT NULL UNIQUE, + "name" VARCHAR(160) NOT NULL, + "price" DECIMAL(10,2) NOT NULL CHECK ("price" >= 0), + "stock" INT NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS orders ( + "id" SERIAL PRIMARY KEY, + "customerId" INT NOT NULL REFERENCES customers("id") ON DELETE CASCADE, + "status" VARCHAR(20) NOT NULL DEFAULT 'pending', + "total" DECIMAL(10,2) NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS order_items ( + "id" SERIAL PRIMARY KEY, + "orderId" INT NOT NULL REFERENCES orders("id") ON DELETE CASCADE, + "productId" INT NOT NULL REFERENCES products("id"), + "quantity" INT NOT NULL CHECK ("quantity" > 0), + "unitPrice" DECIMAL(10,2) NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_orders_customer_id ON orders("customerId"); +CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items("orderId"); diff --git a/integrator-default-profile/samples/customer-order-api/db/init/02_seed.sql b/integrator-default-profile/samples/customer-order-api/db/init/02_seed.sql new file mode 100644 index 00000000..4664a054 --- /dev/null +++ b/integrator-default-profile/samples/customer-order-api/db/init/02_seed.sql @@ -0,0 +1,24 @@ +INSERT INTO customers ("name", "email") VALUES + ('Alice Johnson', 'alice@example.com'), + ('Bob Martinez', 'bob@example.com'), + ('Carol Nguyen', 'carol@example.com'), + ('David Okafor', 'david@example.com'); + +INSERT INTO products ("sku", "name", "price", "stock") VALUES + ('SKU-001', 'Wireless Mouse', 25.99, 120), + ('SKU-002', 'Mechanical Keyboard', 89.50, 45), + ('SKU-003', '27" Monitor', 329.00, 18), + ('SKU-004', 'USB-C Hub', 39.95, 80), + ('SKU-005', 'Laptop Stand', 49.00, 60); + +INSERT INTO orders ("customerId", "status", "total") VALUES + (1, 'paid', 115.49), + (2, 'pending', 329.00), + (3, 'shipped', 65.94); + +INSERT INTO order_items ("orderId", "productId", "quantity", "unitPrice") VALUES + (1, 1, 1, 25.99), + (1, 2, 1, 89.50), + (2, 3, 1, 329.00), + (3, 4, 1, 39.95), + (3, 1, 1, 25.99); diff --git a/integrator-default-profile/samples/customer-order-api/docker-compose.yml b/integrator-default-profile/samples/customer-order-api/docker-compose.yml new file mode 100644 index 00000000..3758b474 --- /dev/null +++ b/integrator-default-profile/samples/customer-order-api/docker-compose.yml @@ -0,0 +1,22 @@ +services: + postgres: + image: postgres:16-alpine + container_name: db_persist_postgres + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: db_persist + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./db/init:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d db_persist"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: diff --git a/integrator-default-profile/samples/customer-order-api/main.bal b/integrator-default-profile/samples/customer-order-api/main.bal new file mode 100644 index 00000000..63044cad --- /dev/null +++ b/integrator-default-profile/samples/customer-order-api/main.bal @@ -0,0 +1,145 @@ +import customer_order_api.dbpersist; + +import ballerina/http; +import ballerina/persist; +import ballerina/time; + +service /api/v1 on new http:Listener(9090) { + + // Customers + + resource function get customers() returns dbpersist:Customer[]|error { + dbpersist:Customer[] customers = check customerDb->/customers.get(); + return customers; + } + + resource function get customers/[int id]() returns dbpersist:Customer|http:NotFound|error { + dbpersist:Customer|persist:Error result = customerDb->/customers/[id].get(); + if result is persist:NotFoundError { + return http:NOT_FOUND; + } + return result; + } + + resource function post customers(NewCustomer payload) returns dbpersist:Customer|error { + int[] ids = check customerDb->/customers.post([ + {name: payload.name, email: payload.email, createdAt: time:utcNow()} + ]); + dbpersist:Customer created = check customerDb->/customers/[ids[0]].get(); + return created; + } + + resource function put customers/[int id](dbpersist:CustomerUpdate payload) + returns dbpersist:Customer|http:NotFound|error { + dbpersist:Customer|persist:Error result = customerDb->/customers/[id].put(payload); + if result is persist:NotFoundError { + return http:NOT_FOUND; + } + return result; + } + + resource function delete customers/[int id]() + returns dbpersist:Customer|http:NotFound|error { + dbpersist:Customer|persist:Error result = customerDb->/customers/[id].delete(); + if result is persist:NotFoundError { + return http:NOT_FOUND; + } + return result; + } + + resource function get customers/[int id]/orders() returns dbpersist:Order[]|error { + dbpersist:Order[] orders = check customerDb->/orders.get(whereClause = `"customerId" = ${id}`); + return orders; + } + + // Products + + resource function get products() returns dbpersist:Product[]|error { + dbpersist:Product[] products = check customerDb->/products.get(); + return products; + } + + // Orders + + resource function post orders(NewOrder payload) + returns dbpersist:Order|http:BadRequest|error { + if payload.items.length() == 0 { + return {body: "order must contain at least one item"}; + } + foreach NewOrderItem item in payload.items { + if item.quantity <= 0 { + return { + body: string `item quantity must be positive (productId=${item.productId}, quantity=${item.quantity})` + }; + } + } + + dbpersist:Customer|persist:Error customer = customerDb->/customers/[payload.customerId].get(); + if customer is persist:NotFoundError { + return {body: string `customer ${payload.customerId} not found`}; + } + if customer is persist:Error { + return customer; + } + + decimal total = 0; + dbpersist:OrderItemInsert[] itemInserts = []; + foreach NewOrderItem item in payload.items { + dbpersist:Product|persist:Error product = customerDb->/products/[item.productId].get(); + if product is persist:NotFoundError { + return {body: string `product ${item.productId} not found`}; + } + if product is persist:Error { + return product; + } + total += product.price * item.quantity; + itemInserts.push({ + orderId: 0, + productId: product.id, + quantity: item.quantity, + unitPrice: product.price + }); + } + + int createdOrderId = 0; + transaction { + int[] orderIds = check customerDb->/orders.post([ + { + customerId: payload.customerId, + status: payload.status ?: "pending", + total: total, + createdAt: time:utcNow() + } + ]); + createdOrderId = orderIds[0]; + + foreach int i in 0 ..< itemInserts.length() { + itemInserts[i].orderId = createdOrderId; + } + _ = check customerDb->/orderitems.post(itemInserts); + + check commit; + } on fail error e { + return e; + } + + dbpersist:Order created = check customerDb->/orders/[createdOrderId].get(); + return created; + } +} + +public type NewCustomer record {| + string name; + string email; +|}; + +public type NewOrderItem record {| + int productId; + int quantity; +|}; + +public type NewOrder record {| + int customerId; + string status?; + NewOrderItem[] items; +|}; diff --git a/integrator-default-profile/samples/customer-order-api/persist/db/model.bal b/integrator-default-profile/samples/customer-order-api/persist/db/model.bal new file mode 100644 index 00000000..c980d978 --- /dev/null +++ b/integrator-default-profile/samples/customer-order-api/persist/db/model.bal @@ -0,0 +1,63 @@ +import ballerina/persist as _; +import ballerina/time; +import ballerinax/persist.sql; + +@sql:Name {value: "orders"} +public type Order record {| + @sql:Generated + readonly int id; + @sql:Index {name: "idx_orders_customer_id"} + int customerId; + @sql:Varchar {length: 20} + string status; + @sql:Decimal {precision: [10, 2]} + decimal total; + time:Utc createdAt; + OrderItem[] orderitems; + @sql:Relation {keys: ["customerId"]} + Customer customer; +|}; + +@sql:Name {value: "customers"} +public type Customer record {| + @sql:Generated + readonly int id; + @sql:Varchar {length: 120} + string name; + @sql:Varchar {length: 160} + @sql:UniqueIndex {name: "customers_email_key"} + string email; + time:Utc createdAt; + Order[] orders; +|}; + +@sql:Name {value: "order_items"} +public type OrderItem record {| + @sql:Generated + readonly int id; + @sql:Index {name: "idx_order_items_order_id"} + int orderId; + int productId; + int quantity; + @sql:Decimal {precision: [10, 2]} + decimal unitPrice; + @sql:Relation {keys: ["orderId"]} + Order 'order; + @sql:Relation {keys: ["productId"]} + Product product; +|}; + +@sql:Name {value: "products"} +public type Product record {| + @sql:Generated + readonly int id; + @sql:Varchar {length: 60} + @sql:UniqueIndex {name: "products_sku_key"} + string sku; + @sql:Varchar {length: 160} + string name; + @sql:Decimal {precision: [10, 2]} + decimal price; + int stock; + OrderItem[] orderitems; +|};