diff --git a/docs/database/_category_.json b/docs/database/_category_.json new file mode 100644 index 00000000..71e5b73b --- /dev/null +++ b/docs/database/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Database", + "position": 8, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "/database", + "description": "See how to use the Athenna database component." + } +} diff --git a/docs/database/getting-started.mdx b/docs/database/getting-started.mdx new file mode 100644 index 00000000..5c911a86 --- /dev/null +++ b/docs/database/getting-started.mdx @@ -0,0 +1,408 @@ +--- +title: Getting Started +sidebar_position: 1 +description: See how to create database connections in Athenna Framework. +--- + +# Database: Getting Started + +See how to create database connections in Athenna Framework. + +## Introduction + +Almost every modern application interacts with a +database. Athenna makes interacting with databases +extremely simple across a variety of supported databases +using a fluent query builder and the ORM. + +## Installation + +First of all you need to install `@athenna/database` package: + +```shell +npm i @athenna/database +``` + +Artisan provides a very simple command to configure the database +library in your project. Simply run the following: + +```shell +node artisan configure @athenna/database +``` + +The database configurer will do the following operations in +your project: + +- Ask you for the default database you want to use. (`mongo`, `mysql`, `sqlite` or `postgres`). +- Create the `database.ts` configuration file. +- Add all database providers in your `.athennarc.json` file. +- Add all database commands in your `.athennarc.json` file. +- Add all database template files in your `.athennarc.json` file. +- Add database environment variables to `.env`, `.env.test` and `.env.example`. +- Configure the service `docker-compose.yml` file acordding to the databas selected. + +## Configuration + +All the configuration options for your application's database +behavior is housed in the `Path.config('database.js')` +configuration file. This file allows you to configure your +application's database connections, so be sure to review each +of the available connections and their options. We'll review +a few common options below. + +### Available connection drivers + +Each database connection is powered by a "driver". The driver +determines how and where the data is actually transported. +The following database connection drivers are available in +every Athenna application. An entry for most of these drivers +is already present in your application's `Path.config('database.ts')` +configuration file, so be sure to review this file to become +familiar with its contents: + +| Driver name | Website | Built with | +|:------------|:--------------------------------:|-----------------------------------------------------:| +| `mongo` | https://www.mongodb.com/ | [mongoose](https://www.npmjs.com/package/mongoose) | +| `mysql` | https://www.mysql.com/ | [knex](https://www.npmjs.com/package/knex) | +| `sqlite` | https://www.sqlite.org/ | [knex](https://www.npmjs.com/package/knex) | +| `postgres` | https://www.postgresql.org/ | [knex](https://www.npmjs.com/package/knex) | + +### Overview of some environment variables + +After installing the database component using the Athenna CLI you +can check your `.env` file in your project root path, the Athenna +CLI have added some environment variables there to help you connect +to your database. These variables are: + +```dotenv +DB_CONNECTION=postgres +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DEBUG=false +DB_USERNAME=root +DB_PASSWORD=root +DB_DATABASE=database +``` + +Let's focus in **DB_CONNECTION and DB_DEBUG** variables: + +#### DB_CONNECTION + +This variable specify for Athenna what is the default connection name +that should be used by `Database` facade when running database operations. + +#### DB_DEBUG + +This variable is useful when running your application locally, If +`DB_DEBUG` is `true`, then you will be able to see all the queries +being executed in your database. + +:::tip + +Before going through the documentation, remember to run +[`docker-compose up -d`](https://docs.docker.com/compose/) to +start up the database in your machine. + +::: + +## Database operations + +Once you have configured your database connection, you may communicate +with it using the `Database` facade. The `Database` facade provides a +lot of methods to perform database operations such as **creating, +dropping and listing databases/tables, running and reverting** +[**migrations**](/docs/database/migrations)**,** +[**transactions**](/docs/database/getting-started#id-database-transactions)**, +queries, connecting to new databases and also closing these +connections.** + +### Creating and dropping databases + +```typescript +import { Database } from '@athenna/database' + +await Database.createDatabase('hello') +await Database.dropDatabase('hello') +``` + +You can also get all databases names as string and check if some +database name exists: + +```typescript +const databases = await Database.getDatabases() // ['postgres'] +const current = await Database.getCurrentDatabase() // 'postgres' + +await Database.hasDatabase('postgres') // true +await Database.hasDatabase('not-found') // false +``` + +### Creating and dropping tables + +```typescript +import { Database } from '@athenna/database' + +await Database.createTable('products', table => { + table.increments('id').primary() +}) + +await Database.dropTable('products') +``` + +You can also get all tables names as string and check if some +table name exists: + +```typescript +const tables = await Database.getTables() // ['users'] + +await Database.hasTable('users') // true +await Database.hasTable('not-found') // false +``` + +### Running and reverting migrations + +If you don't know what is a migration you can +check [`clicking here`](/docs/database/migrations) + +```typescript +await Database.runMigrations() +await Database.revertMigrations() +``` + +### Transactions + +If you don't know what is a transaction you can +check [`clicking here`](/docs/database/getting-started#id-database-transactions) + +```typescript +import { Log } from '@athenna/logger' + +const trx = await Database.startTransaction() +const query = trx.table('users') + +const users = [] // Imagine a lot of users to be inserted here... + +try { + await query.createMany(users) + + await trx.commitTransaction() +} catch (error) { + // If some user in the "users" array has been created, + // it will be removed if one fails to insert. + await trx.rollbackTransaction() + + Log.error('Failed to create one of the users. Original error: %s', JSON.stringify(error)) +} +``` + +## Running queries + +You may use the `table()` method provided by the `Database` facade +to begin a query. The `table()` method returns a fluent query +builder instance for the given table, allowing you to chain more +constraints onto the query and then finally retrieve the results of +the query using one of the **executors** methods. These are the most +relevant methods: + +- `find()` +- `findMany()` +- `create()` +- `createMany()` +- `update()` +- `delete()` + +Everytime that you use the `Database` facade you are using a different +instance of `DatabaseImpl` class. This means that you would need to +call `table()` method everytime for different queries. To get around +this, you can save the instance in a local +variable: + +```typescript +import { Database } from '@athenna/database' + +const userQuery = Database.table('users') // First instance of QueryBuilder +const orderQuery = Database.table('orders') // Second instance of QueryBuilder +const productsQuery = Database.table('products') // Third instance of QueryBuilder +``` + +#### Running a find query + +The `find()` method is useful to retrieve only one record that match +the query statements from database: + +```typescript +const query = Database.table('users') + +const { id, name } = await query + .select('id', 'name') + .where({ id: 10 }) + .find() +``` + +#### Running a findMany query + +The `findMany()` method is useful to retrieve more than one record +that match the query statements from database: + +```typescript +const query = Database.table('users') + +const users = await query + .select('id', 'name') + .whereNull('deletedAt') + .whereLike('name', '%Lenon%') + .orderBy('name', 'DESC') + .findMany() +``` + +#### Running a create query + +The `create()` method is useful to create one record in database: + +```typescript +const query = Database.table('users') + +const user = await query.create({ name: 'Victor Tesoura' }) +``` + +#### Running a createMany query + +The `createMany()` method is useful to create many records in database: + +```typescript +const query = Database.table('users') + +const users = await query.createMany([ + { name: 'Victor Tesoura' }, + { name: 'João Lenon' } +]) +``` + +#### Running an update query + +The `update()` method is useful to update one or more records based +in statements in database: + +```typescript +const query = Database.table('users') + +const users = await query + .select('id', 'name') + .whereIn('id', [1, 2]) + .orderBy('name', 'ASC') + .update({ name: 'Danrley Morais' }) +``` + +#### Running delete query + +The `delete()` method is useful to delete one or more records based +in statements in database: + +```typescript +const query = Database.table('users') + +await query.whereBetween('id', [1, 10]).delete() +``` + +## Using multiple database connections + +If your application defines multiple connections in your +`Path.config('database.ts')` configuration file, you may access each +connection via the `connection()` method provided by the `Database` +facade. The connection name passed to the `connection()` method should +correspond to one of the connections listed in your +`Path.config('database.ts')` configuration file. You also need +to explicit call the `connect()` method when working with other +connection that is not the default: + +```typescript +const query = Database.connection('mysql').table('users') + +const users = await query + .select('id', 'name') + .whereNotIn('id', [1, 2]) + .orderBy('name', 'ASC') + .findMany() +``` + +The connection created by the `connection()` method will be stored +inside the [`DriverFactory`](https://github.com/AthennaIO/Database/blob/develop/src/factories/DriverFactory.ts) +class. This means that if you call the `connection()` method again, +it will use the same connection created that was saved in `DriverFactory`: + +```typescript +await Datatabase.connection('mysql') + .table('users') + .select('id', 'name') + .whereNotIn('id', [1, 2]) + .orderBy('name', 'ASC') + .findMany() +``` + +You can force `connection()` method to not save the connection +instance in [`DriverFactory`](https://github.com/AthennaIO/Database/blob/develop/src/factories/DriverFactory.ts) +passing some properties to it: + +```typescript +const database = Datatabase.connection('mysql', { + // Force the connection to be established + force: false, + // Set if connection will be saved in DriverFactory + // to be reused in all Database instances + saveOnDriver: false +}) + +await database + .table('users') + .select('id', 'name') + .whereNotIn('id', [1, 2]) + .orderBy('name', 'ASC') + .findMany() +``` + +:::caution + +If you specify to `connection()` method that you don't want to save +the connection in driver, you will need to close the connection +using **your** database instance. We will see next how to close a +database connection. + +::: + +## Closing database connections + +You can simply close a connection using the `close()` or `closeAll()` +methods of `Database` facade: + +```typescript +await Database.close() // Close the default connection +await Database.connection('mysql').close() // Close the mysql connection + +await Database.closeAll() // Close all the connections saved in DriverFactory +``` + +Remember that when creating a connection that will not be saved in +driver, you will need to close the connection using **your** +database instance: + +```typescript +const database = Datatabase.connection('mysql', { + force: false, + saveOnDriver: false +}) + +await database + .table('users') + .select('id', 'name') + .whereNotIn('id', [1, 2]) + .orderBy('name', 'ASC') + .findMany() + +// The code below will not close the connection +// created above since DriverFactory doesn't +// know what is your connection. +await Database.connection('mysql').close() // DOES NOT WORK + +await database.close() // WORKS + +``` + diff --git a/docs/database/migrations.mdx b/docs/database/migrations.mdx new file mode 100644 index 00000000..0cfadf16 --- /dev/null +++ b/docs/database/migrations.mdx @@ -0,0 +1,139 @@ +--- +title: Migrations +sidebar_position: 3 +description: See how to create and run database migrations in Athenna Framework. +--- + +# Database: Migrations + +See how to create and run database migrations in Athenna Framework. + +## Introduction + +Migrations are like version control for your database, allowing your +team to define and share the application's database schema +definition. If you have ever had to tell a teammate to manually add a +column to their local database schema after pulling in your changes +from source control, you've faced the problem that database migrations +solve. + +## Generating migrations + +You may use the `make:migration` Artisan command to generate a database migration. The new migration will be placed in your +`Path.migrations()` directory. Each migration filename contains a +timestamp that allows Athenna to determine the order of +the migrations: + +```shell +node artisan make:migration create_flights_table +``` + +:::tip + +Migrations templates may be customized using the +[**template customization command**](https://athenna.io/docs/the-basics/cli/commands#id-registering-custom-templates). + +::: + +## Migration structure + +A migration class contains two methods: `up()` and `down()`. The `up()` +method is used to add new tables, columns, or indexes to your +database, while the `down()` method should reverse the operations +performed by the `up()` method. + +Within both of these methods, you may use the [`knex schema builder`](https://knexjs.org/guide/schema-builder.html) to +expressively create and modify tables. For example, the following migration creates a `flights` table: + +```typescript +import { BaseMigration, type DatabaseImpl } from '@athenna/database' + +export class CreateFlightsTable extends BaseMigration { + public tableName = 'flights' + + public async up(db: DatabaseImpl) { + return db.createTable(this.tableName, (table) => { + table.increments('id') + table.string('name') + table.string('airline') + table.timestamps(true, true, true) + }) + } + + public async down({ schema }: DatabaseImpl) { + return db.dropTableIfExists(this.tableName) + } +} +``` + +### Setting the migration connection + +If your migration will be interacting with a database connection +other than your application's default database connection, +you should set the static getter `connection` in your migration: + +```typescript +import { BaseMigration, type DatabaseImpl } from '@athenna/database' + +export class CreateFlightsTable extends BaseMigration { + public static connection() { + return 'postgres' + } + + public async up(db: DatabaseImpl) { + // ... + } + + public async down(db: DatabaseImpl) { + // ... + } +} +``` + +## Running migrations + +To run all of your outstanding migrations, execute the +`migration:run` Artisan command: + +```shell +node artisan migration:run +``` + +You can use the `--connection` option to run migrations +for a specific connection: + +```shell +node artisan migration:run --connection=postgres +``` + +:::warning + +If `postgres` is your default connection than all the migrations +using the `default` value in the static `connection()` method +will run too. + +::: + +## Reverting + +To revert all your migrations, you may use the `migration:revert` +Artisan command. This command will revert all your migrations: + +```shell +node artisan migration:revert +``` + +You can use the `--connection` option to revert migrations for a +specific connection: + +```shell +node artisan migration:revert --connection=postgres +``` + +:::warning + +If `postgres` is your default connection than all the migrations +using the `default` value in the static `connection()` method +will be reverted too. + +::: diff --git a/docs/database/query-builder.mdx b/docs/database/query-builder.mdx new file mode 100644 index 00000000..e53466fa --- /dev/null +++ b/docs/database/query-builder.mdx @@ -0,0 +1,809 @@ +--- +title: Query Builder +sidebar_position: 2 +description: See how to use the Athenna database query builder. +--- + +# Database: Query Builder + +## Introduction + +Athenna database query builder provides a convenient, fluent interface +to creating and running database queries. It can be used to perform most +database operations in your application and works perfectly with all of +Athenna supported database drivers. + +## Running database queries + +#### Retrieve all rows from a table + +You may use the `table()` method provided by the `Database` facade to +begin a query. The `table()` method returns a fluent query builder +instance for the given table, allowing you to chain more constraints +onto the query and then finally retrieve the results of the query using +the `findMany()` method: + +```typescript +import { Database } from '@athenna/database' + +const users = await Database.table('users') + .select('id', 'name') + .whereILike({ name: '%Valmir Barbosa%' }) + .orderBy('name', 'ASC') + .findMany() +``` + +Athenna also provides the `collection()` method that returns a +[`Collection`](/docs/digging-deeper/collections) instance containing +the results of the query. You may access each column's value using +the `all()` method: + +```typescript +const collection = await Database.table('users') + .select('id', 'name') + .whereILike({ name: '%Valmir Barbosa%' }) + .orderBy('name', 'ASC') + .collection() + +const users = collection.all() +``` + +:::tip + +Athenna collections provide a variety of extremely powerful methods +for mapping and reducing data. For more information on Athenna +collections, check out the +[collection documentation](/docs/digging-deeper/collections). + +::: + +#### Retrieve a single row + +If you just need to retrieve a single row from a database table, +you may use the `Database` facade's `find()` method. This method +will return a single object: + +```typescript +const user = await Database.table('users') + .select('id', 'name') + .where({ name: 'Rodrigo Kamada' }) + .find() +``` + +#### Get the client and query builder of driver + +To get the vanilla client or query builder of your connection +driver you can use the `getClient()` and `getQueryBuilder()` +method: + +```typescript +// Knex client +const client = Database.connection('postgres').getClient() + +await client.close() + +// Knex query builder +const queryBuilder = Database.connection('postgres').getQueryBuilder() + +const result = await queryBuilder + .where({ id: 1, status: 'ACTIVE' }) + .andWhere('status', 'PENDING') +``` + +#### Agreggates + +The query builder also provides a variety of methods for retrieving +aggregate values like **count, max, min, avg,** and **sum**. +You may call any of these methods after constructing your query: + +```typescript +const numberOfUsers = await Database.table('users').count() +const maxPriceOrder = await Database.table('orders').max('price') +``` + +Of course, you may combine these methods with other clauses to +fine-tune how your aggregate value is calculated: + +```typescript +const priceAverage = await Database.table('orders') + .where('finalized', true) + .avg('price') +``` + +## Select statements + +You may not always want to select all columns from a database table. +Using the `select()` method, you can specify a custom "select" clause +for the query: + +```typescript +const { id, name } = await Database.table('users') + .select('id', 'name') + .find() +``` + +If you want to select all fields you can use the `*` operator: + +```typescript +const { id, name, email } = await Database.table('users') + .select('*') + .find() +``` + +## Raw expressions + +Sometimes you may need to insert an arbitrary string into a query. +To create a raw string expression, you may use the `raw()` method +provided by the `Database` facade: + +```typescript +const users = await Database.table('users') + .select(Database.raw('count(*) as users_count, status')) + .where('status', '<>', 1) + .groupBy('status') + .findMany() +``` + +You can also use `await` to wait your query to finish: + +```typescript +const users = await Database.raw('SELECT * FROM users') +``` + +:::caution + +You should be extremely careful to avoid creating SQL injection +vulnerabilities using the `raw()` method. + +::: + +#### Raw methods + +Instead of using the `Database.raw()` method, you may also use the +following methods to insert a raw expression into various parts of +your query. Remember, Athenna can not guarantee that any query using +raw expressions is protected against SQL injection vulnerabilities. + +#### `selectRaw()` + +The `selectRaw()` method can be used in place of +`select(Database.raw(/* ... */))`. This method accepts an +optional array of bindings as its second argument: + +```typescript +const orders = await Database.table('orders') + .selectRaw('price * ? as price_with_tax', [1.0825]) + .findMany() +``` + +#### `whereRaw() / orWhereRaw()` + +The `whereRaw()` and `orWhereRaw()` methods can be used to inject a +raw "where" clause into your query. These methods accept an optional +array of bindings as their second argument: + +```typescript +const orders = await Database.table('orders') + .whereRaw('price > IF(state= "TX", ?, 100)', [200]) + .findMany() +``` + +#### `havingRaw() / orHavingRaw()` + +The `havingRaw()` and `orHavingRaw()` methods may be used to provide a +raw string as the value of the "having" clause. These methods accept +an optional array of bindings as their second argument: + +```typescript +const orders = await Database.table('orders') + .select('department', Database.raw('SUM(price) as total_sales')) + .groupBy('department') + .havingRaw('SUM(price) > ?', [2500]) + .findMany() +``` + +#### `orderByRaw()` + +The `orderByRaw()` method may be used to provide a raw string as the +value of the "order by" clause: + +```typescript +const orders = await Database.table('orders') + .orderByRaw('updated_at - created_at DESC') + .findMany() +``` + +#### `groupByRaw()` + +The `groupByRaw()` method may be used to provide a raw string as the +value of the "group by" clause: + +```typescript +const orders = await Database.table('orders') + .select('city', 'state') + .groupByRaw('city, state') + .findMany() +``` + +## Joins + +#### Inner join clause + +The query builder may also be used to add join clauses to your queries. +To perform a basic "inner join", you may use the join method on a query +builder instance. The first argument passed to the `join` method is the +name of the table you need to join to, while the remaining arguments +specify the column constraints for the join. You may even join multiple +tables in a single query: + +```typescript +const users = await Database.table('users') + .join('contacts', 'users.id', '=', 'contacts.user_id') + .join('orders', 'users.id', '=', 'orders.user_id') + .select('users.*', 'contacts.phone', 'orders.price') + .findMany() +``` + +#### Other join clauses + +If you would like to perform a "left join" or "right join" instead +of an "inner join", use the `leftJoin()` or `rightJoin()` methods. +They have the same signature of `join` method: + +```typescript +const users = await Database.table('users') + .leftJoin('contacts', 'users.id', '=', 'contacts.user_id') + .rightJoin('orders', 'users.id', '=', 'orders.user_id') + .select('users.*', 'contacts.phone', 'orders.price') + .findMany() +``` + +You can use any of the join types bellow in your queries: + +- `leftJoin()` +- `rightJoin()` +- `crossJoin()` +- `fullOuterJoin()` +- `leftOuterJoin()` +- `rightOuterJoin()` + +#### Advanced join clauses + +You may also specify more advanced join clauses using the `Database` +facade: + +```typescript +const users = await Database.table('users') + .join('contacts', join => join.on('users.id', '=', 'contacts.user_id').orOn(/* ... */)) + .findMany() +``` + +## Basic where clauses + +#### Where clauses + +You may use the query builder's `where()` method to add "where" clauses +to the query. The most basic call to the `where()` method requires three +arguments. The first argument is the name of the column. The second +argument is an operator, which can be any of the database's supported +operators. The third argument is the value to compare against the +column's value. + +For example, the following query retrieves users where the value of the +`votes` column is equal to `100` and the value of the `age` +column is greater than `35`: + +```typescript +const user = await Database.table('users') + .where('votes', '=', 100) + .where('age', '>', 35) + .find() +``` + +For convenience, if you want to verify that a column is `=` to a given +value, you may pass the value as the second argument to the `where()` +method. Athenna will assume you would like to use the `=` operator: + +```typescript +const users = await Database.table('users') + .where('votes', 100) + .findMany() +``` + +As previously mentioned, you may use any operator that is supported +by your database system: + +```typescript +const users = await Database.table('users') + .where('votes', '>=', 100) + .findMany() + +const users = await Database.table('users') + .where('votes', '<>', 100) + .findMany() + +const users = await Database.table('users') + .where('name', 'like', 'J%') + .findMany() +``` + +You may also pass an object of conditions, but remember that when +using objects the operation is always going to be `=`: + +```typescript +const users = await Database.table('users') + .where({ name: 'João Lenon', deletedAt: null }) + .findMany() +``` + +#### Or where clauses + +When chaining together calls to the query builder's `where()` method, +the "where" clauses will be joined together using +the `and` operator. However, you may use the `orWhere()` method to join +a clause to the query using the `or` operator. The `orWhere()` method +accepts the same arguments as the `where()` method: + +```typescript +const users = await Database.table('users') + .where('votes', '>', 100) + .orWhere('name', 'João') + .findMany() +``` + +#### Where not clauses + +The `whereNot()` and `orWhereNot()` methods may be used to negate a +given constraint. For example, the following query +excludes the product with `id` ten: + +```typescript +const products = await Database.table('products') + .whereNot('id', 10) + .findMany() +``` + +#### Additional where clauses + +##### `whereBetween() / orWhereBetween()` + +The `whereBetween()` or `orWhereBetween()` methods verifies that a +column's value is between two values: + +```typescript +const users = await Database.table('users') + .whereBetween('votes', [1, 100]) + .findMany() +``` + +##### `whereNotBetween() / orWhereNotBetween()` + +The `whereNotBetween()` or `orWhereNotBetween()` methods verifies that +a column's value lies outside two values: + +```typescript +const users = await Database.table('users') + .whereNotBetween('votes', [1, 100]) + .findMany() +``` + +##### `whereIn() / orWhereIn()` + +The `whereIn()` or `orWhereIn()` methods verifies that a given column's +value is contained within the given array: + +```typescript +const users = await Database.table('users') + .whereIn('id', [1, 2, 3]) + .findMany() +``` + +##### `whereNotIn() / orWhereNotIn()` + +The `whereNotIn()` or `orWhereNotIn()` methods verifies that the given +column's value is not contained in the given array: + +```typescript +const users = await Database.table('users') + .whereNotIn('id', [1, 2, 3]) + .findMany() +``` + +##### `whereNull() / orWhereNull()` + +The `whereNull()` or `orWhereNull()` methods verifies that the value of +the given column is `NULL`: + +```typescript +const users = await Database.table('users') + .whereNull('deletedAt') + .findMany() +``` + +##### `whereNotNull() / orWhereNotNull()` + +The `whereNotNull()` or `orWhereNotNull()` methods verifies that the +column's value is not `NULL`: + +```typescript +const users = await Database.table('users') + .whereNotNull('deletedAt') + .findMany() +``` + +#### Logical grouping + +Sometimes you may need to group several "where" clauses within +parentheses in order to achieve your query's desired logical grouping. +In fact, you should generally always group calls to the `orWhere()` +method in parentheses in order to avoid unexpected query behavior. To +accomplish this, you may pass a closure to the `where` method: + +```typescript +const users = await Database.table('users') + .where('name', '=', 'João') + .where(query => { + query + .where('votes', '>', 100) + .orWhere('title', '=', 'Admin') + }) + .findMany() +``` + +As you can see, passing a closure into the `where()` method instructs +the query builder to begin a constraint group. The closure will receive +a query builder instance which you can use to set the constraints that +should be contained within the parenthesis group. The example above +will produce the following SQL: + +```sql +select * from users where name = 'João' and (votes > 100 or title = 'Admin') +``` + +:::warning + +You should always group `orWhere()` calls in order to avoid unexpected +behavior when global scopes are applied. + +::: + +## Advanced where clauses + +#### Where exists clauses + +The `whereExists()`, `orWhereExists()`, `whereNotExists()` and +`orWhereNotExists()` methods allows you to write "where exists" SQL +clauses. They accept a closure which will receive a query builder +instance, allowing you to define the query that should be placed inside +the "exists" clause: + +```typescript +const users = await Database.table('users') + .whereExists(Database.table('orders') + .selectRaw(1) + .whereRaw("`orders`.`user_id` = `users`.`id`")) + .findMany() +``` + +The query above will produce the following SQL: + +```sql +select * from users +where exists ( + select 1 + from orders + where orders.user_id = users.id +) +``` + +## Ordering, grouping, limit & skip + +#### Ordering + +##### The `orderBy()` method + +The `orderBy()` method allows you to sort the results of the query +by a given column. The first argument accepted by the +`orderBy()` method should be the column you wish to sort by, while +the second argument determines the direction of the +sort and may be either `asc`, `ASC`, `desc` or `DESC`: + +```typescript +const users = await Database.table('users') + .orderBy('name', 'desc') + .findMany() +``` + +To sort by multiple columns, you may simply invoke `orderBy()` +as many times as necessary: + +```typescript +const users = await Database.table('users') + .orderBy('name', 'desc') + .orderBy('email', 'asc') + .findMany() +``` + +##### The `latest()` & `oldest()` methods + +The `latest()` and `oldest()` methods allow you to easily order results +by date. By default, the result will be ordered by the table's +`createdAt` column: + +```typescript +const user = await Database.table('users') + .latest() + .find() +``` + +Or, you may pass the column name that you wish to sort by: + +```typescript +const user = await Database.table('users') + .oldest('updatedAt') + .find() +``` + +#### Grouping + +##### The `groupBy()` & `having()` methods + +As you might expect, the `groupBy()` and `having()` methods may be used +to group the query results. The having method's signature is similar +to that of the `where()` method: + +```typescript +const users = await Database.table('users') + .groupBy('account_id') + .having('account_id', '>', 100) + .findMany() +``` + +You can use the `havingBetween()` method to filter the results within +a given range: + +```typescript +const users = await Database.table('users') + .selectRaw('count(id) as number_of_users, account_id') + .groupBy('account_id') + .havingBetween('number_of_users', [0, 100]) + .findMany() +``` + +You may pass multiple arguments to the `groupBy()` method to group by +multiple columns: + +```typescript +const users = await Database.table('users') + .groupBy('first_name', 'status') + .having('account_id', '>', 100) + .findMany() +``` + +To build more advanced `having()` statements, see the +[`havingRaw()`](/docs/database/query-builder#id-having-raw) method. + +#### Limit & Offset + +You may use the `offset()` and `limit()` methods to limit the number +of results returned from the query or to skip a given number of results +in the query: + +```typescript +const users = await Database.table('users') + .offset(10) + .limit(5) + .findMany() +``` + +:::tip + +The `offset()` method is equivalent to `skip()` and the `limit()` +method is equivalent to `take()`. `take()` and `skip()` are usually +used by other query builders. + +::: + +## Conditional clauses + +Sometimes you may want certain query clauses to apply to a query based +on another condition. For instance, you may only want to apply a `where()` +statement if a given input value is present on the incoming HTTP request. +You may accomplish this using the `when()` method: + +```typescript +const role = request.payload('role') + +await Database.table('users') + .when(role, (query, role) => query.where('roleId', role)) + .findMany() +``` + +The `when()` method only executes the given closure when the first +argument is `true`. If the first argument is `false`, the closure will +not be executed. So, in the example above, the closure given to the +`when()` method will only be invoked if the role field is present on +the incoming request and evaluates to a **truthy** value. + +You can also add two `when()` methods to your query to execute a different +closure when the `role` **IS NOT** present in your query: + +```typescript +const role = request.payload('role') + +await Database.table('users') + // Executes if role is present + .when(role, (query, role) => query.where('roleId', role)) + // Executes if role is not present + .when(!role, (query, role) => query.where('roleId', role)) + .findMany() +``` + +## Pagination + +You can paginate the results of your database using the `paginate()` +method. This method support 3 arguments, the first argument is the +page (default value is `0`) it defines the page where your pagination +will start, the second is the limit (default value is `10`) it defines +the limit of data that will be retrieved per page and the third one +defines the resource url that Athenna will use to create the pagination +links: + +```typescript +const { data, meta, links } = await Database.table('users') + .whereNull('deletedAt') + .paginate(0, 10, '/users') +``` + +The `data` param is where all the data retrieved from database will stay: + +```typescript +console.log(data) // -> [{...}] +``` + +The `meta` param will have information about the pagination such as +the total of items finds using that query, items per page, total +pages left, current page and the number of itens in that specific +execution: + +```typescript +console.log(meta) +/** +* { +* totalItems: 10, +* itemsPerPage: 10, +* totalPages: 10, +* currentPage: 1, +* itemCount: 10 +* } +*/ +``` + +The `links` object will help ho is consuming you API to know what is +the next resource to call to go through your paginated data: + +```typescript +console.log(links) +/** +* { +* next: '/users?page=2&limit=10', +* previous: '/users?page=0&limit=10', +* last: '/users?page=10&limit=10, +* first: '/users?&limit=10' +* } +*/ +``` + +## Insert statements + +The query builder also provides the `create()` and `createMany()` +methods that may be used to insert records into the database +table. The `create()` method accepts a record with columns names +and values: + +```typescript +const user = await Database.table('users').create({ + name: 'Valmir Barbosa', + email: 'valmirphp@gmail.com' +}) +``` + +The `createMany()` method accepts an array of records: + +```typescript +const users = await Database.table('users').createMany([ + { + name: 'Valmir Barbosa', + email: 'valmirphp@gmail.com' + }, + { + name: 'Danrley Morais', + email: 'danrley.morais@gmail.com' + } +]) +``` + +#### Create or update (Upsert) + +The `createOrUpdate()` method will insert records that do not exist +and update the records that already exist with new values that you +may specify. The method's first argument consists of the values to +insert or update, while the second argument is the column that +uniquely identify records within the associated table +(the default is `id`). In the example above we are going to create +a new record in the users table **only if** the `txsoura@athenna.io` +email is not already registered in `users` table: + +```typescript +const user = await Database.table('users') + .createOrUpdate({ + name: 'Victor Tesoura', + email: 'txsoura@athenna.io' + }, 'email') // <- The uniquely identifier +``` + +## Update statements + +In addition to inserting records into the database, the query builder +can also update existing records using the `update()` method. The +`update()` method, like the `create()` method, accepts a record with +columns names and values indicating the columns to be updated. You +may constrain the update query using where clauses. In the example +above we are going to "undo" the soft delete by searching for all +records where the `deletedAt` column is not null and setting it to `null`: + +```typescript +const user = await Database.table('users') + .whereNotNull('deletedAt') + .update({ deletedAt: null }) +``` + +#### Incrementing & decrementing + +The query builder also provides convenient methods for incrementing +or decrementing the value of a given column. Both of these methods +accept at least one argument: the column to modify: + +```typescript +await Database.table('users').increment('votes') +await Database.table('users').where('id', 1).increment('votes') + +await Database.table('users').decrement('votes') +await Database.table('users').where('id', 1).decrement('votes') +``` + +## Delete statements + +The query builder's `delete()` method may be used to delete records +from the table. You may constrain delete statements by adding "where" +clauses before calling the `delete()` method: + +```typescript +await Database.table('users').delete() +await Database.table('users').where('votes', '>', 100).delete() +``` + +If you wish to truncate an entire table, which will remove all records +from the table and reset the auto-incrementing ID to zero, you may use +the truncate method: + +```typescript +const tableName = 'users' + +await Database.truncate(tableName) +``` + +## Debugging + +You may use the `dump()` method while building a query to dump the +current query bindings and SQL. The `dump()` method will display +the debug information and continue executing the code: + +```typescript +const users = await Database.table('users') + .whereNull('deletedAt') + .dump() // <- Will log in the terminal your query until this point + .oldest('deletedAt') + .dump() // <- Will log in the terminal your query until this point + .findMany() +``` diff --git a/docs/database/seeding.mdx b/docs/database/seeding.mdx new file mode 100644 index 00000000..799f1112 --- /dev/null +++ b/docs/database/seeding.mdx @@ -0,0 +1,78 @@ +--- +title: Seeding +sidebar_position: 4 +description: See how to create and run database seeders. +--- + +# Database: Seeding + +## Introduction + +Athenna includes the ability to seed your database with data using +seed classes. All seed classes are stored in the `Path.seeders()` +directory. + +## Writing seeders + +To generate a seeder, execute the `make:seeder` Artisan command. All +seeders generated by the framework will be placed in the `Path.seeders()` +directory: + +```shell +node artisan make:seeder UserSeeder +``` + +A seeder class only contains one method by default: `run()`. This method +is called when the `db:seed` Artisan command is executed. Within the +`run()` method, you may insert data into your database however you wish. +You may use the query builder to manually insert data, or you may use +[the model factories](/docs/orm/getting-started): + +```typescript +import { User } from '#app/models/user' +import { BaseSeeder, type DatabaseImpl } from '@athenna/database' + +export class UserSeeder extends BaseSeeder { + public async run(db: DatabaseImpl) { + await db.table('users').createMany([/*.....*/]) + + await User.factory().count(20).create() + } +} +``` + +## Running seeders + +You may execute the `db:seed` Artisan command to seed your database. +By default, the `db:seed` command will run all the seeders +inside the `Path.seeders()` folder, but you can run only one seeder +using the `--classes` argument: + +```shell +node artisan db:seed + +node artisan db:seed --classes=UserSeeder +``` + +### Setting the seeder connection + +If your seeder will be interacting with a database connection other +than your application's default database connection, you should set +the static method `connection()` in your seeder: + +```typescript +import { BaseSeeder, type DatabaseImpl } from '@athenna/database' + +export class UserSeeder extends BaseSeeder { + public static connection() { + return 'postgres' + } + + public async run(db: DatabaseImpl) { + await Database.table('users').createMany([/*.....*/]) + + await User.factory().count(20).create() + } +} +``` + diff --git a/docs/orm/_category_.json b/docs/orm/_category_.json new file mode 100644 index 00000000..63ebcb61 --- /dev/null +++ b/docs/orm/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ORM", + "position": 9, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "/orm", + "description": "See how to configure a model in Athenna Framework." + } +} diff --git a/docs/orm/getting-started.mdx b/docs/orm/getting-started.mdx new file mode 100644 index 00000000..23d76a11 --- /dev/null +++ b/docs/orm/getting-started.mdx @@ -0,0 +1,413 @@ +--- +title: Getting Started +sidebar_position: 1 +description: See how to create models in Athenna Framework. +--- + +# ORM: Getting Started + +## Introduction + +Athenna has an object-relational mapper (ORM) that makes it enjoyable +to interact with your database. When using the Athenna ORM, each database +table has a corresponding "Model" that is used to interact with that +table. In addition to retrieving records from the database table, the +models allow you to insert, update, and delete records from the table +as well. + +:::tip + +Before getting started, be sure to configure a database connection in +your application's `Path.config('database.ts')` configuration file. +For more information on configuring your database, check out +[`the database configuration documentation.`](/docs/database/getting-started) + +::: + + +## Generating models + +To get started, let's create a model. Models typically live in the +`app/models` directory (`Path.models()`) and extend the [`BaseModel`](https://github.com/AthennaIO/Database/blob/develop/src/models/BaseModel.ts) +class. You may use the `make:model` Artisan command to generate a +new model: + +```shell +node artisan make:model Flight +``` + +## Columns + +You will have to define your database columns as properties on the +class and annotate them using the `@Column()` annotation. Any property +annotate with it could be distinguished between standard class properties +and database columns. Let's see an example of defining the flight table +columns as properties on the `Flight` model: + +```typescript +import { Column, BaseModel } from '@athenna/database' + +export class Flight extends BaseModel { + @Column() + public id: number + + @Column() + public from: string + + @Column() + public to: string + + @Column({ isCreateDate: true }) + public createdAt: Date + + @Column({ isUpdateDate: true }) + public updatedAt: Date +} +``` + +### Column options + +#### `name` + +Map which will be the name of your column in database: + +```typescript +@Column({ name: 'my_name' }) +public name: string +``` + +The default value of this property will be the name of +your class property as **camelCase**. + +#### `type` + +Map the type of your column. This property is usefull +only to synchronize your model with database: + +```typescript +@Column({ type: Number }) +public id: string +``` + +By default the type of your model will be set as the +type of your class property, in the example above, if +we remove the `type` property, it would automatically +be set as `String`. + +#### `length` + +Map the column length in database. This property is +usefull only when synchronizing your model with database: + +```typescript +@Column({ length: 10 }) +public name: string +``` + +#### `defaultTo` + +This property doesn't change the behavior in your database, +they are used only when the class property is undefined or +null before running your model `create()`, `createMany()`, +`update()` and `createOrUpdate()` methods: + +```typescript +@Column({ defaultTo: null }) +public deletedAt: Date +``` + +:::warn + +The value set to `defaulTo` property will only be used when +the value for the specified column was not provided when calling +the above methods and also when it was not set in static `attributes()` +method of your model. + +::: + +#### `isPrimary` + +Set if the column is a primary key: + +```typescript +@Column({ isPrimary: true }) +public id: number +``` + +#### `isHidden` + +Set if the column should be hidden when retrieving it from database: + +```typescript +@Column({ isHidden: true }) +public password: string +``` + +#### `isUnique` + +Set if the column needs to have a unique value in database: + +```typescript +@Column({ isUnique: true }) +public email: string +``` + +:::note + +If you try to create duplicated values Athenna will throw an +exception until it gets in your database. This means that you +migration could have or not the unique index defined + +::: + +#### `isNullable` + +Set if the column is nullable or not: + +```typescript +@Column({ isNullable: false }) +public name: string +``` + +:::note + +Just like `isUnique` property, if `isNullable` is set to false +and you try to create a model with null or undefined `name`, it +will throw an exception. + +::: + + +#### `isIndex` + +Set if the column is an index: + +```typescript +@Column({ isIndex: true }) +public email: string +``` + +#### `isSparse` + +Set if the column is an index sparse: + +```typescript +@Column({ isSparse: true }) +public email: string +``` + +#### `persist` + +Set if the column should be persist in database +or not. If set as `false`, Athenna will remove this +column from operations like create or update, but it +will still me available in listing operations: + +```typescript +@Column({ persist: false }) +public name: string +``` + +#### `isCreateDate` + +Set if the column is a createdAt column. If this option +is `true`, Athenna will automatically set a `new Date()` +value in the column when creating it: + +```typescript +@Column({ isCreateDate: true }) +public createdAt: Date +``` + +#### `isUpdateDate` + +Set if the column is an updatedAt column. If this option +is `true`, Athenna will automatically set a `new Date()` +value in the column when creating it: + +```typescript +@Column({ isUpdateDate: true }) +public updatedAt: Date +``` + +#### `isDeleteDate` + +Set if the column is a deletedAt column and also if the model +is using soft delete approach. If this option is `true`, Athenna +will automatically set a `new Date()` value in the column when +deleting it: + +```typescript +@Column({ isDeleteDate: true }) +public deletedAt: Date +``` + +## Model conventions + +Models generated by the `make:model` command will be placed in the +`Path.models()` directory. Let's examine a basic model class and +discuss some of Model's key conventions: + +```typescript +import { BaseModel } from '@athenna/database' + +export class Flight extends BaseModel { + @Column() + public id: number + + public static attributes(): Partial { + return {} + } + + public static async definition(): Promise> { + return { + id: this.faker.number.int() + } + } +} +``` + +### Table names + +After glancing at the example above, you may have noticed that we +did not tell the model which database table corresponds to our `Flight` +model. By convention, the "snake_case", plural name of the class will +be used as the table name unless another name is explicitly specified. +So, in this case, the model will assume the `Flight` model stores records +in the `flights` table, while an `AirTrafficController` model would store +records in an `air_traffic_controllers` table. + +If your model's corresponding database table does not fit this convention, you may manually specify the model's table name by +defining a static getter `table` on the model: + +```typescript +import { BaseModel } from '@athenna/database' + +export class Flight extends BaseModel { + public static table() { + return 'my_flights' + } + + /*...*/ +} +``` + +### Primary keys + +The model will also assume that each model's corresponding database +table has a primary key column named `id` if using a SQL driver and +`_id` if using mongo driver. If necessary, you may define +a property `isMainPrimary` as true in one of your model columns to +specify a different column that serves as your model's main primary +key: + +```typescript +import { Column, BaseModel } from '@athenna/database' + +export class Flight extends BaseModel { + @Column({ isMainPrimary: true }) + public id: number + + /*...*/ +} +``` + +### Default attributes values + +By default, a newly instantiated model instance will not contain any +attribute values. If you would like to define the default values +for some of your model's attributes, you may define a static method +`attributes()` on your model: + +```typescript +import { Uuid } from '@athenna/common' +import { BaseModel } from '@athenna/database' + +export class Flight extends BaseModel { + public static attributes(): Partial { + return { + id: Uuid.generate() + } + } + + /*...*/ +} +``` + +As you can see we are defining an `id` property in our static method +`attributes()`. This property will have the value of a generated +uuid randomly everytime that Athenna calls the `attributes()` method. +Athenna will call the `attributes()` method everytime that `create()`, +`createMany()`, `update()` and `createOrUpdate()` methods are called, +this means that a new uuid will be generated for each call: + +```typescript +import { Flight } from '#app/models/Flight' + +const flight1 = await Flight.create() +const flight2 = await Flight.query().create() + +console.log(flight1.id) // 43bf66ec-658a-4f59-8f89-2aac5ae96e6a +console.log(flight2.id) // cbe35c9c-60f3-11ed-9b6a-0242ac120002 +``` + +:::tip + +But always remember that if you have already set the property in +one of these methods, the `attributes()` method will not overwrite +them: + +```typescript +import { Flight } from '#app/models/Flight' + +// Setting my own id attribute +const flight = await Flight.create({ + id: '299dabf8-60f4-11ed-9b6a-0242ac120002' +}) + +console.log(flight.id) // 299dabf8-60f4-11ed-9b6a-0242ac120002 +``` + +::: + +### Database connections + +By default, all models will use the default database connection that +is configured for your application. If you would like to specify a +different connection that should be used when interacting with a +particular model, you should define a static `connection()` method on +the model: + +```typescript +import { BaseModel } from '@athenna/database' + +export class Article extends BaseModel { + public static connection() { + return 'mysql' + } + + /*...*/ +} +``` + +## Retrieving models + +Once you have created a model and its associated database table, you +are ready to start retrieving data from your database. You can think +of each model as a powerful query builder allowing you to fluently +query the database table associated with the model. The model's +`findMany()` method will retrieve all the records from the model's +associated database table: + +```typescript +import { Flight } from '#app/models/Flight' + +const flights = await Flight.findMany() + +flights.forEach(flight => console.log(flight.name)) +``` + +### Building queries + +Coming soon... + diff --git a/docs/testing/_category_.json b/docs/testing/_category_.json index 0d14fa47..6b1f37c3 100644 --- a/docs/testing/_category_.json +++ b/docs/testing/_category_.json @@ -1,6 +1,6 @@ { "label": "Testing", - "position": 8, + "position": 10, "collapsed": true, "link": { "type": "generated-index",