diff --git a/test/test-markdown-frontmatter.js b/test/test-markdown-frontmatter.js index 68ea5bc..e1de589 100644 --- a/test/test-markdown-frontmatter.js +++ b/test/test-markdown-frontmatter.js @@ -4,9 +4,9 @@ const yaml = require('js-yaml') const chalk = require('chalk') // accepted data field values -const sdk_languages = ['nodejs', 'scala', 'python', 'swift', 'csharp', 'objective-c', 'android-java', 'any', 'java', 'kotlin', 'dart', 'golang', 'c++'] +const sdk_languages = ['nodejs', 'scala', 'python', 'swift', 'csharp', 'objective-c', 'android-java', 'any', 'java', 'kotlin', 'dart', 'golang', 'ruby', 'c++'] -const tags = ['Ottoman', 'Ktor', 'REST API', 'Express', 'Flask', 'TLS', 'Configuration', 'Next.js', 'iOS', 'Xcode', '.NET', 'Xamarin', 'Authentication', 'OpenID', 'Keycloak', 'Android', 'P2P', 'UIKit', 'Installation', 'Spring Boot', 'Spring Data', 'Transactions', 'SQL++ (N1QL)', 'Optimization', 'Community Edition', 'Docker', 'Data Modeling', 'Metadata', 'Best Practices', 'Data Ingestion', 'Kafka', 'Support', 'Customer', 'Prometheus', 'Monitoring', 'Observability', 'Metrics', 'Query Workbench', 'ASP.NET', 'linq', 'DBaaS', 'App Services', 'Flutter', 'Gin Gonic', 'FastAPI', 'LangChain', "OpenAI", "Streamlit", 'Google Gemini', 'Nvidia NIM', 'LLama3', 'AWS', 'Artificial Intelligence', 'Cohere', 'Jina AI', 'Mistral AI', 'Ragas', 'Haystack', 'LangGraph', 'Amazon Bedrock', 'CrewAI', 'PydanticAI', 'C++', 'C++ SDK', 'smolagents', 'Ag2', 'Autogen', 'Couchbase Edge Server', 'Deepseek', 'OpenRouter', 'mastra', 'Looker Studio', 'Google Data Studio', 'Connector', 'Couchbase Columnar', 'TAVs', 'Custom Queries', 'Data API', 'FTS', 'GSI', 'Hugging Face'] +const tags = ['Ottoman', 'Ktor', 'REST API', 'Express', 'Flask', 'TLS', 'Configuration', 'Next.js', 'iOS', 'Xcode', '.NET', 'Xamarin', 'Authentication', 'OpenID', 'Keycloak', 'Android', 'P2P', 'UIKit', 'Installation', 'Spring Boot', 'Spring Data', 'Transactions', 'SQL++ (N1QL)', 'Optimization', 'Community Edition', 'Docker', 'Data Modeling', 'Metadata', 'Best Practices', 'Data Ingestion', 'Kafka', 'Support', 'Customer', 'Prometheus', 'Monitoring', 'Observability', 'Metrics', 'Query Workbench', 'ASP.NET', 'linq', 'DBaaS', 'App Services', 'Flutter', 'Gin Gonic', 'FastAPI', 'Rails', 'Couchbase ORM', 'LangChain', 'OpenAI', 'Streamlit', 'Google Gemini', 'Nvidia NIM', 'LLama3', 'AWS', 'Artificial Intelligence', 'Cohere', 'Jina AI', 'Mistral AI', 'Ragas', 'Haystack', 'LangGraph', 'Amazon Bedrock', 'CrewAI', 'PydanticAI', 'C++', 'C++ SDK', 'smolagents', 'Ag2', 'Autogen', 'Couchbase Edge Server', 'Deepseek', 'OpenRouter', 'mastra', 'Looker Studio', 'Google Data Studio', 'Connector', 'Couchbase Columnar', 'TAVs', 'Custom Queries', 'Data API', 'FTS', 'GSI', 'Hugging Face'] const technologies = ['connectors', 'kv', 'query', 'capella', 'server', 'index', 'mobile', 'fts', 'sync gateway', 'eventing', 'analytics', 'udf', 'vector search', 'react', 'edge-server', 'app-services'] diff --git a/tutorial/markdown/java/transactions.md b/tutorial/markdown/java/transactions.md index 4048f35..161a257 100644 --- a/tutorial/markdown/java/transactions.md +++ b/tutorial/markdown/java/transactions.md @@ -158,26 +158,10 @@ spring.couchbase.bucket.password=password > **_NOTE:_** Couchbase Capella clusters are using Transport Security Layer (TLS) cryptographic protocol to secure traffic between the cluster and client applications. > For simplicity, we will forego certificate validation step in this tutorial. > It is highly recommended to always verify your cluster's certificate on production environments. - -<<<<<<< HEAD -> **_NOTE:_** Couchbase Capepella clusters are using Transport Security Layer (TLS) cryptographic protocol to secure traffic between the cluster and client applications. -Every Capella cluster comes with its own self-signed TLS certificate. -This certificate can be used by client applications to verify the identity of the server and prevent attackers from intercepting database traffic. -It is highly recommended to always verify your cluster's certificate on production environments. -Please refer to "Full Example / Couchbase Capella Sample" and "Cloud Connections" sections of [Install and Start Using the Java SDK with Couchbase Server](https://docs.couchbase.com/java-sdk/current/hello-world/start-using-sdk.html#full-example) for information on how to validate cluster certificates. - -To connect to Capella, use `couchbases://` connection protocol and add `?tls=no_verify` to the connection string: - -```ini -spring.couchbase.bootstrap-hosts=couchbases://cb.jncm2s9gv4ealbm.cloud.couchbase.com?ssl=no_veriify -``` - -Save the file and start the sample application by running the following command in the root folder of the project: -======= > Please refer to "Full Example / Couchbase Capella Sample" and "Cloud Connections" sections of [Install and Start Using the Java SDK with Couchbase Server](https://docs.couchbase.com/java-sdk/current/hello-world/start-using-sdk.html#full-example) for information on how to validate cluster certificates. -To confiure Capella connections, use a `ClusterEnvironment` with enabled TLS security: +To configure Capella connections, use a `ClusterEnvironment` with enabled TLS security: ```java @Bean @@ -191,7 +175,7 @@ To confiure Capella connections, use a `ClusterEnvironment` with enabled TLS sec public ClusterEnvironment getClusterEnvironment() { ClusterEnvironment.Builder environmentBuilder = ClusterEnvironment.builder(); - + SecurityConfig.Builder securityConfig = SecurityConfig.enableTls(true) .trustManagerFactory(InsecureTrustManagerFactory.INSTANCE); environmentBuilder.securityConfig(securityConfig); @@ -200,15 +184,14 @@ To confiure Capella connections, use a `ClusterEnvironment` with enabled TLS sec } ``` -Another alternative is to use`couchbases://` connection protocol and add `?ssl=no_verify` to the connection string: +Another alternative is to use `couchbases://` connection protocol and add `?ssl=no_verify` to the connection string: ```ini spring.couchbase.bootstrap-hosts=couchbases://cb.jnym5s9gv4ealbe.cloud.couchbase.com?ssl=no_verify ``` -> **_NOTE_**: these Capella connection confguration examples should be used for development purposes only and are not recommended for production. +> **_NOTE_**: these Capella connection configuration examples should be used for development purposes only and are not recommended for production. Save your customized configuration and start the sample application by running the following command in the root folder of the project: ->>>>>>> 066839e6fbc8c2f00fa28c21cd9a4472abeb62f1 ```shell mvn spring-boot:run diff --git a/tutorial/markdown/ruby/ruby-couchbase-orm/quickstart-ruby-couchbase-orm.md b/tutorial/markdown/ruby/ruby-couchbase-orm/quickstart-ruby-couchbase-orm.md new file mode 100644 index 0000000..700cca3 --- /dev/null +++ b/tutorial/markdown/ruby/ruby-couchbase-orm/quickstart-ruby-couchbase-orm.md @@ -0,0 +1,645 @@ +--- +# frontmatter +path: "/tutorial-quickstart-ruby-couchbase-orm" +title: Quickstart in Couchbase with Ruby Couchbase ORM +short_title: Ruby Couchbase ORM +description: + - Learn to build a REST API with Ruby Couchbase ORM + - See how you can fetch data from Couchbase using SQL++ queries + - Explore CRUD operations in action with Couchbase +content_type: quickstart +filter: sdk +technology: + - kv + - index + - query +tags: + - Couchbase ORM + - REST API +sdk_language: + - ruby +length: 30 Mins +--- + + + +In this tutorial, you will learn how to connect to a Couchbase Capella cluster to create, read, update, and delete documents and how to write simple parametrized SQL++ queries. + +## Prerequisites + +To run this prebuilt project, you will need: + +- [Couchbase Capella](https://www.couchbase.com/products/capella/) cluster with [travel-sample](https://docs.couchbase.com/ruby-sdk/current/ref/travel-app-data-model.html) bucket loaded. + - To run this tutorial using a self managed Couchbase cluster, please refer to the [appendix](#running-self-managed-couchbase-cluster). +- [Ruby](https://www.ruby-lang.org/en/documentation/installation/) installed on your machine. + - Ensure that the Ruby version is [compatible](https://docs.couchbase.com/ruby-sdk/current/project-docs/compatibility.html#ruby-version-compatibility) with the Couchbase SDK. +- Loading Travel Sample Bucket + If travel-sample is not loaded in your Capella cluster, you can load it by following the instructions for your Capella Cluster: + + - [Load travel-sample bucket in Couchbase Capella](https://docs.couchbase.com/cloud/clusters/data-service/import-data-documents.html#import-sample-data) + +### Couchbase Capella Configuration + +When running Couchbase using [Capella](https://cloud.couchbase.com/), the following prerequisites need to be met. + +- The application requires the travel-sample bucket to be [loaded](https://docs.couchbase.com/cloud/clusters/data-service/import-data-documents.html#import-sample-data) in the cluster from the Capella UI. +- Create the [database credentials](https://docs.couchbase.com/cloud/clusters/manage-database-users.html) to access the travel-sample bucket (Read and Write) used in the application. +- [Allow access](https://docs.couchbase.com/cloud/clusters/allow-ip-address.html) to the Cluster from the IP on which the application is running. + +## App Setup + +### Cloning Repo + +```shell +git clone https://github.com/couchbase-examples/ruby-couchbase-orm-quickstart.git +``` + +### Install Dependencies + +The dependencies for the application are specified in the `Gemfile` file in the source folder. Dependencies can be installed through `bundler` the default package manager for Ruby. + +```shell +cd ruby-couchbase-orm-quickstart +gem install bundler +bundle install +``` + +> Refer to the [instructions in the SDK](https://github.com/couchbase/couchbase-ruby-client#installation) for more info. + +### Setup Database Configuration + +To use Couchbase ORM in your Ruby application, you need to configure the connection settings for your Couchbase Server instance. Couchbase ORM provides a simple way to configure the connection. + +1. **Rails Applications**: + + If you're using Couchbase ORM in a Rails application, create a new configuration file named `couchbase.yml` in the `config` directory of your Rails project. Add the following content to the file: + + ```yaml + development: + connection_string: couchbase://localhost + bucket: my_app_development + username: my_username + password: my_password + + test: + connection_string: couchbase://localhost + bucket: my_app_test + username: my_username + password: my_password + + production: + connection_string: couchbase://localhost + bucket: my_app_production + username: my_username + password: my_password + ``` + + Replace the values for `connection_string`, `bucket`, `username`, and `password` with your actual Couchbase Server connection details. + + CouchbaseOrm will automatically load the configuration based on the current Rails environment. + +2. **Non-Rails Applications**: + + For non-Rails applications, you can configure the connection settings programmatically. Add the following code to your application's initialization file or before you start using Couchbase ORM: + + ```ruby + CouchbaseOrm.configure do |config| + config.connection_string = 'couchbase://localhost' + config.bucket = 'my_app' + config.username = 'my_username' + config.password = 'my_password' + end + ``` + + Replace the values for `connection_string`, `bucket`, `username`, and `password` with your actual Couchbase Server connection details. + +## Running the Application + +### Directly on Local Machine + +At this point, we have installed the dependencies, loaded the travel-sample data and configured the application with the credentials. The application is now ready and you can run it. + +The application will run on port 3000 of your local machine (http://localhost:3000). You will find the Swagger documentation of the API which you can use to try the API endpoints. + +The Swagger Page will be available at http://localhost:3000/api-docs. + +```shell +rails s +``` + +### Using Docker + +If you prefer to run this quick start using Docker, we have provided the Dockerfile which you can use to build the image and run the API as a container. + +- Build the Docker image + +```sh +docker build -t ruby-couchbase-orm-quickstart . +``` + +- Run the Docker image + +```sh +docker run --env-file dev.env -p 3000:3000 ruby-couchbase-orm-quickstart +``` + +> Note: This app reads Couchbase connection settings from `config/couchbase.yml`. Ensure this file is included in the image (COPY) or mount it into the container at runtime. + +Once the app is up and running, open http://localhost:3000/api-docs to test the APIs. + +### Verifying the Application + +Once the application starts, you can see the details of the application on the logs. + +![Application Startup](./app_startup.png) + +The application will run on port 3000 of your local machine (http://localhost:3000). You will find the interactive Swagger documentation of the API if you go to the URL in your browser. Swagger documentation is used in this demo to showcase the different API end points and how they can be invoked. More details on the Swagger documentation can be found in the [appendix](#swagger-documentation). + +![Swagger Documentation](swagger_documentation.png) + +## Data Model + +For this tutorial, we use three collections, `airport`, `airline` and `route` that contain sample airports, airlines and airline routes respectively. The route collection connects the airports and airlines as seen in the figure below. We use these connections in the quickstart to generate airports that are directly connected and airlines connecting to a destination airport. Note that these are just examples to highlight how you can use SQL++ queries to join the collections. + +![Travel Sample Data Model](./travel_sample_data_model.png) + +## Let Us Review the Code + +To begin this tutorial, clone the repo and open it up in the IDE of your choice. Now you can learn about how to create, read, update and delete documents in Couchbase Server. + +### Code Layout + +``` +. +├── app +│ ├── controllers +│ │ ├── api +│ │ │ └── v1 +│ │ │ ├── airlines_controller.rb +│ │ │ ├── airports_controller.rb +│ │ │ └── routes_controller.rb +│ │ ├── application_controller.rb +│ │ └── concerns +│ ├── models +│ │ ├── airline.rb +│ │ ├── airport.rb +│ │ ├── application_record.rb +│ │ ├── concerns +│ │ └── route.rb +│ └── views +│ └── layouts +│ ├── application.html.erb +│ ├── mailer.html.erb +│ └── mailer.text.erb +├── config +│ ├── importmap.rb +│ ├── initializers +│ │ ├── couchbase.rb +│ │ ├── rswag_api.rb +│ │ └── rswag_ui.rb +│ ├── routes.rb +├── config.ru +├── db +│ └── seeds.rb +├── dev.env +├── dev.env.example +├── Dockerfile +├── Gemfile +├── Gemfile.lock +├── spec +│ ├── rails_helper.rb +│ ├── requests +│ │ └── api +│ │ └── v1 +│ │ ├── airlines_spec.rb +│ │ ├── airports_spec.rb +│ │ └── routes_spec.rb +│ ├── spec_helper.rb +│ └── swagger_helper.rb +├── swagger +│ └── v1 +│ └── swagger.yaml +├── test +│ ├── channels +│ │ └── application_cable +│ │ └── connection_test.rb +│ ├── controllers +│ ├── integration +│ │ ├── airlines_spec.rb +│ │ ├── airports_spec.rb +│ │ └── routes_spec.rb +│ ├── models +``` + +We have separated out the API code into separate files by the entity (collection) in the `api` folder. The tests are similarly separated out by entity in the `tests` folder. + +In `routes.rb`, we define the routes for the application including the API routes. + +We have the Couchbase SDK operations defined in the `CouchbaseClient` class inside the `config/initializers/couchbase.rb` file. + +We recommend creating a single Couchbase connection when your application starts up and reusing this connection throughout your application. This approach improves performance by avoiding the overhead of creating new connections for each request. If you know which buckets, scopes, and collections your application will use, obtain these references at startup and share them throughout your application as well. + +In this application, we have created the connection object in `config/initializers/couchbase.rb` and use it in all of our APIs. We have also stored references to the `travel-sample` bucket and the `inventory` scope in global constants for easy access. + +```rb +require 'couchbase' + +# Get environment variables +DB_USERNAME = ENV['DB_USERNAME'] +DB_PASSWORD = ENV['DB_PASSWORD'] +DB_CONN_STR = ENV['DB_CONN_STR'] +DB_BUCKET_NAME = 'travel-sample' # Hardcoded bucket name + +# Check if running in CI environment +if ENV['CI'] + # Use environment variables from GitHub Secrets + options = Couchbase::Cluster::ClusterOptions.new + options.authenticate(DB_USERNAME, DB_PASSWORD) + COUCHBASE_CLUSTER = Couchbase::Cluster.connect(DB_CONN_STR, options) +else + # Load environment variables from dev.env file + require 'dotenv' + Dotenv.load('dev.env') + + # Define default values + DEFAULT_DB_USERNAME = 'Administrator' + DEFAULT_DB_PASSWORD = 'password' + DEFAULT_DB_CONN_STR = 'couchbase://localhost' + + # Get environment variables with fallback to default values + DB_USERNAME = ENV.fetch('DB_USERNAME', DEFAULT_DB_USERNAME) + DB_PASSWORD = ENV.fetch('DB_PASSWORD', DEFAULT_DB_PASSWORD) + DB_CONN_STR = ENV.fetch('DB_CONN_STR', DEFAULT_DB_CONN_STR) + + # Connect to the Couchbase cluster + options = Couchbase::Cluster::ClusterOptions.new + options.authenticate(DB_USERNAME, DB_PASSWORD) + COUCHBASE_CLUSTER = Couchbase::Cluster.connect(DB_CONN_STR, options) +end + +# Open the bucket +bucket = COUCHBASE_CLUSTER.bucket(DB_BUCKET_NAME) + +# Open the default collection +default_collection = bucket.default_collection + +# Create scope and collections if they don't exist +begin + scope = bucket.scope('inventory') +rescue Couchbase::Error::ScopeNotFoundError + bucket.create_scope('inventory') + scope = bucket.scope('inventory') +end + +%w[airline airport route].each do |collection_name| + scope.collection(collection_name) +rescue Couchbase::Error::CollectionNotFoundError + scope.create_collection(collection_name) +end + +AIRLINE_COLLECTION = scope.collection('airline') +AIRPORT_COLLECTION = scope.collection('airport') +ROUTE_COLLECTION = scope.collection('route') + +``` + +In the above code, we first get the environment variables for the connection string, username, and password. We then create a connection object using the `Couchbase::Cluster.connect` method. We authenticate the connection using the username and password. We then open the bucket and the default collection. We create a scope and collections if they don't exist. We store the reference to the collections in the connection object. + +### Airline Entity + +For this tutorial, we will focus on the airline entity. The other entities are similar. + +We will be setting up a REST API to manage airline documents. + +- [POST Airline](#post-airline) – Create a new airline +- [GET Airline](#get-airline) – Read specified airline +- [PUT Airline](#put-airline) – Update specified airline +- [DELETE Airline](#delete-airline) – Delete airline +- [Airline List](#list-airline) – Get all airlines. Optionally filter the list by country +- [Direct Connections](#direct-connections) - Get a list of airlines directly connected to the specified airport + +For CRUD operations, we will use [Key-Value operations](https://docs.couchbase.com/ruby-sdk/current/howtos/kv-operations.html) using the SDK. Every document will need an ID (similar to a primary key in other databases) to save it to the database. This ID is passed in the URL. For other end points, we will use [SQL++](https://docs.couchbase.com/ruby-sdk/current/howtos/n1ql-queries-with-sdk.html) to query for documents. + +### Airline Document Structure + +Our airline document will have an airline name, IATA code, ICAO code, callsign, and country. For this demo, each airline's information is stored as a separate document in the `airline` collection in the `travel-sample` bucket. + +```json +{ + "name": "40-Mile Air", + "iata": "Q5", + "icao": "MLA", + "callsign": "MILE-AIR", + "country": "United States" +} +``` + +### POST Airline + +```rb +# frozen_string_literal: true + +module Api + module V1 + class AirlinesController < ApplicationController + skip_before_action :verify_authenticity_token, only: %i[create update destroy] + before_action :set_airline, only: %i[show update destroy] + + ... + # POST /api/v1/airlines/{id} + def create + @airline = Airline.create(params[:id], airline_params) + if @airline + render json: @airline, status: :created + else + render json: { message: 'Airline already exists' }, status: :conflict + end + rescue ArgumentError => e + render json: { error: 'Invalid request', message: e.message }, status: :bad_request + rescue Couchbase::Error::DocumentExists => e + render json: { error: "Airline with ID #{params[:id]} already exists", message: e.message }, status: :conflict + rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error + end + end + ... + + end +end +``` + +In the above code, we first create an instance of the `Airline` class using the `create` method. We pass the ID and the airline data to the `create` method. If the airline is created successfully, we return the airline document with a status of 201. If the airline already exists, we return a conflict status of 409. If there are any missing fields in the request, we return a bad request status of 400. If there is any other error, we return an internal server error status of 500. + +```rb +# frozen_string_literal: true + +# Airline model +class Airline + attr_accessor :name, :iata, :icao, :callsign, :country + + def initialize(attributes) + @name = attributes['name'] + @iata = attributes['iata'] + @icao = attributes['icao'] + @callsign = attributes['callsign'] + @country = attributes['country'] + end + ... + + def self.create(id, attributes) + required_fields = %w[name iata icao callsign country] + missing_fields = required_fields - attributes.keys + extra_fields = attributes.keys - required_fields + + raise ArgumentError, "Missing fields: #{missing_fields.join(', ')}" if missing_fields.any? + + raise ArgumentError, "Extra fields: #{extra_fields.join(', ')}" if extra_fields.any? + + formatted_attributes = { + 'name' => attributes['name'], + 'iata' => attributes['iata'], + 'icao' => attributes['icao'], + 'callsign' => attributes['callsign'], + 'country' => attributes['country'] + } + AIRLINE_COLLECTION.insert(id, formatted_attributes) + new(formatted_attributes) + end + + ... +end + +``` + +In the above code, we first check if all the required fields are present in the request. If any fields are missing, we raise an ArgumentError with the missing fields. If there are any extra fields in the request, we raise an ArgumentError with the extra fields. We then format the attributes and insert the document into the database using the `insert` method. We then create a new instance of the `Airline` class with the formatted attributes and return it. + +### GET Airline + +```rb + # GET /api/v1/airlines/{id} + def show + if @airline + render json: @airline, status: :ok + else + render json: { error: "Airline with ID #{params[:id]} not found" }, status: :not_found + end + rescue Couchbase::Error::DocumentNotFound => e + render json: { error: "Airline with ID #{params[:id]} not found", message: e.message }, status: :not_found + rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error + end +``` + +In the above code, we retrieve the airline document using the `show` method in the controller. The `@airline` instance variable is set by the `set_airline` before_action callback. If the airline is found, we return the airline document with a status of 200. If the airline is not found, we return a not found status of 404. If there is any other error, we return an internal server error status of 500. + +```rb + def self.find(id) + result = AIRLINE_COLLECTION.get(id) + new(result.content) if result.success? + rescue Couchbase::Error::DocumentNotFound + nil + end +``` + +In the `find` method, we use the `get` method to fetch the document from the database using the key. If the document is found, we create a new instance of the `Airline` class with the document content and return it. If the document is not found, we return `nil`. + +### PUT Airline + +```rb + # PUT /api/v1/airlines/{id} + def update + @airline = Airline.new(airline_params).update(params[:id], airline_params) + render json: @airline, status: :ok + rescue ArgumentError => e + render json: { error: 'Invalid request', message: e.message }, status: :bad_request + rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error + end +``` + +In the above code, we first create an instance of the `Airline` class using the `update` method. We pass the ID and the airline data to the `update` method. If the airline is updated successfully, we return the airline document with a status of 200. If there are any missing fields in the request, we return a bad request status of 400. If there is any other error, we return an internal server error status of 500. + +```rb + def update(id, attributes) + required_fields = %w[name iata icao callsign country] + missing_fields = required_fields - attributes.keys + extra_fields = attributes.keys - required_fields + + raise ArgumentError, "Missing fields: #{missing_fields.join(', ')}" if missing_fields.any? + + raise ArgumentError, "Extra fields: #{extra_fields.join(', ')}" if extra_fields.any? + + formatted_attributes = { + 'name' => attributes['name'], + 'iata' => attributes['iata'], + 'icao' => attributes['icao'], + 'callsign' => attributes['callsign'], + 'country' => attributes['country'] + } + AIRLINE_COLLECTION.upsert(id, formatted_attributes) + self.class.new(formatted_attributes) + end + + def destroy(id) + AIRLINE_COLLECTION.remove(id) + end +end +``` + +In the `update` method, we first check if all the required fields are present in the request. If any fields are missing, we raise an ArgumentError with the missing fields. If there are any extra fields in the request, we raise an ArgumentError with the extra fields. We then format the attributes and update the document in the database using the `upsert` method. We then create a new instance of the `Airline` class with the formatted attributes and return it. + +### DELETE Airline + +```rb + # DELETE /api/v1/airlines/{id} + def destroy + if @airline + if @airline.destroy(params[:id]) + render json: { message: 'Airline deleted successfully' }, status: :accepted + else + render json: { message: 'Failed to delete airline' }, status: :bad_request + end + else + render json: { error: "Airline with ID #{params[:id]} not found" }, status: :not_found + end + rescue Couchbase::Error::DocumentNotFound => e + render json: { error: "Airline with ID #{params[:id]} not found", message: e.message }, status: :not_found + rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error + end +``` + +In the above code, we first check if the airline document exists. If the document exists, we call the `destroy` method on the `Airline` class. If the document is deleted successfully, we return a status of 202. If the document is not deleted successfully, we return a bad request status of 400. If the document is not found, we return a not found status of 404. If there is any other error, we return an internal server error status of 500. + +```rb + def destroy(id) + AIRLINE_COLLECTION.remove(id) + end +end +``` + +### List Airlines + +```rb + # GET /api/v1/airlines/list + def index + country = params[:country] + limit = params[:limit] || 10 + offset = params[:offset] || 0 + + @airlines = Airline.all(country, limit, offset) + + if @airlines.empty? + render json: { message: 'No airlines found' }, status: :ok + else + render json: @airlines, status: :ok + end + rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error + end +``` + +In the above code, we first get the values for the country, limit, and offset from the request parameters. We then call the `all` method in the `Airline` class to fetch the list of airlines. If the list is empty, we return a status of 200 with a message. If the list is not empty, we return a status of 200 with the list of airlines. If there is any other error, we return an internal server error status of 500. + +```rb + def self.all(country = nil, limit = 10, offset = 0) + bucket_name = 'travel-sample' + scope_name = 'inventory' + collection_name = 'airline' + + query = country ? "SELECT * FROM `#{bucket_name}`.`#{scope_name}`.`#{collection_name}` WHERE country = $country LIMIT $limit OFFSET $offset" : "SELECT * FROM `#{bucket_name}`.`#{scope_name}`.`#{collection_name}` LIMIT $limit OFFSET $offset" + options = Couchbase::Cluster::QueryOptions.new + options.named_parameters(country ? { 'country' => country, 'limit' => limit.to_i, + 'offset' => offset.to_i } : { 'limit' => limit.to_i, 'offset' => offset.to_i }) + + result = COUCHBASE_CLUSTER.query(query, options) + result.rows.map { |row| new(row.fetch('airline', {})) }.to_a + end +``` + +In the `all` method, we first define the bucket, scope, and collection names. We then build the SQL++ query based on the country parameter. We then create a `QueryOptions` object and set the named parameters based on the country parameter. We then execute the query using the `query` method and iterate over the results to create a list of `Airline` objects and return it. + +### Direct Connections + +Navigate to the `direct_connections` method in the `AirportController` class in the `airports_controller.rb` file. We only need the airport document ID or our key from the user to retrieve a list of airlines directly connected to the specified airport using a SQL++ query. The result is converted into a JSON object using the `to_json` method. + +```rb +def direct_connections + destination_airport_code = params[:destinationAirportCode] + limit = params[:limit] || 10 + offset = params[:offset] || 0 + + if destination_airport_code.blank? + render json: { message: 'Destination airport code is required' }, status: :bad_request + else + @connections = Airport.direct_connections(destination_airport_code, limit, offset) + render json: @connections, status: :ok + end +rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error +end +``` + +## Running Tests + +We have defined integration tests using [RSpec](https://rspec.info/) for all the API end points. The integration tests use the same database configuration as the application. For the tests, we perform the operation using the API and confirm the results by checking the documents in the database. For example, to check the creation of the document by the API, we would call the API to create the document and then read the same document directly from the database using the CouchbaseClient and compare them. After the tests, the documents are cleaned up. + +To run all integration tests, use the following command: + +```bash +bundle exec rspec test/integration/ +``` + +## Appendix + +### Extending API by Adding New Entity + +If you would like to add another entity to the APIs, follow these steps: + +1. **Create the new collection in Couchbase**: Create the new entity (collection) in the Couchbase bucket. You can create the collection using the [SDK](https://docs.couchbase.com/sdk-api/couchbase-ruby-client/Couchbase/Collection.html#create_collection-instance_method) or via the [Couchbase Server interface](https://docs.couchbase.com/server/current/manage/manage-settings/manage-collections.html). + +2. **Create the model**: Create a new model file in the `app/models/` folder (e.g., `app/models/hotel.rb`). Define the model class with attributes and methods for CRUD operations similar to the `Airline` model. + +3. **Create the controller**: Create a new controller file in the `app/controllers/api/v1/` folder (e.g., `app/controllers/api/v1/hotels_controller.rb`). Implement the CRUD action methods (index, show, create, update, destroy) similar to `airlines_controller.rb`. + +4. **Add routes**: Open `config/routes.rb` and add the new routes for your entity within the `namespace :api` and `namespace :v1` blocks. For example: + + ```ruby + resources :hotels + ``` + +5. **Add integration tests**: Create a new test file in the `test/integration/` folder (e.g., `test/integration/hotels_spec.rb`) to test your API endpoints similar to `airlines_spec.rb`. + +6. **Add Swagger documentation**: Create a new spec file in the `spec/requests/api/v1/` folder (e.g., `spec/requests/api/v1/hotels_spec.rb`) for Swagger documentation similar to the airlines spec. + +7. **Generate Swagger docs**: Run the command `rake rswag:specs:swaggerize` to generate the Swagger documentation. + +Your new entity is now ready to be used in the API. + +### Running Self Managed Couchbase Cluster + +If you are running this quickstart with a self managed Couchbase cluster, you need to [load](https://docs.couchbase.com/server/current/manage/manage-settings/install-sample-buckets.html) the travel-sample data bucket in your cluster and generate the credentials for the bucket. + +- Follow [Couchbase Installation Options](/tutorial-couchbase-installation-options) for installing the latest Couchbase Database Server Instance. + +You need to update the connection string and the credentials in the `dev.env` file in the source folder. + +> Note: Couchbase Server must be installed and running prior to running the Ruby on Rails App + +### Swagger Documentation + +Swagger documentation provides a clear view of the API including endpoints, HTTP methods, request parameters, and response objects. + +Click on an individual endpoint to expand it and see detailed information. This includes the endpoint's description, possible response status codes, and the request parameters it accepts. + +#### Trying Out the API + +You can try out an API by clicking on the "Try it out" button next to the endpoints. + +- Parameters: If an endpoint requires parameters, Swagger UI provides input boxes for you to fill in. This could include path parameters, query strings, headers, or the body of a POST/PUT request. + +- Execution: Once you've inputted all the necessary parameters, you can click the "Execute" button to make a live API call. Swagger UI will send the request to the API and display the response directly in the documentation. This includes the response code, response headers, and response body. + +#### Models + +Swagger documents the structure of request and response bodies using models. These models define the expected data structure using JSON schema and are extremely helpful in understanding what data to send and expect. diff --git a/tutorial/markdown/ruby/ruby-couchbase-orm/travel_sample_data_model.png b/tutorial/markdown/ruby/ruby-couchbase-orm/travel_sample_data_model.png new file mode 100644 index 0000000..84c750d Binary files /dev/null and b/tutorial/markdown/ruby/ruby-couchbase-orm/travel_sample_data_model.png differ diff --git a/tutorial/markdown/ruby/ruby-on-rails/app_startup.png b/tutorial/markdown/ruby/ruby-on-rails/app_startup.png new file mode 100644 index 0000000..52af812 Binary files /dev/null and b/tutorial/markdown/ruby/ruby-on-rails/app_startup.png differ diff --git a/tutorial/markdown/ruby/ruby-on-rails/quickstart-ruby-on-rails.md b/tutorial/markdown/ruby/ruby-on-rails/quickstart-ruby-on-rails.md new file mode 100644 index 0000000..3186539 --- /dev/null +++ b/tutorial/markdown/ruby/ruby-on-rails/quickstart-ruby-on-rails.md @@ -0,0 +1,608 @@ +--- +# frontmatter +path: "/tutorial-quickstart-ruby-on-rails" +title: Quickstart in Couchbase with Ruby on Rails +short_title: Ruby on Rails +description: + - Learn to build a REST API in Ruby on Rails and Couchbase + - See how you can fetch data from Couchbase using SQL++ queries + - Explore CRUD operations in action with Couchbase +content_type: quickstart +filter: sdk +technology: + - kv + - index + - query +tags: + - Rails + - REST API +sdk_language: + - ruby +length: 30 Mins +--- + + + +In this tutorial, you will learn how to connect to a Couchbase Capella cluster to create, read, update, and delete documents and how to write simple parametrized SQL++ queries. + +## Prerequisites + +To run this prebuilt project, you will need: + +- [Couchbase Capella](https://www.couchbase.com/products/capella/) cluster with [travel-sample](https://docs.couchbase.com/ruby-sdk/current/ref/travel-app-data-model.html) bucket loaded. + - To run this tutorial using a self managed Couchbase cluster, please refer to the [appendix](#running-self-managed-couchbase-cluster). +- [Ruby](https://www.ruby-lang.org/en/documentation/installation/) installed on your machine. + - Ensure that the Ruby version is [compatible](https://docs.couchbase.com/ruby-sdk/current/project-docs/compatibility.html#ruby-version-compatibility) with the Couchbase SDK. +- Loading Travel Sample Bucket + If travel-sample is not loaded in your Capella cluster, you can load it by following the instructions for your Capella Cluster: + + - [Load travel-sample bucket in Couchbase Capella](https://docs.couchbase.com/cloud/clusters/data-service/import-data-documents.html#import-sample-data) + + +### Couchbase Capella Configuration + +When running Couchbase using [Capella](https://cloud.couchbase.com/), the following prerequisites need to be met. + +- The application requires the travel-sample bucket to be [loaded](https://docs.couchbase.com/cloud/clusters/data-service/import-data-documents.html#import-sample-data) in the cluster from the Capella UI. +- Create the [database credentials](https://docs.couchbase.com/cloud/clusters/manage-database-users.html) to access the travel-sample bucket (Read and Write) used in the application. +- [Allow access](https://docs.couchbase.com/cloud/clusters/allow-ip-address.html) to the Cluster from the IP on which the application is running. + +## App Setup + +### Cloning Repo + +```shell +git clone https://github.com/couchbase-examples/ruby-on-rails-quickstart.git +``` + +### Install Dependencies + +The dependencies for the application are specified in the `Gemfile` file in the source folder. Dependencies can be installed through `bundler` the default package manager for Ruby. + +```shell +cd ruby-on-rails-quickstart +gem install bundler +bundle install +``` + +> Refer to the [instructions in the SDK](https://github.com/couchbase/couchbase-ruby-client#installation) for more info. + +### Setup Database Configuration + +All configuration for communication with the database is fetched from the environment variables. As a security best practice, this quickstart reads the environment variables from a local file, `dev.env` in the source folder. + +Create a copy of `dev.env.example` file & rename it to `dev.env` & add the values for the Couchbase cluster. + +```sh +DB_CONN_STR= +DB_USERNAME= +DB_PASSWORD= +``` + +> Note: The connection string expects the `couchbases://` or `couchbase://` part. + +## Running the Application + +### Directly on Local Machine + +At this point, we have installed the dependencies, loaded the travel-sample data and configured the application with the credentials. The application is now ready and you can run it. + +The application will run on port 3000 of your local machine (http://localhost:3000). You will find the Swagger UI to try the API endpoints. + +The Swagger Page will be available at http://localhost:3000/api-docs. + +```shell +rails s +``` + +### Using Docker + +If you prefer to run this quick start using Docker, we have provided the Dockerfile which you can use to build the image and run the API as a container. + +- Build the Docker image + +```sh +docker build -t ruby-on-rails-quickstart . +``` + +- Run the Docker image + +```sh +docker run --env-file dev.env -p 3000:3000 ruby-on-rails-quickstart +``` + +> Note: The `.env` file has the connection information to connect to your Capella cluster. With the `--env-file` flag, Docker injects those environment variables to the container, overriding any default values defined in the application code. + +Once the app is up and running, open http://localhost:3000/api-docs to test the APIs. + +### Verifying the Application + +Once the application starts, you can see the details of the application on the logs. + +![Application Startup](app_startup.png) + +The application will run on port 3000 of your local machine (http://localhost:3000). You will find the interactive Swagger documentation of the API if you go to the URL in your browser. Swagger documentation is used in this demo to showcase the different API end points and how they can be invoked. More details on the Swagger documentation can be found in the [appendix](#swagger-documentation). + +![Swagger Documentation](swagger_documentation.png) + +## Data Model + +For this tutorial, we use three collections, `airport`, `airline` and `route` that contain sample airports, airlines and airline routes respectively. The route collection connects the airports and airlines as seen in the figure below. We use these connections in the quickstart to generate airports that are directly connected and airlines connecting to a destination airport. Note that these are just examples to highlight how you can use SQL++ queries to join the collections. + +![img](travel_sample_data_model.png) + +## Code Structure + +To begin this tutorial, clone the repo and open it up in the IDE of your choice. Now you can learn about how to create, read, update and delete documents in Couchbase Server. + +### Code Layout + +``` +. +├── app +│ ├── controllers +│ │ ├── api +│ │ │ └── v1 +│ │ │ ├── airlines_controller.rb +│ │ │ ├── airports_controller.rb +│ │ │ └── routes_controller.rb +│ │ ├── application_controller.rb +│ │ └── concerns +│ ├── models +│ │ ├── airline.rb +│ │ ├── airport.rb +│ │ ├── application_record.rb +│ │ ├── concerns +│ │ └── route.rb +│ └── views +│ └── layouts +│ ├── application.html.erb +│ ├── mailer.html.erb +│ └── mailer.text.erb +├── config +│ ├── importmap.rb +│ ├── initializers +│ │ ├── couchbase.rb +│ │ ├── rswag_api.rb +│ │ └── rswag_ui.rb +│ ├── routes.rb +├── config.ru +├── db +│ └── seeds.rb +├── dev.env +├── dev.env.example +├── Dockerfile +├── Gemfile +├── Gemfile.lock +├── spec +│ ├── rails_helper.rb +│ ├── requests +│ │ └── api +│ │ └── v1 +│ │ ├── airlines_spec.rb +│ │ ├── airports_spec.rb +│ │ └── routes_spec.rb +│ ├── spec_helper.rb +│ └── swagger_helper.rb +├── swagger +│ └── v1 +│ └── swagger.yaml +├── test +│ ├── channels +│ │ └── application_cable +│ │ └── connection_test.rb +│ ├── controllers +│ ├── integration +│ │ ├── airlines_spec.rb +│ │ ├── airports_spec.rb +│ │ └── routes_spec.rb +│ ├── models +``` + +The API endpoints are organized by entity (collection) with separate controller files in the `app/controllers/api/v1/` folder. Each entity has corresponding test files in the `test/integration/` and `spec/requests/api/v1/` folders. + +In `routes.rb`, we define the routes for the application including the API routes. + +The Couchbase connection and collection references are configured in the `config/initializers/couchbase.rb` file. + +In this application, we have created the connection object in `config/initializers/couchbase.rb` and we use this object in all of our APIs. The object is initialized in `config/initializers/couchbase.rb`. We have also stored the reference to our bucket, `travel-sample` and the scope, `inventory` in the connection object. + +```rb +require 'couchbase' + +# Get environment variables +DB_USERNAME = ENV['DB_USERNAME'] +DB_PASSWORD = ENV['DB_PASSWORD'] +DB_CONN_STR = ENV['DB_CONN_STR'] +DB_BUCKET_NAME = 'travel-sample' # Hardcoded bucket name + +# Check if running in CI environment +if ENV['CI'] + # Use environment variables from GitHub Secrets + options = Couchbase::Cluster::ClusterOptions.new + options.authenticate(DB_USERNAME, DB_PASSWORD) + COUCHBASE_CLUSTER = Couchbase::Cluster.connect(DB_CONN_STR, options) +else + # Load environment variables from dev.env file + require 'dotenv' + Dotenv.load('dev.env') + + # Define default values + DEFAULT_DB_USERNAME = 'Administrator' + DEFAULT_DB_PASSWORD = 'password' + DEFAULT_DB_CONN_STR = 'couchbase://localhost' + + # Get environment variables with fallback to default values + DB_USERNAME = ENV.fetch('DB_USERNAME', DEFAULT_DB_USERNAME) + DB_PASSWORD = ENV.fetch('DB_PASSWORD', DEFAULT_DB_PASSWORD) + DB_CONN_STR = ENV.fetch('DB_CONN_STR', DEFAULT_DB_CONN_STR) + + # Connect to the Couchbase cluster + options = Couchbase::Cluster::ClusterOptions.new + options.authenticate(DB_USERNAME, DB_PASSWORD) + COUCHBASE_CLUSTER = Couchbase::Cluster.connect(DB_CONN_STR, options) +end + +# Open the bucket +bucket = COUCHBASE_CLUSTER.bucket(DB_BUCKET_NAME) + +# Open the default collection +default_collection = bucket.default_collection + +# Create scope and collections if they don't exist +begin + scope = bucket.scope('inventory') +rescue Couchbase::Error::ScopeNotFoundError + bucket.create_scope('inventory') + scope = bucket.scope('inventory') +end + +%w[airline airport route].each do |collection_name| + scope.collection(collection_name) +rescue Couchbase::Error::CollectionNotFoundError + scope.create_collection(collection_name) +end + +AIRLINE_COLLECTION = scope.collection('airline') +AIRPORT_COLLECTION = scope.collection('airport') +ROUTE_COLLECTION = scope.collection('route') + +``` + +In the above code, we first get the environment variables for the connection string, username, and password. We then create a connection object using the `Couchbase::Cluster.connect` method. We authenticate the connection using the username and password. We then open the bucket and the default collection. We create a scope and collections if they don't exist. We store the reference to the collections in the connection object. + +### Airline Entity + +For this tutorial, we will focus on the airline entity to demonstrate CRUD operations and SQL++ queries. + +We will be setting up a REST API to manage airline documents. + +- [POST Airline](#post-airline) – Create a new airline +- [GET Airline](#get-airline) – Read specified airline +- [PUT Airline](#put-airline) – Update specified airline +- [DELETE Airline](#delete-airline) – Delete airline +- [Airline List](#list-airline) – Get all airlines. Optionally filter the list by country +- [Direct Connections](#direct-connections) - Get a list of airlines directly connected to the specified airport + +For CRUD operations, we will use the [Key-Value operations](https://docs.couchbase.com/ruby-sdk/current/howtos/kv-operations.html) that are built into the Couchbase SDK to create, read, update, and delete a document. Every document will need an ID (similar to a primary key in other databases) to save it to the database. This ID is passed in the URL. For other end points, we will use [SQL++](https://docs.couchbase.com/ruby-sdk/current/howtos/n1ql-queries-with-sdk.html) to query for documents. + +### Airline Document Structure + +Our airline document will have an airline name, IATA code, ICAO code, callsign, and country. For this demo, we will store all airline information in one document in the `airline` collection in the `travel-sample` bucket. + +```json +{ + "name": "40-Mile Air", + "iata": "Q5", + "icao": "MLA", + "callsign": "MILE-AIR", + "country": "United States" +} +``` + +### POST Airline + + +```rb +# frozen_string_literal: true + +module Api + module V1 + class AirlinesController < ApplicationController + skip_before_action :verify_authenticity_token, only: %i[create update destroy] + before_action :set_airline, only: %i[show update destroy] + + ... + # POST /api/v1/airlines/{id} + def create + @airline = Airline.create(params[:id], airline_params) + if @airline + render json: @airline, status: :created + else + render json: { message: 'Airline already exists' }, status: :conflict + end + rescue ArgumentError => e + render json: { error: 'Invalid request', message: e.message }, status: :bad_request + rescue Couchbase::Error::DocumentExists => e + render json: { error: "Airline with ID #{params[:id]} already exists", message: e.message }, status: :conflict + rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error + end + end + ... + + end +end +``` + +In the above code, we first create an instance of the `Airline` class using the `create` method. We pass the ID and the airline data to the `create` method. If the airline is created successfully, we return the airline document with a status of 201. If the airline already exists, we return a conflict status of 409. If there are any missing fields in the request, we return a bad request status of 400. If there is any other error, we return an internal server error status of 500. + +```rb +# frozen_string_literal: true + +class Airline + attr_accessor :name, :iata, :icao, :callsign, :country + + def initialize(attributes) + @name = attributes['name'] + @iata = attributes['iata'] + @icao = attributes['icao'] + @callsign = attributes['callsign'] + @country = attributes['country'] + end + ... + + def self.create(id, attributes) + required_fields = %w[name iata icao callsign country] + missing_fields = required_fields - attributes.keys + extra_fields = attributes.keys - required_fields + + raise ArgumentError, "Missing fields: #{missing_fields.join(', ')}" if missing_fields.any? + + raise ArgumentError, "Extra fields: #{extra_fields.join(', ')}" if extra_fields.any? + + formatted_attributes = { + 'name' => attributes['name'], + 'iata' => attributes['iata'], + 'icao' => attributes['icao'], + 'callsign' => attributes['callsign'], + 'country' => attributes['country'] + } + AIRLINE_COLLECTION.insert(id, formatted_attributes) + new(formatted_attributes) + end + + ... +end + +``` + +In the above code, we first check if all the required fields are present in the request. If any fields are missing, we raise an ArgumentError with the missing fields. If there are any extra fields in the request, we raise an ArgumentError with the extra fields. We then format the attributes and insert the document into the database using the `insert` method. We then create a new instance of the `Airline` class with the formatted attributes and return it. + +### GET Airline + +Navigate to the `show` method in the `AirlinesController` class in the `airlines_controller.rb` file. We only need the airline document ID or our key from the user to retrieve a particular airline document using a key-value operation which is passed in the `find` method. The result is converted into a JSON object using the `to_json` method. + +```rb + # GET /api/v1/airlines/{id} + def show + if @airline + render json: @airline, status: :ok + else + render json: { error: "Airline with ID #{params[:id]} not found" }, status: :not_found + end + rescue Couchbase::Error::DocumentNotFound => e + render json: { error: "Airline with ID #{params[:id]} not found", message: e.message }, status: :not_found + rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error + end +``` + +The `find` method in the `Airline` class is used to fetch the document from the database using the key. If the document is found, we return the document with a status of 200. If the document is not found, we return a not found status of 404. If there is any other error, we return an internal server error status of 500. + +```rb + def self.find(id) + result = AIRLINE_COLLECTION.get(id) + new(result.content) if result.success? + rescue Couchbase::Error::DocumentNotFound + nil + end +``` + +In the `find` method, we use the `get` method to fetch the document from the database using the key. If the document is found, we create a new instance of the `Airline` class with the document content and return it. If the document is not found, we return `nil`. + +### PUT Airline + +Navigate to the `update` method in the `AirlinesController` class in the `airlines_controller.rb` file. We only need the airline document ID or our key from the user to update a particular airline document using a key-value operation which is passed in the `update` method. The result is converted into a JSON object using the `to_json` method. + +```rb + # PUT /api/v1/airlines/{id} + def update + @airline = Airline.new(airline_params).update(params[:id], airline_params) + render json: @airline, status: :ok + rescue ArgumentError => e + render json: { error: 'Invalid request', message: e.message }, status: :bad_request + rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error + end +``` + +Inn the above code, we first create an instance of the `Airline` class using the `update` method. We pass the ID and the airline data to the `update` method. If the airline is updated successfully, we return the airline document with a status of 200. If there are any missing fields in the request, we return a bad request status of 400. If there is any other error, we return an internal server error status of 500. + +```rb + def update(id, attributes) + required_fields = %w[name iata icao callsign country] + missing_fields = required_fields - attributes.keys + extra_fields = attributes.keys - required_fields + + raise ArgumentError, "Missing fields: #{missing_fields.join(', ')}" if missing_fields.any? + + raise ArgumentError, "Extra fields: #{extra_fields.join(', ')}" if extra_fields.any? + + formatted_attributes = { + 'name' => attributes['name'], + 'iata' => attributes['iata'], + 'icao' => attributes['icao'], + 'callsign' => attributes['callsign'], + 'country' => attributes['country'] + } + AIRLINE_COLLECTION.upsert(id, formatted_attributes) + self.class.new(formatted_attributes) + end + + def destroy(id) + AIRLINE_COLLECTION.remove(id) + end +end +``` + +In the `update` method, we first check if all the required fields are present in the request. If any fields are missing, we raise an ArgumentError with the missing fields. If there are any extra fields in the request, we raise an ArgumentError with the extra fields. We then format the attributes and update the document in the database using the `upsert` method. We then create a new instance of the `Airline` class with the formatted attributes and return it. + +### DELETE Airline + +Navigate to the `destroy` method in the `AirlinesController` class in the `airlines_controller.rb` file. We only need the airline document ID or our key from the user to delete a particular airline document using a key-value operation which is passed in the `destroy` method. + +```rb + # DELETE /api/v1/airlines/{id} + def destroy + if @airline + if @airline.destroy(params[:id]) + render json: { message: 'Airline deleted successfully' }, status: :accepted + else + render json: { message: 'Failed to delete airline' }, status: :bad_request + end + else + render json: { error: "Airline with ID #{params[:id]} not found" }, status: :not_found + end + rescue Couchbase::Error::DocumentNotFound => e + render json: { error: "Airline with ID #{params[:id]} not found", message: e.message }, status: :not_found + rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error + end +``` + +In the above code, we first check if the airline document exists. If the document exists, we call the `destroy` method on the `Airline` class. If the document is deleted successfully, we return a status of 202. If the document is not deleted successfully, we return a bad request status of 400. If the document is not found, we return a not found status of 404. If there is any other error, we return an internal server error status of 500. + +```rb + def destroy(id) + AIRLINE_COLLECTION.remove(id) + end +end +``` + +### List Airlines + +Navigate to the `index` method in the `AirlinesController` class in the `airlines_controller.rb` file. We only need the country, limit, and offset values from the user to retrieve a list of airline documents using a SQL++ query. The result is converted into a JSON object using the `to_json` method. + +```rb + # GET /api/v1/airlines/list + def index + country = params[:country] + limit = params[:limit] || 10 + offset = params[:offset] || 0 + + @airlines = Airline.all(country, limit, offset) + + if @airlines.empty? + render json: { message: 'No airlines found' }, status: :ok + else + render json: @airlines, status: :ok + end + rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error + end +``` + +In the above code, we first get the values for the country, limit, and offset from the request parameters. We then call the `all` method in the `Airline` class to fetch the list of airlines. If the list is empty, we return a status of 200 with a message. If the list is not empty, we return a status of 200 with the list of airlines. If there is any other error, we return an internal server error status of 500. + + +```rb + def self.all(country = nil, limit = 10, offset = 0) + bucket_name = 'travel-sample' + scope_name = 'inventory' + collection_name = 'airline' + + query = country ? "SELECT * FROM `#{bucket_name}`.`#{scope_name}`.`#{collection_name}` WHERE country = $country LIMIT $limit OFFSET $offset" : "SELECT * FROM `#{bucket_name}`.`#{scope_name}`.`#{collection_name}` LIMIT $limit OFFSET $offset" + options = Couchbase::Cluster::QueryOptions.new + options.named_parameters(country ? { 'country' => country, 'limit' => limit.to_i, + 'offset' => offset.to_i } : { 'limit' => limit.to_i, 'offset' => offset.to_i }) + + result = COUCHBASE_CLUSTER.query(query, options) + result.rows.map { |row| new(row.fetch('airline', {})) }.to_a + end +``` + +In the `all` method, we first define the bucket, scope, and collection names. We then build the SQL++ query based on the country parameter. We then create a `QueryOptions` object and set the named parameters based on the country parameter. We then execute the query using the `query` method and iterate over the results to create a list of `Airline` objects and return it. + +### Direct Connections + +Navigate to the `direct_connections` method in the `AirportController` class in the `airports_controller.rb` file. We only need the airport document ID or our key from the user to retrieve a list of airlines directly connected to the specified airport using a SQL++ query. The result is converted into a JSON object using the `to_json` method. + +```rb +def direct_connections + destination_airport_code = params[:destinationAirportCode] + limit = params[:limit] || 10 + offset = params[:offset] || 0 + + if destination_airport_code.blank? + render json: { message: 'Destination airport code is required' }, status: :bad_request + else + @connections = Airport.direct_connections(destination_airport_code, limit, offset) + render json: @connections, status: :ok + end +rescue StandardError => e + render json: { error: 'Internal server error', message: e.message }, status: :internal_server_error +end +``` + +## Running Tests + +Integration tests are implemented using [RSpec](https://rspec.info/) for all API endpoints. Tests verify operations by calling the API and confirming results in the database. + +To run all tests: + +```bash +rspec test/integration/ +``` + +## Appendix + +### Extending API by Adding New Entity + +If you would like to add another entity to the APIs, these are the steps to follow: + +- Create the new entity (collection) in the Couchbase bucket. You can create the collection using the [SDK](https://docs.couchbase.com/sdk-api/couchbase-ruby-client/Couchbase/Collection.html#create_collection-instance_method) or via the [Couchbase Server interface](https://docs.couchbase.com/server/current/manage/manage-settings/manage-collections.html). +- Define the routes in a new file in the `api` folder similar to the existing routes like `airline_controller.rb`. +- Add the new routes to the application in `routes.rb`. +- Add the tests for the new routes in a new file in the `test/integration/api/v1` folder similar to `airlines_spec.rb`. +- For Swagger documentation, add the configuration in the `spec/request/api/v1` folder similar to `airlines_spec.rb`. +- Run the command `rake rswag:specs:swaggerize` to generate the Swagger documentation. +- The new entity is now ready to be used in the API. + +### Running Self Managed Couchbase Cluster + +If you are running this quickstart with a self managed Couchbase cluster, you need to [load](https://docs.couchbase.com/server/current/manage/manage-settings/install-sample-buckets.html) the travel-sample data bucket in your cluster and generate the credentials for the bucket. + +- Follow [Couchbase Installation Options](/tutorial-couchbase-installation-options) for installing the latest Couchbase Database Server Instance. + +You need to update the connection string and the credentials in the `dev.env` file in the source folder. + +> Note: Couchbase Server must be installed and running prior to running the Ruby on Rails App + +### Swagger Documentation + +Swagger documentation provides a clear view of the API including endpoints, HTTP methods, request parameters, and response objects. + +Click on an individual endpoint to expand it and see detailed information. This includes the endpoint's description, possible response status codes, and the request parameters it accepts. + +#### Trying Out the API + +You can try out an API by clicking on the "Try it out" button next to the endpoints. + +- Parameters: If an endpoint requires parameters, Swagger UI provides input boxes for you to fill in. This could include path parameters, query strings, headers, or the body of a POST/PUT request. + +- Execution: Once you've inputted all the necessary parameters, you can click the "Execute" button to make a live API call. Swagger UI will send the request to the API and display the response directly in the documentation. This includes the response code, response headers, and response body. + +#### Models + +Swagger documents the structure of request and response bodies using models. These models define the expected data structure using JSON schema and are extremely helpful in understanding what data to send and expect. diff --git a/tutorial/markdown/ruby/ruby-on-rails/swagger_documentation.png b/tutorial/markdown/ruby/ruby-on-rails/swagger_documentation.png new file mode 100644 index 0000000..0e6a59e Binary files /dev/null and b/tutorial/markdown/ruby/ruby-on-rails/swagger_documentation.png differ diff --git a/tutorial/markdown/ruby/ruby-on-rails/travel_sample_data_model.png b/tutorial/markdown/ruby/ruby-on-rails/travel_sample_data_model.png new file mode 100644 index 0000000..84c750d Binary files /dev/null and b/tutorial/markdown/ruby/ruby-on-rails/travel_sample_data_model.png differ