diff --git a/docs/api-reference/cli.md b/docs/api-reference/cli.md new file mode 100644 index 00000000..a3991154 --- /dev/null +++ b/docs/api-reference/cli.md @@ -0,0 +1,14 @@ +--- +sidebar_position: 2 +title: Operon Command line interface (CLI) +--- + +The operon runtime with the application is started using the command line + +``` +npx operon start -p -l +``` + +-p or --port: The http port on which the runtime with listen for http requests + +-l or --loglevel: 'info','warn','error','debug' \ No newline at end of file diff --git a/docs/api-reference/configuration.md b/docs/api-reference/configuration.md new file mode 100644 index 00000000..873c34d8 --- /dev/null +++ b/docs/api-reference/configuration.md @@ -0,0 +1,89 @@ +--- +sidebar_position: 1 +title: Operon Configuration +--- + +Configuration is set of properties and values provided by the application develop to influence the behaviour of the operon runtime as well as the application. + +## configuration file + +Standard yaml file operon-config.yaml. +The file should be in the root directory of the project. +A sample operon-config.yaml is shown below. + +A value in the form ${SOMEVALUE} implies that the runtime will get the value from the environment variable SOMEVALUE. + +``` +database: + hostname: 'localhost' + port: 5432 + username: 'postgres' + password: ${PGPASSWORD} + user_database: 'hello' + system_database: 'hello_systemdb' + connectionTimeoutMillis: 3000 + user_dbclient: 'knex' +telemetryExporters: + - 'ConsoleExporter' +``` + +## Operon configuration + +### database +The database section contains configuration parameters needed by operon to connect to user and system databases. + +#### hostname +The hostname or ip address of the machine hosting the database. + +#### port +The port that the database is listening on. + +#### username +The username to use to connect to the database. + +#### password +The password to use to connect to the database. It is strongly recommended that you do not put password in cleartext here. Instead use indirection like ${PGPASSWORD} so that the runtime can get the value from the environment variable PGPASSWORD. + +#### user_database +This is the database that the application code reads and writes from. + +#### system_database +This is the database that the operon runtime reads and writes from. + +#### user_dbclient +This is the sql builder or ORM used to communicate with the database. Supported values are knex, prisma or typeorm. Default is knex + +#### connectionTimeoutMillis +The timeout in milliseconds after which the database driver will timeout from connecting to the database. + +#### observability_database +The name of the database to which the observability database is written to. + +#### ssl_ca +The path to ssl certificate to connect to the database. + +### localRuntimeConfig +``` +localRuntimeConfig + port: 6000 +``` +This section has properties needed to configure the runtime. + +#### port +This is the port on which the embedded http server listens. Default is 3000. + + +### telemetryExporters + +List of exporter to whom telemetry logs are to be sent. Supported values are 'ConsoleExporter', 'JaegerExporter', 'PGExporter'. + + +## Application configuration + +The application section can have any user defined properties and values used by the application. + +``` +application: + PAYMENTS_SERVICE: 'http://stripe.com/payment' + +``` \ No newline at end of file diff --git a/docs/api-reference/contexts.md b/docs/api-reference/contexts.md index 3429bd26..30abc5a0 100644 --- a/docs/api-reference/contexts.md +++ b/docs/api-reference/contexts.md @@ -1,5 +1,5 @@ --- -sidebar_position: 1 +sidebar_position: 4 title: Operon Contexts --- diff --git a/docs/api-reference/decorators.md b/docs/api-reference/decorators.md index cd708f8d..11d035b6 100644 --- a/docs/api-reference/decorators.md +++ b/docs/api-reference/decorators.md @@ -1,5 +1,5 @@ --- -sidebar_position: 2 +sidebar_position: 3 title: Decorator Reference description: Usage of decorators in Operon, with exhaustive list --- diff --git a/docs/api-reference/workflow-handles.md b/docs/api-reference/workflow-handles.md index 63ce52bf..08dd84fc 100644 --- a/docs/api-reference/workflow-handles.md +++ b/docs/api-reference/workflow-handles.md @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 5 title: Workflow handles description: API documentation for Operon Workflow Handles --- diff --git a/docs/explanations/_category_.json b/docs/explanations/_category_.json new file mode 100644 index 00000000..b3056268 --- /dev/null +++ b/docs/explanations/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "Concepts and Explanations", + "position": 4, + "link": { + "type": "generated-index", + "description": "These explanations help you learn core Operon concepts." + } + } + \ No newline at end of file diff --git a/docs/explanations/application-structure-explanation.md b/docs/explanations/application-structure-explanation.md new file mode 100644 index 00000000..3a668f21 --- /dev/null +++ b/docs/explanations/application-structure-explanation.md @@ -0,0 +1,116 @@ +--- +sidebar_position: 1 +title: Application Structure +description: Learn about the structure of an Operon application +--- + +In this guide, you'll learn about the structure of an Operon application. + +### Directory Structure + +When you initialize an Operon project with `npx operon init`, it has the following structure: + +```bash +operon-hello-app/ +├── README.md +├── knexfile.ts +├── migrations/ +├── node_modules/ +├── operon-config.yaml +├── package-lock.json +├── package.json +├── src/ +│ └── userFunctions.ts +├── start_postgres_docker.sh +└── tsconfig.json +``` + +The two most important files in an Operon project are `operon-config.yaml` and `src/userFunctions.ts`. + +`operon-config.yaml` defines the configuration of an Operon project, including database connection information, ORM configuration, and global logging configuration. +All options are documented in our [configuration reference](..). + +`src/userFunctions.ts` is where Operon looks for your code. +At startup, the Operon runtime automatically loads all classes exported from this file, serving their endpoints and registering their transactions and workflows. +If you're writing a small application, you can write all your code directly in this file. +In a larger application, you can write your code wherever you want, but should use `src/userFunctions.ts` as an index file, exporting code written elsewhere. + +As for the rest of the directory: + +- `knexfile.ts` is a configuration file for [Knex](https://knexjs.org), which we use as a query builder and migration tool. +- `migrations` is initialized with a Knex database migration used in the [quickstart guide](../getting-started/quickstart). If you're using Knex for schema management, you can create your own migrations here. +- `node_modules`, `package-lock.json`, `package.json`, and `tsconfig.json` are needed by all Node/Typescript projects. +- `start_postgres_docker.sh` is a convenience script that initializes a Docker-hosted Postgres database for use in the [quickstart](../getting-started/quickstart). You can modify this script if you want to use Docker-hosted Postgres for local development. + +### Code Structure + +Here's the initial source code generated by `npx operon init` (in `src/userFunctions.ts`): + +```javascript +import { TransactionContext, OperonTransaction, GetApi, HandlerContext } from '@dbos-inc/operon' +import { Knex } from 'knex'; + +type KnexTransactionContext = TransactionContext; + +interface operon_hello { + name: string; + greet_count: number; +} + +export class Hello { + + @OperonTransaction() + static async helloTransaction(txnCtxt: KnexTransactionContext, name: string) { + // Look up greet_count. + let greet_count = await txnCtxt.client("operon_hello") + .select("greet_count") + .where({ name: name }) + .first() + .then(row => row?.greet_count); + if (greet_count) { + // If greet_count is set, increment it. + greet_count++; + await txnCtxt.client("operon_hello") + .where({ name: name }) + .increment('greet_count', 1); + } else { + // If greet_count is not set, set it to 1. + greet_count = 1; + await txnCtxt.client("operon_hello") + .insert({ name: name, greet_count: 1 }) + } + return `Hello, ${name}! You have been greeted ${greet_count} times.\n`; + } + + @GetApi('/greeting/:name') + static async helloHandler(handlerCtxt: HandlerContext, name: string) { + return handlerCtxt.invoke(Hello).helloTransaction(name); + } +} + +``` +An Operon application like this one is made up of classes encapsulating _functions_, written as decorated static class methods. +There are four basic types of functions. +This example contains two of them: + +- [**Transactions**](../tutorials/transaction-tutorial), like `helloTransaction` perform database operations. +- [**Handlers**](../tutorials/http-serving-tutorial), like `helloHandler`, serve HTTP requests. + +There are two more: + +- [**Communicators**](../tutorials/communicator-tutorial) manage communication with external services and APIs. +- [**Workflows**](../tutorials/workflow-tutorial) reliably orchestrate other functions. + +A function needs to follow a few rules: + +- It must be a static class method. For Operon to find it, that class must be exported from `src/userFunctions.ts`. +- It must have a decorator telling Operon what kind of function it is: [`@OperonTransaction`](../api-reference/decorators#operontransaction) for transactions, [`@OperonCommunicator`](../api-reference/decorators#operoncommunicator) for communicators, [`@OperonWorkflow`](../api-reference/decorators#operonworkflow) for workflows, or [`GetApi`](../api-reference/decorators#getapi) or [`PostApi`](../api-reference/decorators#postapi) for handlers. +- Its first argument must be the appropriate kind of [Operon context](../api-reference/contexts). Contexts provide functions with useful methods, such as access to a database client for transactions. +- Its input and return types must be serializable to JSON. + +Once you've written your functions, there are two basic ways to call them: + +1. Any function (not just handlers) can be called from HTTP if it's annotated with the [`GetApi`](../api-reference/decorators#getapi) or [`PostApi`](../api-reference/decorators#postapi) decorators. See our [HTTP serving tutorial](../tutorials/http-serving-tutorial.md) for details. +2. Handlers and workflows can invoke other functions via their contexts' [invoke](..) method. + +To learn more about each individual type of function and what it can do, see our [tutorials](../category/tutorials/). diff --git a/docs/getting-started/coreconcepts.md b/docs/getting-started/coreconcepts.md index 71e1e4f8..147ba7ad 100644 --- a/docs/getting-started/coreconcepts.md +++ b/docs/getting-started/coreconcepts.md @@ -4,7 +4,7 @@ sidebar_position: 4 # Core Concepts -Operon is a simple easy to use serverless framework for developing transactional application. +Operon is a simple easy to use serverless framework for developing transactional applications. Operon applications are made up of transactions and workflows. Operon workflows group together a set of transactions and provide them with Once-and-Only-Once-Execution guarantees. This means Operon workflows are guaranteed to run to completion and their composing transactions will be executed only once. diff --git a/docs/tutorials/_category_.json b/docs/tutorials/_category_.json index 2eb1eb40..2023d2c7 100644 --- a/docs/tutorials/_category_.json +++ b/docs/tutorials/_category_.json @@ -3,6 +3,6 @@ "position": 2, "link": { "type": "generated-index", - "description": "These tutorials help you learn core Operon concepts." + "description": "These tutorials help you learn how to build powerful Operon applications." } } diff --git a/docs/tutorials/client-workflow-interactions-tutorial.md b/docs/tutorials/client-workflow-interactions-tutorial.md index 132faea3..4c653320 100644 --- a/docs/tutorials/client-workflow-interactions-tutorial.md +++ b/docs/tutorials/client-workflow-interactions-tutorial.md @@ -1,5 +1,5 @@ --- -sidebar_position: 8 +sidebar_position: 6 title: Workflow events description: Learn how to send events from workflows and receive them from handlers --- diff --git a/docs/tutorials/communicator-tutorial.md b/docs/tutorials/communicator-tutorial.md new file mode 100644 index 00000000..9efdcee7 --- /dev/null +++ b/docs/tutorials/communicator-tutorial.md @@ -0,0 +1,30 @@ +--- +sidebar_position: 3 +title: Communicators +description: Learn how to communicate with external APIs and services +--- + +In this guide, you'll learn how to communicate with external APIs and services from an Operon application. + +We recommend that all communication with external services be done in _communicator_ functions. +For example, you can use communicators to serve a file from [AWS S3](https://aws.amazon.com/s3/), call an external API like [Stripe](https://stripe.com/) or access a non-Postgres data store like [Elasticsearch](https://www.elastic.co/elasticsearch/). +Encapsulating these calls in communicators is especially important if you're using [workflows](..) as it lets the workflow know to make their results persistent through server failures. + +Communicators must be annotated with the [`@OperonCommunicator`](../api-reference/decorators#operoncommunicator) decorator and must have a [`CommunicatorContext`](..) as their first argument. +Like for other Operon functions, inputs and outputs must be serializable to JSON. +Here's a simple example using [Axios](https://axios-http.com/docs/intro) to call the [Postman Echo API](https://learning.postman.com/docs/developer/echo-api/): + + +```javascript + @OperonCommunicator() + static async postmanEcho(_ctxt: CommunicatorContext) { + const resp = await axios.get("https://postman-echo.com/get"); + return resp.data; + } +``` + +### Retries + +By default, Operon automatically retries any communicator function that throws an exception. +It retries communicator functions a set number of times with exponential backoff, throwing an [`OperonError`](..) if the maximum number of retries is exceed. +Retries are fully configurable through arguments to the [`@OperonCommunicator`](../api-reference/decorators#operoncommunicator) decorator. diff --git a/docs/tutorials/handlers-tutorial.md b/docs/tutorials/handlers-tutorial.md deleted file mode 100644 index 95b68448..00000000 --- a/docs/tutorials/handlers-tutorial.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -sidebar_position: 2 -title: HTTP handlers -description: Learn how to register Operon HTTP handlers ---- - -Operon lets you declare HTTP handlers to make your workflows available through HTTP. -Under the hood, `npx operon start` runs a [Koa](https://koajs.com/) server exposing your handlers. - -```tsx -import { HandlerContext, GetApi } from '@dbos-inc/operon' - -export class Hello { - @GetApi('/greeting/:name') - static async greetingEndpoint(ctx: HandlerContext, name: string) { - return `Greeting, ${name}`; - } -} -``` - -Operon operations must be declared as static methods of a class -- here, `Hello`. To register a `GET` HTTP handler, use the `@GetApi` decorator with your handler path. - - -## `GetApi(url: string)` - -`url` is the endpoint you wish to register your handler under. Under the hood, Operon uses [koa-router](https://github.com/ZijianHe/koa-router), which uses [path_to_regex](https://github.com/pillarjs/path-to-regexp) to translate this `url` to a regular expression for the router to match incoming requests. - -## `ctx: HandlerContext` diff --git a/docs/tutorials/http-serving-tutorial.md b/docs/tutorials/http-serving-tutorial.md new file mode 100644 index 00000000..05e3874f --- /dev/null +++ b/docs/tutorials/http-serving-tutorial.md @@ -0,0 +1,72 @@ +--- +sidebar_position: 1 +title: HTTP Serving +description: Learn how to serve HTTP requests +--- + +In this guide, you'll learn how to serve HTTP requests with Operon. +Any Operon function can be made into an HTTP endpoint by annotating it with an _endpoint decorator_. +For example: + +```javascript + @GetApi('/greeting/:name') + static async greetingEndpoint(ctx: HandlerContext, name: string) { + return `Greeting, ${name}`; + } +``` + +We currently support two endpoint decorators, [`GetApi`](../api-reference/decorators#getapi) (HTTP `GET`) and [`PostApi`](../api-reference/decorators#postapi) (HTTP `POST`). +Each associates a function with an HTTP URL. + +### Handlers + +A function annotated with an endpoint decorator but no other decorators is called a _handler_ and must take a [`HandlerContext`](..) as its first argument, like in the example above. +Handlers can invoke other functions and directly access HTTP requests and responses. +However, Operon makes no guarantees about handler execution: if a handler fails, it is not automatically retried. +You should use handlers when you need to access HTTP responses directly or when you're doing a lightweight task that doesn't need the strong guarantees of transactions and workflows. + +You don't need a handler function to serve a transaction, workflow, or communicator from an HTTP URL. +You can also annotate your existing function with an endpoint decorator in addition to its [`@OperonTransaction`](../api-reference/decorators#operontransaction), [`@OperonWorkflow`](../api-reference/decorators#operonworkflow), or [`@OperonCommunicator`](../api-reference/decorators#operoncommunicator) decorator. +For example (from our [quickstart](..)): + +```javascript + @PostApi('/clear/:name') + @OperonTransaction() + static async clearTransaction(txnCtxt: KnexTransactionContext, name: string) { + // Delete greet_count for a user. + await txnCtxt.client("operon_hello") + .where({ name: name }) + .delete() + return `Cleared greet_count for ${name}!\n` + } +``` + +### Inputs and HTTP Requests + +Any Operon method invoked via HTTP request can access the raw request from its `context.request` field. + +If a function has arguments other than its context, Operon attempts to automatically parse them from the HTTP request. +Arguments can be parsed from three places: + +1. From a URL query string parameter with the same name (by default, only for `GET`). +2. From an HTTP body field with the same name (by default, only for `POST`). +3. From an URL path parameter, if one is specified in the decorated URL. + +Input parsing can be configured using the [`@ArgSource`](../api-reference/decorators#argsource) parameter decorator. + +By default, Operon automatically validates parsed inputs, throwing an error if a function is missing required inputs or if the input received is of a different type than specified in the method signature. +Validation can be turned off at the class level using [`@DefaultArgOptional`](..) or controlled at the parameter level using [`@ArgRequired`](..) and [`@ArgOptional`](..). + +### Outputs and HTTP Responses + +By default, if an Operon function invoked via HTTP request returns successfuly, its return value is sent in the HTTP response body with status code `200` (or `204` if nothing is returned). +If the function throws an exception, the error message is sent in the response body with a `400` or `500` status code. +If the error contains a `status` field (we provide [`OperonResponseError`](..) for this purpose), Operon uses that status code instead. + +If you need custom HTTP response behavior, you can use a handler to access the HTTP response directly. +Operon uses [Koa](https://koajs.com/) for HTTP serving internally, so the raw response can be accessed via the `.koaContext.response` field of [`HandlerContext`](..), which provides a [Koa response](https://koajs.com/#response). + +### Middleware + +Operon supports running custom [Koa](https://koajs.com/) middleware for serving HTTP requests. +Middleware is configured at the class level through the [`@KoaMiddleware`](../api-reference/decorators#koamiddleware) decorator. \ No newline at end of file diff --git a/docs/tutorials/project-structure-tutorial.md b/docs/tutorials/project-structure-tutorial.md deleted file mode 100644 index b730cac7..00000000 --- a/docs/tutorials/project-structure-tutorial.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -sidebar_position: 1 -title: Operon Project Structure -description: Learn how to structure an Operon application ---- - -In this guide, you'll learn how to structure an Operon project. -When you initialize an Operon project with `npx operon init`, it has the following structure: - -```bash -operon-hello-app/ -├── README.md -├── knexfile.ts -├── migrations/ -├── node_modules/ -├── operon-config.yaml -├── package-lock.json -├── package.json -├── src/ -│ └── userFunctions.ts -├── start_postgres_docker.sh -└── tsconfig.json -``` - -The two most important files in an Operon project are `operon-config.yaml` and `src/userFunctions.ts`. - -`operon-config.yaml` defines the configuration of an Operon project, including database connection information, ORM configuration, and global logging configuration. -All options are documented in our [configuration reference](..). - -`src/userFunctions.ts` is where Operon looks for your code. -At startup, the Operon runtime automatically loads all classes exported from this file, serving their endpoints and registering their transactions and workflows. -If you're writing a small application, you can write all your code directly in this file. -In a larger application, you can write your code wherever you want, but should use `src/userFunctions.ts` as an index file, exporting code written elsewhere. - -As for the rest of the directory: - -- `knexfile.ts` is a configuration file for [Knex](https://knexjs.org), which we use as a query builder and migration tool. -- `migrations` is initialized with a Knex database migration used in the [quickstart guide](../getting-started/quickstart). If you're using Knex for schema management, you can create your own migrations here. -- `node_modules`, `package-lock.json`, `package.json`, and `tsconfig.json` are needed by all Node/Typescript projects. -- `start_postgres_docker.sh` is a convenience script that initializes a Docker-hosted Postgres database for use in the [quickstart](../getting-started/quickstart). You can modify this script if you want to use Docker-hosted Postgres for local development. diff --git a/docs/tutorials/third-party-tutorial.md b/docs/tutorials/third-party-tutorial.md deleted file mode 100644 index 25d1ddec..00000000 --- a/docs/tutorials/third-party-tutorial.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -sidebar_position: 7 -title: Third party calls -description: Send requests to a third party API from a workflow ---- - -Sending calls to third party API can be done through special [Communicator](../api-reference/decorators#operoncommunicator) methods. Communicators must return JSON serializable objects. - -Here is a simple example that calls `https://postman-echo.com/get`: - -```tsx - @OperonCommunicator() - static async postmanEcho(ctxt: CommunicatorContext) { - ctxt.info("Calling Postman Echo"); - const resp = await axios.get("https://postman-echo.com/get"); - return resp.data; - } -``` - -Communicators output are recorded, like transactions', such that they are only executed once during a workflow execution. -An `OperonCommunicator` has a retry logic configurable through a `CommunicatorConfig`. -Specifically, you can enable or disable retries (enabled by default) and configure the number of retries, their interval, and the exponential backoff multiplier. - -## Final code -```tsx -import { - CommunicatorContext, - OperonCommunicator, - OperonWorkflow, - WorkflowContext, - GetApi, -} from "@dbos-inc/operon"; -import axios from "axios"; - -export class External { - @OperonCommunicator() - static async postmanEcho(ctxt: CommunicatorContext) { - ctxt.info("Calling Postman Echo"); - const resp = await axios.get("https://postman-echo.com/get"); - return resp.data; - } - - @GetApi("/external") - @OperonWorkflow() - static async postmanEchoEndpoint(ctx: WorkflowContext) { - return await ctx.invoke(External).postmanEcho(); - } -} -``` diff --git a/docs/tutorials/transaction-tutorial.md b/docs/tutorials/transaction-tutorial.md index 3d308833..fe72f91a 100644 --- a/docs/tutorials/transaction-tutorial.md +++ b/docs/tutorials/transaction-tutorial.md @@ -1,52 +1,63 @@ --- -sidebar_position: 3 +sidebar_position: 2 title: Transactions -description: Write transactional code +description: Learn how to perform database operations --- -An operon transaction is a function decorated with `@OperonTransaction` and taking a `TransactionContext` as a first argument. In this example, we will extend the `greetingEndpoint` to record greetings in the database. +In this guide, you'll learn how to perform database operations in Operon. -First, let's add a new static function in the `Hello` class: -```tsx -@OperonTransaction() -static async hello(txnCtxt: TransactionContext, name: string) { - const greeting = `Hello, ${name}!` - const { rows } = await txnCtxt.pgClient.query<{ greeting_id: number }>("INSERT INTO OperonHello(greeting) VALUES ($1) RETURNING greeting_id", [greeting]) - return `Greeting ${rows[0].greeting_id}: ${greeting}`; -} -``` +In Operon, your application's database is a first-class citizen. +To perform operations on it, you use a _transaction_ function. +As their name implies, these functions execute as [database transactions](https://en.wikipedia.org/wiki/Database_transaction). -The function is annotated with the [OperonTransaction decoratator](../api-reference/decorators#OperonTransaction). -It takes as parameters a [`TransactionContext`](../api-reference/contexts#transactioncontext) and the `name` to greet. It uses the transaction context to insert a record in your database using a postgres client. Finally, it returns the greeting. +Transaction functions must be annotated with the [`@OperonTransaction`](../api-reference/decorators#operontransaction) decorator and must have a [`TransactionContext`](..) as their first argument. +Like for other Operon functions, inputs and outputs must be serializable to JSON. +The [`TransactionContext`](..) provides a `.client` field you can use to transactionally interact with the database, so you don't need to worry about managing database connections. +By default, this is a [Knex.js](https://knexjs.org/) client. +We like Knex because it's lightweight and helps us write fast but type-safe queries. +However, if you prefer a traditional ORM, we also support [Prisma](..) and [TypeORM](..). -now, let's modify the `greetingEndpoint` to invoke the transaction: -```tsx -@GetApi('/greeting/:name') -static async greetingEndpoint(ctx: HandlerContext, name: string) { - return await ctx.invoke(Hello).hello(name); -} -``` -- `ctx.invoke(Hello)` returns a proxy object containing all Operon operations registered with the `Hello` class -- `.hello(name)` calls the `hello` transaction with the `name` received in the HTTP request and returns a promise +Here's an example of a transaction function (from the [quickstart](../getting-started/quickstart)) written using Knex: -Note Operon will automatically pass a `TransactionContext` to your registered transactions under the hood. +```javascript +import { TransactionContext, OperonTransaction, GetApi, HandlerContext } from '@dbos-inc/operon' +import { Knex } from 'knex'; -## Full code +type KnexTransactionContext = TransactionContext; -```tsx -import { HandlerContext, OperonTransaction, TransactionContext, GetApi } from '@dbos-inc/operon' +interface operon_hello { + name: string; + greet_count: number; +} export class Hello { - @OperonTransaction() - static async hello(txnCtxt: TransactionContext, name: string) { - const greeting = `Hello, ${name}!` - const { rows } = await txnCtxt.pgClient.query<{ greeting_id: number }>("INSERT INTO OperonHello(greeting) VALUES ($1) RETURNING greeting_id", [greeting]) - return `Greeting ${rows[0].greeting_id}: ${greeting}`; - } - @GetApi('/greeting/:name') - static async greetingEndpoint(ctx: HandlerContext, name: string) { - return await ctx.invoke(Hello).hello(name); + @OperonTransaction() + static async helloTransaction(txnCtxt: KnexTransactionContext, name: string) { + // Look up greet_count. + let greet_count = await txnCtxt.client("operon_hello") + .select("greet_count") + .where({ name: name }) + .first() + .then(row => row?.greet_count); + if (greet_count) { + // If greet_count is set, increment it. + greet_count++; + await txnCtxt.client("operon_hello") + .where({ name: name }) + .increment('greet_count', 1); + } else { + // If greet_count is not set, set it to 1. + greet_count = 1; + await txnCtxt.client("operon_hello") + .insert({ name: name, greet_count: 1 }) + } + return `Hello, ${name}! You have been greeted ${greet_count} times.\n`; } } ``` +This function uses the Knex client to first look up `greet_count` for an input `name`, then increment it if it's already set or set it to 1 otherwise. +Operon executes all these operations in a single transaction, so there's no need to worry about race conditions between concurrent updates. +Operon supports the full Knex Postgres API, but doesn't allow manually committing or aborting transactions. +Instead, transactions automatically commit when the function successfully returns and abort and roll back if the function throws an exception. +If you need to orchestrate multiple transactions, use a [workflow](./workflow-tutorial). \ No newline at end of file diff --git a/docs/tutorials/using-typeorm.md b/docs/tutorials/using-typeorm.md new file mode 100644 index 00000000..db574def --- /dev/null +++ b/docs/tutorials/using-typeorm.md @@ -0,0 +1,38 @@ +--- +sidebar_position: 8 +title: Using TypeORM +description: Learn how to create and register TypeORM entities and perform transactional updates +--- + +[TypeORM](https://typeorm.io) is an ORM for TypeScript. It is based on the idea of creating [`Entity`](https://typeorm.io/entities) classes to represent each database table, with the persistent and join key fields marked with [decorators](https://typeorm.io/decorator-reference). + +# Using TypeORM +- [TypeORM Overview](#typeorm-overview) +- [Usage Overview](#usage-overview) + - [Setting Up The Database Connection](#setting-up-the-database-connection) + - [Setting Up Entities](#setting-up-entities) + - [Setting Up The Schema](#setting-up-the-schema) + - [Invoking Transactions](#invoking-transactions) + - [Unit Testing](#unit-testing) +- [TypeORM Example](#typeorm-example) + +## TypeORM Overview +[TypeORM](https://typeorm.io) is an ORM for TypeScript. It is based on the idea of creating [`Entity`](https://typeorm.io/entities) classes to represent each database table, with the persistent and join key fields marked with [decorators](https://typeorm.io/decorator-reference). Once entity classes are defined, TypeORM provides methods for storing, updating, and querying the entities via the [`EntityManager`](https://typeorm.io/working-with-entity-manager). TypeORM can also be used to create and maintain the database schema. + +If you are using TypeORM, Operon needs to know about it so that it can insert workflow status updates into the same transactions used by your applications. + +Use of TypeORM is optional. Operon supports several other libraries for transactional data management. + +## Usage Overview + +### Setting Up The Database Connection + +### Setting Up Entities + +### Setting Up The Schema + +### Invoking Transactions + +### Unit Testing + +## TypeORM Example diff --git a/docs/tutorials/workflow-comm-tutorial.md b/docs/tutorials/workflow-comm-tutorial.md index acf5e67d..70b8a6e1 100644 --- a/docs/tutorials/workflow-comm-tutorial.md +++ b/docs/tutorials/workflow-comm-tutorial.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +sidebar_position: 7 title: Workflows communication description: Two workflows play ping pong ---