From 22c05ecce93385ed3f682bf1c067a6754c81507d Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Sun, 29 Dec 2024 13:01:32 -0300 Subject: [PATCH 1/3] feat(helper): add docs about ulid helper --- docs/the-basics/helpers.mdx | 168 ++++++++++++++++++++++++++++++++---- 1 file changed, 152 insertions(+), 16 deletions(-) diff --git a/docs/the-basics/helpers.mdx b/docs/the-basics/helpers.mdx index 193301eb..0093b879 100644 --- a/docs/the-basics/helpers.mdx +++ b/docs/the-basics/helpers.mdx @@ -31,7 +31,7 @@ more details to handle them easily. - [`Exec`](/docs/the-basics/helpers#exec) - Simple helpers that executes some operation, like executing a command in a child process. - [`FakeApi`](/docs/the-basics/helpers#fakeapi) - Create a fake REST API -using `json` files to map the routes and their returns (similiar to [WireMock](https://wiremock.org/)). +using `json` files to map the routes and their returns (similar to [WireMock](https://wiremock.org/)). - [`File`](/docs/the-basics/helpers#file) - Create, copy, move, delete and get information about files. - [`Folder`](/docs/the-basics/helpers#folder) - Create, copy, move, @@ -63,8 +63,10 @@ folder starting from your application route params, query params and more. - [`String`](/docs/the-basics/helpers#string) - Simple helpers to manipulate strings. +- [`Ulid`](/docs/the-basics/helpers#ulid) - Create customized ULID and +check if they are valid. - [`Uuid`](/docs/the-basics/helpers#uuid) - Create customized UUID v4 and -validate if they are valid. +check if they are valid. ### Clean @@ -98,7 +100,7 @@ console.log(cleanedArray) ``` The `cleanArray()` method supports two options, `removeEmpty` to remove empty -values like `{}` and `[]` and `recursive` to recursivelly remove data +values like `{}` and `[]` and `recursive` to recursively remove data from objects and arrays: ```typescript @@ -144,7 +146,7 @@ console.log(cleanedObject) ``` The `cleanObject()` method supports two options, `removeEmpty` to remove empty -values like `{}` and `[]` and `recursive` to recursivelly remove data +values like `{}` and `[]` and `recursive` to recursively remove data from objects and arrays: ```typescript @@ -277,7 +279,7 @@ the available documentation for error handling by application: #### `Exception.toJSON()` -Get the informations of the exception as JSON: +Get the information of the exception as JSON: ```typescript import { Exception } from '@athenna/common' @@ -297,7 +299,7 @@ const json = exception.toJSON() // { status: 500, name: 'ErrorName', ... } #### `Exception.prettify()` -Transform the exception to a human redable format: +Transform the exception to a human readable format: ```typescript import { Exception } from '@athenna/common' @@ -308,7 +310,7 @@ const exception = new Exception({ otherInfos: {}, name: 'ErrorName', code: 'ERROR_CODE', - message: 'Some exception has ocurred.', + message: 'Some exception has occurred.', help: 'Try restarting your computer, should work.' }) @@ -598,7 +600,7 @@ await FakeApi.registerFolder(Path.resources('fake-api')) #### `FakeApi::build()` -Use this method to programatically build the routes u +Use this method to programmatically build the routes u sing the builder pattern: ```typescript @@ -648,7 +650,7 @@ await existent.load({ withContent: true }) 👈 nonExistent.loadSync() 👈 ``` -After loading process, the file will contain new informations: +After loading process, the file will contain new information: - createdAt - The date when the file was created. - accessedAt - The date when the file was last accessed. @@ -860,7 +862,7 @@ const stream = file.createWriteStream() #### `File.toJSON()` -Get the informations of the file as JSON: +Get the information of the file as JSON: ```typescript const infos = file.toJSON() 👈 @@ -969,7 +971,7 @@ await existent.load({ withSub: true, withContent: true }) 👈 nonExistent.loadSync() 👈 ``` -After loading process, the folder will contain new informations: +After loading process, the folder will contain new information: - createdAt - The date when the folder was created. - accessedAt - The date when the folder was last accessed. @@ -1057,7 +1059,7 @@ await folder.remove() 👈 #### `Folder.toJSON()` -Get the informations of the folder as JSON: +Get the information of the folder as JSON: ```typescript const infos = folder.toJSON() 👈 @@ -2650,6 +2652,17 @@ Is.Module(new File('./hello.ts')) // true Is.Module(new File('./hello.json')) // false ``` +#### `Is::Ulid()` + +Validate if the value is a valid ULID: + +```typescript +import { Is } from '@athenna/common' + +Is.Ulid('adm::01JG9J4KEDQ9KJCZNYAZCG5ECK') // true +Is.Ulid('adm::01JG9J4KEDQ9KJCZNYAZCG5ECK', { prefix: 'adm' }) // true +``` + #### `Is::Uuid()` Validate if the value is a valid UUID v4: @@ -4083,14 +4096,14 @@ import { Path } from '@athenna/common' Path.setModels('app/models/models') ``` -#### `Path::boostrap()` +#### `Path::bootstrap()` -Merge the project root path with `Path.dirs.boostrap`: +Merge the project root path with `Path.dirs.bootstrap`: ```typescript import { Path } from '@athenna/common' -console.log(Path.boostrap()) // /home/user/athenna-project/boostrap +console.log(Path.bootstrap()) // /home/user/athenna-project/bootstrap ``` #### `Path::setBootstrap()` @@ -4220,7 +4233,7 @@ Set the `Path.dirs.facades` value: ```typescript import { Path } from '@athenna/common' -Path.setFacades('pfacades/app') +Path.setFacades('facades/app') ``` #### `Path::public()` @@ -5106,6 +5119,129 @@ const base64 = String.normalizeBase64('+++///===') // '---___' ``` +### Ulid + +#### `Ulid::generate()` + +Generate a ULID: + +```typescript +import { Ulid } from '@athenna/common' + +const ulid = Ulid.generate() +// 01JG9J4KEDQ9KJCZNYAZCG5ECK +``` + +You can also generate an UUID with a prefixed string: + +```typescript +const prefixedUlid = Ulid.generate('adm') +// adm::01JG9J4KEDQ9KJCZNYAZCG5ECK +``` + +#### `Ulid::verify()` + +Verify if a string is a valid ULID: + +```typescript +import { Ulid } from '@athenna/common' + +if (Ulid.verify('01JG9J4KEDQ9KJCZNYAZCG5ECK')) { + // do something +} +``` + +You can also verify if a string is a valid ULID and if +the prefix matches: + +```typescript +const prefixedUlid = 'adm::01JG9J4KEDQ9KJCZNYAZCG5ECK' + +// Verify if the string is a valid ULID and the prefix +// is equals to adm +if (Ulid.verify(prefixedUlid, { prefix: 'adm' })) { + // do something +} +``` + +By default, the `verify()` method will ignore the prefix to verify +if the string is a valid ULID, to avoid this set the `ignorePrefix` +option as `false`: + +```typescript +const prefixedUlid = 'adm::01JG9J4KEDQ9KJCZNYAZCG5ECK' + +if (Ulid.verify(prefixedUlid, { ignorePrefix: false })) { + // will never enter here with the example above, + // only with plain ULID's. +} +``` + +#### `Ulid::getToken()` + +Get the ULID token without the prefix: + +```typescript +import { Ulid } from '@athenna/common' + +const prefixedUlid = 'adm::01JG9J4KEDQ9KJCZNYAZCG5ECK' +const uuid = Ulid.getToken(prefixedUlid) +// 01JG9J4KEDQ9KJCZNYAZCG5ECK +``` + +#### `Ulid::getPrefix()` + +Get the prefix without the UUID token: + +```typescript +import { Ulid } from '@athenna/common' + +const prefixedUlid = 'adm::01JG9J4KEDQ9KJCZNYAZCG5ECK' +const prefix = Ulid.getPrefix(prefixedUlid) // adm +``` + +#### `Ulid::injectPrefix()` + +Inject a prefix in the UUID token, if the UUID token is not +valid, this method will throw: + +```typescript +import { Ulid } from '@athenna/common' + +const uuid = '01JG9J4KEDQ9KJCZNYAZCG5ECK' +const prefixedUlid = Ulid.injectPrefix(uuid, 'adm') +// adm::01JG9J4KEDQ9KJCZNYAZCG5ECK +``` + +#### `Ulid::changePrefix()` + +Change the prefix of an UUID token, if the UUID token is not +valid, this function will throw: + +```typescript +import { Ulid } from '@athenna/common' + +const ulid = 'adm::01JG9J4KEDQ9KJCZNYAZCG5ECK' +const prefixedUlid = Ulid.changePrefix(ulid, 'mng') +// mng::01JG9J4KEDQ9KJCZNYAZCG5ECK +``` + +#### `Ulid::changeOrGenerate()` + +Change the prefix of an ULID token, or generate the token +if the ULID token is `undefined`: + +```typescript +import { Ulid } from '@athenna/common' + +const ulid = undefined +const prefixedUlid = Ulid.changeOrGenerate('adm', ulid) +// adm::01JG9J4KEDQ9KJCZNYAZCG5ECK + +const changedPrefixUlid = Ulid.changeOrGenerate('mng', prefixedUlid) +// mng::01JG9J4KEDQ9KJCZNYAZCG5ECK +``` + ### Uuid #### `Uuid::generate()` From aadd612cd188dd1e0b632070458078dcd62765db Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Sun, 29 Dec 2024 13:04:33 -0300 Subject: [PATCH 2/3] chore(cron): wording --- docs/cron-application/annotations.mdx | 2 ++ docs/cron-application/schedulers.mdx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/cron-application/annotations.mdx b/docs/cron-application/annotations.mdx index 7993a752..ed54481a 100644 --- a/docs/cron-application/annotations.mdx +++ b/docs/cron-application/annotations.mdx @@ -11,4 +11,6 @@ import Path from '@site/src/components/path' Check all available CRON annotations and it options. +## Introduction + Coming Soon... diff --git a/docs/cron-application/schedulers.mdx b/docs/cron-application/schedulers.mdx index de203a53..df1fa854 100644 --- a/docs/cron-application/schedulers.mdx +++ b/docs/cron-application/schedulers.mdx @@ -23,7 +23,7 @@ inside . ## Defining Schedulers -Schedulers are typically stored in the `src/cron/scheduelrs` +Schedulers are typically stored in the `src/cron/schedulers` directory; however, you are free to choose your own storage location as long as your schedulers can be imported and registered. From e9007601eda1a0bc762283432f21da19f2ddb3e3 Mon Sep 17 00:00:00 2001 From: jlenon7 Date: Mon, 30 Dec 2024 12:34:39 -0300 Subject: [PATCH 3/3] feat(validation): add validation docs --- docs/orm/getting-started.mdx | 4 +- docs/the-basics/compilation.mdx | 2 +- docs/the-basics/helpers.mdx | 8 +- docs/the-basics/validation.mdx | 324 ++++++++++++++++++++++++++++++++ src/components/path.tsx | 3 + 5 files changed, 334 insertions(+), 7 deletions(-) create mode 100644 docs/the-basics/validation.mdx diff --git a/docs/orm/getting-started.mdx b/docs/orm/getting-started.mdx index 8bd48ca6..a2227aba 100644 --- a/docs/orm/getting-started.mdx +++ b/docs/orm/getting-started.mdx @@ -166,8 +166,8 @@ export class Flight extends BaseModel { 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()`, +uuid randomly every time that Athenna calls the `attributes()` method. +Athenna will call the `attributes()` method every time that `create()`, `createMany()`, `update()` and `createOrUpdate()` methods are called, this means that a new uuid will be generated for each call: diff --git a/docs/the-basics/compilation.mdx b/docs/the-basics/compilation.mdx index 1b95d5ab..a1ba092c 100644 --- a/docs/the-basics/compilation.mdx +++ b/docs/the-basics/compilation.mdx @@ -1,6 +1,6 @@ --- title: Compilation -sidebar_position: 4 +sidebar_position: 3 description: Understand the TypeScript compilation process of Athenna. --- diff --git a/docs/the-basics/helpers.mdx b/docs/the-basics/helpers.mdx index 0093b879..34e36ea7 100644 --- a/docs/the-basics/helpers.mdx +++ b/docs/the-basics/helpers.mdx @@ -1,6 +1,6 @@ --- title: Helpers -sidebar_position: 3 +sidebar_position: 2 description: Understand how to use all the Athenna Helpers from @athenna/common and other packages. --- @@ -226,7 +226,7 @@ Color.httpMethod('OPTIONS').bold('Request Received') Return all the keys of your enum: -```ts +```typescript import { Enum } from '@athenna/common' class Status extends Enum { @@ -241,7 +241,7 @@ const keys = Status.keys() // ['PENDING', 'APPROVED'] Return all the values of your enum: -```ts +```typescript import { Enum } from '@athenna/common' class Status extends Enum { @@ -256,7 +256,7 @@ const values = Status.values() // [0, 1] Return all the entries of your enum: -```ts +```typescript import { Enum } from '@athenna/common' class Status extends Enum { diff --git a/docs/the-basics/validation.mdx b/docs/the-basics/validation.mdx new file mode 100644 index 00000000..c2015b84 --- /dev/null +++ b/docs/the-basics/validation.mdx @@ -0,0 +1,324 @@ +--- +title: Validation +sidebar_position: 5 +description: Understand how you can use the Athenna validation API. +--- + +import Path from '@site/src/components/path' + +# Validation + +Understand how you can use the Athenna validation API. + +## Introduction + +Athenna provides several different approaches to validate your +application's incoming data by using [VineJS](https://vinejs.dev/docs/introduction) +under the hood. It is most common to create a validation +class extending the `BaseValidator` class available after installing +the `@athenna/validator` package. However, we will discuss other approaches +to validation as well. + +Athenna includes a wide variety of convenient validation rules that you may +apply to data, even providing the ability to validate if values are unique +in a given database table. We'll cover each of these validation rules in +detail so that you are familiar with all of Athenna's validation features. + +## Installation + +First of all you need to install `@athenna/validator` package +and configure it. Artisan provides a very simple command to +install and configure the validator library in your project. +Simply run the following: + +```bash +node artisan install @athenna/validator +``` + +The validator configurer will do the following operations in +your project: + +- Add all database commands in your `.athennarc.json` file. +- Add all database templates files in your `.athennarc.json` file. +- Add all validator providers in your `.athennarc.json` file. + +## Validation quickstart + +To learn about Athenna's powerful validation features, let's look at a +complete example of validating a form and displaying the error messages +back to the user. By reading this high-level overview, you'll be able to +gain a good general understanding of how to validate incoming request data +using Athenna: + +### Defining routes + +First, let's assume we have the following routes defined in our + file: + +```typescript title="Path.routes('http.ts')" +import { Route } from '@athenna/http' + +Route.post('/articles', 'ArticleController.store') +``` + +### Creating the controller + +Next, let's take a look at a simple controller that handles incoming +requests to this route. We'll leave the `store()` method empty for now: + +```typescript title="Path.controllers('article.controller.ts')" +import { Context, Controller } from '@athenna/http' + +@Controller() +export class ArticleController { + public async store({ response }: Context) { + // Validator and store the article + const article = /** ... */ + + return response.status(200).send(article) + } +} +``` + +### Writing the validation class + +To get started, let's create a validator. Validators typically live in the + and extend the [`BaseValidator`](https://github.com/AthennaIO/Validator/blob/develop/src/validator/BaseValidator.ts) +class. You may use the `make:validator` Artisan command to generate a +new validator: + +```bash +node artisan make:validator article.validator +``` + +Now we just need to create the validation schema and gave a name to +our validation class: + +```typescript title="Path.validators('article.validator.ts')" +import { type Context } from '@athenna/http' +import { v, Validator, BaseValidator } from '@athenna/validator' + +@Validator({ name: 'article:create' }) 👈 +export class ArticleValidator extends BaseValidator { + public schema = v.object({ + title: v.string() + .unique({ table: 'articles' }) + .maxLength(255), + body: v.string() + }) + + public async handle({ request }: Context) { + const data = request.only(['title', 'body']) + + await v.validate(data) + } +} +``` + +With our schema and name defined in our validation class, it's time to define +it in our route: + +```typescript title="Path.routes('http.ts')" +import { Route } from '@athenna/http' + +Route.post('/articles', 'ArticleController.store').validator('article:create') +``` + +Now every time that a request to create an article comes, if the validation rule pass, +your code will keep executing normally; however, if validation fails, a `ValidationException` +exception will be thrown and the proper error response will automatically be sent back to +the user. + +Voilà! You have defined you first validator using Athenna 🥳 + +### Validating REST API components + +When using validator classes for [REST API](docs/rest-api-application/routing), +keep in mind that they are just [middlewares](docs/rest-api-application/middlewares) +with some helpers to make validation easier. So you are free to define multiple +validation classes: + +```typescript +import { Route } from '@athenna/http' + +Route.post('/articles', 'ArticleController.store') + .validator('article:create') 👈 + .validator('article:update') 👈 +``` + +There might be some cases where you need to validate fields inside request params or query +params. Since validators are just like [middlewares](docs/rest-api-application/middlewares), +You have access to all that information through the `ctx.request` object: + +```typescript title="Path.validators('article.validator.ts')" +import { type Context } from '@athenna/http' +import { v, Validator, BaseValidator } from '@athenna/validator' + +@Validator({ name: 'article:create' }) +export class ArticleValidator extends BaseValidator { + public schema = v.object({ + title: v.string() + .unique({ table: 'articles' }) + .maxLength(255), + body: v.string(), + created_by: v.string() + }) + + public async handle({ request }: Context) { + const data = { + ...request.only(['title', 'body']), + created_by: request.query('created_by') 👈 + } + + await v.validate(data) + } +} +``` + +## Validation outside of REST API's + +For applications where you don't have an easy integration with a validator class or if +you simply want to have control over the validation flow you can use the `v` object +directly, `v` object is the same as `vite` but smaller to write smaller schemas: + +```typescript title="Path.controllers('article.controller.ts')" +import { v } from '@athenna/validator' +import { type Context, Controller } from '@athenna/http' + +@Controller() +export class ArticleController { + public async store({ request, response }: Context) { + const data = request.only(['title', 'body']) + + const schema = v.object({ + title: v.string() + .unique({ table: 'articles' }) + .maxLength(255), + body: v.string() + }) + + const article = await v.validate({ schema, data }) + + return response.status(200).send(article) + } +} +``` + +Just like the validation class, if the validation rule pass, your code will keep +executing normally; however, if validation fails, a `ValidationException` exception +will be thrown and the proper error response will automatically be sent back to the user. + +## Available validation rules + +All the of the native validation rules can be found in [VineJS](https://vinejs.dev/docs/introduction) +documentation. Athenna supports all schema types of VineJS, you can find the documentation for each one here: + +- [`v.string()`](https://vinejs.dev/docs/types/string) +- [`v.boolean()`](https://vinejs.dev/docs/types/boolean) +- [`v.number()`](https://vinejs.dev/docs/types/number) +- [`v.date()`](https://vinejs.dev/docs/types/date) +- [`v.accepted()`](https://vinejs.dev/docs/types/accepted) +- [`v.enum()`](https://vinejs.dev/docs/types/enum) +- [`v.literal()`](https://vinejs.dev/docs/types/literal) +- [`v.object()`](https://vinejs.dev/docs/types/object) +- [`v.record()`](https://vinejs.dev/docs/types/record) +- [`v.array()`](https://vinejs.dev/docs/types/array) +- [`v.tuple()`](https://vinejs.dev/docs/types/tuple) +- [`v.union()`](https://vinejs.dev/docs/types/union) +- [`v.any()`](https://vinejs.dev/docs/types/any) + +## Custom validation rules + +To create custom validations we just need to create a provider where we will +use the `Validate` facade to extend some of the schema types. Let's check in +the example bellow a simpler version of the `unique` custom validation rule +of Athenna, let's start by creating a validation provider: + +```bash +node artisan make:provider customvalidation.provider +``` + +With our provider created and registered in `.athennarc.json`, we just need +to make use of the `Validate.extend()` method inside the `boot()` method: + +```typescript +import { Database } from '@athenna/database' +import { Validate } from '@athenna/validator' +import { ServiceProvider } from '@athenna/ioc' + +type UniqueOptions = { + table: string +} + +declare module '@vinejs/vine' { + interface VineString { + unique(options: UniqueOptions): this + } +} + +export default class CustomValidationProvider extends ServiceProvider { + public async boot() { + Validate.extend().string('unique', async (value, options, field) => { + /** + * Don't validate non string values, let `string` + * validation rule throw the error. + */ + if (!Is.String(value)) { + return + } + + const existsRow = await Database.table(options.table) + .select(options.column) + .where(options.column, value) + .exists() + + if (existsRow) { + field.report('The {{ field }} field is not unique', 'unique', field) + } + }) + } +} +``` + +:::tip + +For more information around how to create custom rules for your schemas +please rely on [VineJS custom rules documentation](https://vinejs.dev/docs/extend/custom_rules). + +::: + +## Custom validation messages + +For custom validation messages you can use the `Validate.extend().messages()` method. +So following the same approach of creating [custom validation rules](#custom-validation-rules), we +need to create a service provider to call this method: + +```typescript +import { Validate } from '@athenna/validator' +import { ServiceProvider } from '@athenna/ioc' + +export default class CustomValidationProvider extends ServiceProvider { + public async boot() { + Validate.extend().messages({ + // Applicable for all fields + 'required': 'The {{ field }} field is required', + 'string': 'The value of {{ field }} field must be a string', + 'email': 'The value is not a valid email address', + + // Error message only for the username field + 'username.required': 'Please choose a username for your account' + + // For arrays + 'contacts.0.email.required': 'The primary email of the contact is required', + 'contacts.*.email.required': 'Contact email is required' + }) + } +} +``` + +:::tip + +For more information around syntaxes you can use whe creating custom error messages +please rely on [VineJS custom error messages documentation](https://vinejs.dev/docs/custom_error_messages). + +::: diff --git a/src/components/path.tsx b/src/components/path.tsx index 1abbdd2e..2abbb347 100644 --- a/src/components/path.tsx +++ b/src/components/path.tsx @@ -83,6 +83,9 @@ export default function Path(props: { father: string; child: string }) { case 'routes': father = 'src/routes' break + case 'validators': + father = 'src/validators' + break } return (