diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index fce1626e..8d4ac2ba 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -27,19 +27,51 @@ env: EMAIL: jack20220723@gmail.com jobs: - yml-md-style-and-link-checks: - uses: QubitPi/hashistack/.github/workflows/yml-md-style-and-link-checks.yml@master + cancel-previous: + name: Cancel Previous Runs In Order to Allocate Action Resources Immediately for Current Run + if: github.ref != 'refs/heads/master' + runs-on: ubuntu-latest + steps: + - name: Cancel previous + uses: styfle/cancel-workflow-action@0.10.1 + with: + access_token: ${{ github.token }} + + yaml-lint: + name: YAML Style Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actionshub/yamllint@main + + markdown-lint: + name: Markdown Style Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actionshub/markdownlint@main + + linkChecker: + name: Link Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Link Checker + uses: lycheeverse/lychee-action@v1.9.0 + with: + fail: true tests: name: Unit & Integration Tests - needs: yml-md-style-and-link-checks + needs: [yaml-lint, markdown-lint, linkChecker] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Test environment setup - uses: ./.github/actions/ci-setup - - name: Set up Docker for Integration Tests - uses: docker-practice/actions-setup-docker@master + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: ${{ env.JDK_VERSION }} + distribution: ${{ env.JDK_DISTRIBUTION }} - name: Run unit & integration tests run: mvn -B clean verify @@ -52,8 +84,11 @@ jobs: - uses: actions/setup-node@v3 with: node-version: 18 - - name: Test environment setup - uses: ./.github/actions/ci-setup + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: ${{ env.JDK_VERSION }} + distribution: ${{ env.JDK_DISTRIBUTION }} - name: Install dependencies working-directory: docs run: yarn @@ -82,8 +117,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - - name: Test environment setup - uses: ./.github/actions/ci-setup + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: ${{ env.JDK_VERSION }} + distribution: ${{ env.JDK_DISTRIBUTION }} - name: Build App WAR file so that Docker can pickup during image build run: mvn clean package - name: Set up QEMU diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 00000000..a52d9c5d --- /dev/null +++ b/.mdlrc @@ -0,0 +1,2 @@ +rules "~MD002", "~MD003", "~MD005", "~MD007", "~MD013", "~MD022", "~MD024", "~MD027", "~MD028", "~MD029", "~MD033", "~MD034", "~MD036", "~MD041", "~MD055", "~MD057" +style "#{File.dirname(__FILE__)}/markdownlint.rb" diff --git a/.yamllint b/.yamllint new file mode 100644 index 00000000..6584a13c --- /dev/null +++ b/.yamllint @@ -0,0 +1,23 @@ +--- +extends: default +rules: + line-length: + max: 256 + level: warning + document-start: disable + braces: + forbid: false + min-spaces-inside: 0 + max-spaces-inside: 1 + min-spaces-inside-empty: -1 + max-spaces-inside-empty: -1 + commas: + max-spaces-before: -1 + min-spaces-after: 1 + max-spaces-after: -1 + brackets: + max-spaces-inside: -1 + empty-lines: + max: 3 + indentation: + spaces: 2 diff --git a/README.md b/README.md index dc859dcd..3514e0d3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Jersey Webservice Template [Java Version Badge][Java Version Badge] [![GitHub Workflow Status][GitHub Workflow Status]](https://github.com/QubitPi/jersey-webservice-template/actions/workflows/ci-cd.yml) -![Last Commit](https://img.shields.io/github/last-commit/QubitPi/jersey-webservice-template/master?logo=github&style=for-the-badge) [![Apache License Badge]][Apache License, Version 2.0] [Jersey Webservice Template (JWT)][jersey-webservice-template] is a [JSR 370] web service **template** that lets us @@ -28,12 +27,6 @@ Coming Soon! Documentation ------------- -JWT supports 3 kinds of webservice templates: - -- [A general scaffolding without any vertical-business logics](https://qubitpi.github.io/jersey-webservice-template/docs/intro) -- [A JPA webservice template backed by yahoo/elide](https://qubitpi.github.io/jersey-webservice-template/docs/crud/) -- An async jobstore webservice template similar to yahoo/fili's JobStore design (Developing...) - Comprehensive documentation is viewable on our [website][Documentation] License diff --git a/docs/docs/crud/_category_.json b/docs/docs/crud/_category_.json deleted file mode 100644 index 9ba353b3..00000000 --- a/docs/docs/crud/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "CRUD Webservice", - "position": 6, - "link": { - "type": "generated-index", - "description": "Spinning Up CRUD Jersey Webservice in a Minute" - } -} \ No newline at end of file diff --git a/docs/docs/crud/configuration.mdx b/docs/docs/crud/configuration.mdx deleted file mode 100644 index 9e267a03..00000000 --- a/docs/docs/crud/configuration.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -sidebar_position: 2 -title: Configuration -description: Configurations specific to JPA through yahoo/elide ---- - -The configurations in this page can be set from several sources in the following order: - -1. the [operating system's environment variables]; for instance, an environment variable can be set with - `export DB_URL="jdbc:mysql://db/elide?serverTimezone=UTC"` -2. the [Java system properties]; for example, a Java system property can be set using - `System.setProperty("DB_URL", "jdbc:mysql://db/elide?serverTimezone=UTC")` -3. a **.properties** file placed under CLASSPATH. This file can be put under `src/main/resources` source directory with - contents, for example, `DB_URL=jdbc:mysql://db/elide?serverTimezone=UTC` - -Core Properties ---------------- - -:::note - -The following configurations can be placed in the properties file called **application.properties** - -::: - -- __MODEL_PACKAGE_NAME__: The fully qualified package name that contains a set of Elide JPA models - -JPA DataStore -------------- - -:::note - -The following configurations can be placed in the properties file called **jpadatastore.properties** - -::: - -- **DB_USER**: Persistence DB username (needs have both Read and Write permissions). -- **DB_PASSWORD**: The persistence DB user password. -- **DB_URL**: The persistence DB URL, such as "jdbc:mysql://localhost/elide?serverTimezone=UTC". -- **DB_DRIVER**: The SQL DB driver class name, such as "com.mysql.jdbc.Driver". -- **DB_DIALECT**: The SQL DB dialect name, such as "org.hibernate.dialect.MySQLDialect". -- **HIBERNATE_HBM2DDL_AUTO**: What to do with existing JPA database when webservice starts; can be one of the 4 values: - - 1. _validate_: validate that the schema matches, make no changes to the schema of the database. _This is the default - value of **HIBERNATE_HBM2DDL_AUTO**_ - 2. _update_: update the schema to reflect the entities being persisted - 3. _create_: creates the schema necessary for your entities, destroying any previous data. - 4. _create-drop_: create the schema as in create above, but also drop the schema at the end of the session. This is - great in development or for testing. - - :::note - - This property is exactly the same as [Hibernate `hibernate.hbm2ddl.auto` property]. - - ::: - -CI/CD ------ - -In addition to the ones mentioned in [general CI/CD configs](../configuration#cicd), these -[GitHub Action Secrets][GitHub Action - How to set up] needs to be setup: - -| Secret Name | Definition | How to Get | -|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------| -| APPLICATION_PROPERTIES | The contents of the `src/main/resources/application.properties` mentioned above | See [Core Properties](#core-properties) section above | -| JPADATASTORE_PROPERTIES | The contents of the `src/main/resources/jpadatastore.properties` mentioned above | See [JPA DataStore](#jpa-datastore) section above | -| DATA_MODELS_PRIVATE_REPO_TOKEN | The GitHub Fine-grained token with at least "Read access to code and metadata" repository permissions to the Elide data models repo | [Creating a fine-grained personal access token] | -| DATA_MODELS_PRIVATE_REPO_ORG | The org/user name of the GitHub repo for Elide data models | For [this example][jersey-webservice-template-jpa-data-models], DATA_MODELS_PRIVATE_REPO_ORG is "QubitPi" | -| DATA_MODELS_PRIVATE_REPO_NAME | The name of the GitHub repo for Elide data models | For [this example][jersey-webservice-template-jpa-data-models], DATA_MODELS_PRIVATE_REPO_NAME is "jersey-webservice-template" | - -### CI/CD Chain - -Jersey Webservice Templates adopts the best CI/CD strategies by incorporating its sister projects, [jersey-webservice-template-jpa-data-models] and -[jersey-webservice-template-jpa-data-models-acceptance-tests], into its CI/CD pipeline. Any PR merge into `jpa-elide` branch will trigger the -[CI/CD of its data model](https://github.com/QubitPi/jersey-webservice-template-jpa-data-models/actions), which then triggers -[CI/CD of data model's acceptance tests](https://github.com/QubitPi/jersey-webservice-template-jpa-data-models-acceptance-tests/actions). - -The triggering of its direct downstream project is done through GitHub Actions. See the **triggering** job in [CI/CD definition file]. Basically, the triggering is proxied to -[peter-evans/repository-dispatch]: - -```yaml - triggering: - name: Triggering data model CI/CD - runs-on: ubuntu-latest - steps: - - name: Trigger data model CI/CD - uses: peter-evans/repository-dispatch@v2 - with: - token: ${{ secrets.MY_DATA_MODEL_CICD_TRIGGER }} - repository: my-org/my-data-model-repo - event-type: my-webservice-repo-changes -``` - -For **MY_DATA_MODEL_CICD_TRIGGER** token, it is recommended to use a -[fine-grained personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) -with the following permissions on the target repository (i.e. _my-data-model-repo_): - -- contents: read & write -- metadata: read only (automatically selected when selecting the contents permission) - -In downstream project CI/CD workflow, add the following to the -[on-clause](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on): - -```yaml -"on": - repository_dispatch: - types: [my-webservice-repo-changes] -``` - -Note that how `my-webservice-repo-changes` is used to bridge the event dispatcher (Jersey Webservice Template) and event subscriber (data model project). - -[CI/CD definition file]: https://github.com/QubitPi/jersey-webservice-template/blob/jpa-elide/.github/workflows/ci-cd.yml -[Creating a fine-grained personal access token]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token - -[GitHub Action - How to set up]: https://docs.github.com/en/actions/security-guides/encrypted-secrets - -[Hibernate `hibernate.hbm2ddl.auto` property]: https://stackoverflow.com/questions/18077327/hibernate-hbm2ddl-auto-possible-values-and-what-they-do - -[Java system properties]: https://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html -[jersey-webservice-template-jpa-data-models]: https://github.com/QubitPi/jersey-webservice-template-jpa-data-models -[jersey-webservice-template-jpa-data-models-acceptance-tests]: https://github.com/QubitPi/jersey-webservice-template-jpa-data-models-acceptance-tests - -[operating system's environment variables]: https://docs.oracle.com/javase/tutorial/essential/environment/env.html - -[peter-evans/repository-dispatch]: https://github.com/peter-evans/repository-dispatch diff --git a/docs/docs/crud/elide/_category_.json b/docs/docs/crud/elide/_category_.json deleted file mode 100644 index 29a12e4e..00000000 --- a/docs/docs/crud/elide/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Elide Library Documentation", - "position": 3, - "link": { - "type": "generated-index", - "description": "Spinning Up CRUD Jersey Webservice in a Minute" - } -} \ No newline at end of file diff --git a/docs/docs/crud/elide/analytics.mdx b/docs/docs/crud/elide/analytics.mdx deleted file mode 100644 index 1ea7d06a..00000000 --- a/docs/docs/crud/elide/analytics.mdx +++ /dev/null @@ -1,1810 +0,0 @@ ---- -sidebar_position: 6 -title: Analytic Query Support ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Overview --------- - -Elide includes a semantic modeling layer and analytic query API for OLAP style queries against our database. - -A **semantic model** is the view of the data we want our users to understand. It is typically non-relational (for -simplicity) and consists of concepts like tables, measures, and dimensions. End users refer to these concepts by name -only (they are not expected to derive formulas or know about the physical storage or serialization of data). - -A **virtual semantic layer** maps a semantic model to columns and tables in a physical database. Elide's virtual -semantic layer accomplishes this mapping through a [Hjson](https://hjson.github.io/) configuration language. Hjson is a -human friendly adaptation of JSON that allows comments and a relaxed syntax among other features. Elide's virtual -semantic layer includes the following information: - -- The defintions of tables, measures, and dimensions we want to expose to the end user. -- Metadata like descriptions, categories, and tags that better describe and label the semantic model. -- For every table, measure, and dimension, a SQL fragment that maps it to the physical data. These fragements are used - by elide to generate native SQL queries against the target database. - -Elide leverages the `AggregationDataStore` store to expose the read-only models defined in the semantic model. Model -attributes represent either metrics (for aggregating, filtering, and sorting) and dimensions (for grouping, filtering, -and sorting). Models exposed through the aggregation store are flat and do not contain relationships to other models. - -The Aggregation store includes a companion store, the `MetaDataStore`, which exposes metadata about the Aggregation -store models including their metrics and dimensions. The metadata store models are predefined, read-only, and served -from server memory. - -There are two mechanisms to create models in the Aggregation store's semantic layer: - -1. Through [Hjson](https://hjson.github.io/) configuration files that can be maintained without writing code or - rebuilding the application. -2. Through JVM language classes annotated with Elide annotations. - -The former is preferred for most use cases because of better ergonomics for non-developers. The latter is useful to add -custom Elide security rules or life cycle hooks. - -Querying --------- - -Models managed by the `AggregationDataStore` can be queried via JSON-API or GraphQL similar to other Elide models. -There are a few important distinctions: - -1. If one or more metrics are included in the query, every requested dimension will be used to aggregate the selected - metrics. -2. If only dimensions (no metrics) are included in the query, Elide will return a distinct list of the requested - dimension value combinations. -3. Every elide model includes an ID field. The ID field returned from aggregation store models is not a true identifier. - It represents the row number from a returned result. Attempts to load the model by its identifier will result in an - error. - -### Analytic Queries - -Similar to other Elide models, analytic models can be sorted, filtered, and paginated. A typical analytic query might -look like: - - - - - ```console - /playerStats?fields[playerStats]=highScore,overallRating,countryIsoCode&sort=highScore - ``` - - - - - ```console - { - playerStats(sort: "highScore") { - edges { - node { - highScore - overallRating - countryIsoCode - } - } - } - } - ``` - - - - -Conceptually, these queries might generate SQL similar to: - -```sql -SELECT MAX(highScore), overallRating, countryIsoCode FROM playerStats GROUP BY overallRating, countryIsoCode ORDER BY MAX(highScore) ASC; -``` - -Here are the respective responses: - - - - - ```json - { - "data": [ - { - "type": "playerStats", - "id": "0", - "attributes": { - "countryIsoCode": "HKG", - "highScore": 1000, - "overallRating": "Good" - } - }, - { - "type": "playerStats", - "id": "1", - "attributes": { - "countryIsoCode": "USA", - "highScore": 1234, - "overallRating": "Good" - } - }, - { - "type": "playerStats", - "id": "2", - "attributes": { - "countryIsoCode": "USA", - "highScore": 2412, - "overallRating": "Great" - } - } - ] - } - ``` - - - - - ```json - { - "data": { - "playerStats": { - "edges": [ - { - "node": { - "highScore": 1000, - "overallRating": "Good", - "countryIsoCode": "HKG" - } - }, - { - "node": { - "highScore": 1234, - "overallRating": "Good", - "countryIsoCode": "USA" - } - }, - { - "node": { - "highScore": 2412, - "overallRating": "Great", - "countryIsoCode": "USA" - } - } - ] - } - } - } - ``` - - - - -### Metadata Queries - -A full list of available table and column metadata is covered in the [configuration section](#tables). Metadata can be -queried through the *table* model and its associated relationships. - - - - - ```console - /table/playerStats?fields[table]=name,category,description,requiredFilter,tags,metrics,dimensions,timeDimensions - ``` - - - - - ```console - { - table(ids: ["playerStats"]) { - edges { - node { - name - category - description - requiredFilter - tags - metrics {edges {node {id}}} - dimensions {edges {node {id}}} - timeDimensions {edges {node {id}}} - } - } - } - } - ``` - - - - -Here are the respective responses: - - - - - ```json - { - "data": { - "type": "table", - "id": "playerStats", - "attributes": { - "category": "Sports Category", - "description": "Player Statistics", - "name": "playerStats", - "requiredFilter": "", - "tags": [ - "Game", - "Statistics" - ] - }, - "relationships": { - "dimensions": { - "data": [ - { - "type": "dimension", - "id": "playerStats.playerName" - }, - { - "type": "dimension", - "id": "playerStats.player2Name" - }, - { - "type": "dimension", - "id": "playerStats.playerLevel" - }, - { - "type": "dimension", - "id": "playerStats.overallRating" - }, - { - "type": "dimension", - "id": "playerStats.countryIsInUsa" - }, - { - "type": "dimension", - "id": "playerStats.countryIsoCode" - }, - { - "type": "dimension", - "id": "playerStats.countryUnSeats" - }, - { - "type": "dimension", - "id": "playerStats.countryNickName" - }, - { - "type": "dimension", - "id": "playerStats.subCountryIsoCode" - } - ] - }, - "metrics": { - "data": [ - { - "type": "dimension", - "id": "playerStats.id" - }, - { - "type": "metric", - "id": "playerStats.lowScore" - }, - { - "type": "metric", - "id": "playerStats.highScore" - }, - { - "type": "metric", - "id": "playerStats.highScoreNoAgg" - } - ] - }, - "timeDimensions": { - "data": [ - { - "type": "timeDimension", - "id": "playerStats.updatedDate" - }, - { - "type": "timeDimension", - "id": "playerStats.recordedDate" - } - ] - } - } - } - } - ``` - - - - - ```json - { - "data": { - "table": { - "edges": [ - { - "node": { - "name": "playerStats", - "category": "Sports Category", - "description": "Player Statistics", - "requiredFilter": "", - "tags": [ - "Game", - "Statistics" - ], - "metrics": { - "edges": [ - { - "node": { - "id": "playerStats.id" - } - }, - { - "node": { - "id": "playerStats.highScoreNoAgg" - } - }, - { - "node": { - "id": "playerStats.lowScore" - } - }, - { - "node": { - "id": "playerStats.highScore" - } - } - ] - }, - "dimensions": { - "edges": [ - { - "node": { - "id": "playerStats.countryUnSeats" - } - }, - { - "node": { - "id": "playerStats.overallRating" - } - }, - { - "node": { - "id": "playerStats.countryNickName" - } - }, - { - "node": { - "id": "playerStats.player2Name" - } - }, - { - "node": { - "id": "playerStats.countryIsoCode" - } - }, - { - "node": { - "id": "playerStats.playerName" - } - }, - { - "node": { - "id": "playerStats.playerLevel" - } - }, - { - "node": { - "id": "playerStats.countryIsInUsa" - } - }, - { - "node": { - "id": "playerStats.subCountryIsoCode" - } - } - ] - }, - "timeDimensions": { - "edges": [ - { - "node": { - "id": "playerStats.recordedDate" - } - }, - { - "node": { - "id": "playerStats.updatedDate" - } - } - ] - } - } - } - ] - } - } - } - ``` - - - - -Configuration -------------- - -### Feature Flags - -There are feature flags that enable Hjson configuration, analytic queries, and [Metadata queries](#metadata-queries) -respectively: - -| Name | Description | Default | -| ---------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| ------- | -| `elide.aggregation-store.dynamic-config.enabled` | Enable model creation through the Hjson configuration files. | `false` | -| `elide.aggregation-store.enabled` | Enable support for data analytic queries. | `false` | -| `elide.aggregation-store.metadata-store.enabled` | Enable the metadata query APIs exposing the metadata about the Aggregation store models including their metrics and dimensions. | `false` | - - - - - Configure in `application.yaml`. - - ```yaml - elide: - aggregation-store: - enabled: true - metadata-store: - enabled: true - dynamic-config: - enabled: true - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneAnalyticSettings getAnalyticProperties() { - return new ElideStandaloneAnalyticSettings() { - @Override - public boolean enableDynamicModelConfig() { - return true; - } - @Override - public boolean enableAggregationDataStore() { - return true; - } - @Override - public boolean enableMetaDataStore() { - return true; - } - }; - } - } - ``` - - - - -### File Layout - -Analtyic model configuration can either be specified through JVM classes decorated with Elide annotations _or_ Hjson -configuration files. Hjson configuration files can be sourced either from the local filesystem or the classpath. If -Hjson configuration is found in the classpath, the filesystem is ignored. All Hjson configuration must conform to the -following directory structure: - -``` -CONFIG_ROOT/ - ├── models/ - | ├── tables/ - | | ├── model1.hjson - | | ├── model2.hjson - | ├── namespaces/ - | | ├── namespace1.hjson - | | ├── namespace2.hjson - | ├── security.hjson - | └── variables.hjson - ├── db/ - | ├── sql/ - | | ├── db1.hjson - | ├── variables.hjson -``` - -1. Analytic model files are stored in `/models/tables`. Multiple models can be grouped together into a single file. -2. Analytic models can optionally belong to a namespace - a grouping of related models with the same API prefix. - Namespace configuration is defined in `/models/namespaces`. -3. Security rules are stored in `/models/security.hjson`. -4. Model, namespace, and security Hjson files support variable substitution with variables defined in - `/models/variables.hjson`. -5. Data source configurations are stored in `/db/sql`. Multiple configurations can be grouped together into a single - file. -6. Data source Hjson files support variable substitution with variables defined in `/db/variables.hjson`. - -CONFIG_ROOT can be any directory in the filesystem or classpath. The root configuration location can be set as follows: - - - - - Configure in `application.yaml`. - - ```yaml - elide: - aggregation-store: - dynamic-config: - path: src/resources/configs - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneAnalyticSettings getAnalyticProperties() { - return new ElideStandaloneAnalyticSettings() { - @Override - public String getDynamicConfigPath() { - return File.separator + "configs" + File.separator; - } - }; - } - } - ``` - - - - -### Data Source Configuration - -The Aggregation Data Store does not leverage JPA, but rather uses JDBC directly. By default, Elide will leverage the -default JPA configuration for establishing connections through the Aggregation Data Store. However, more complex -configurations are possible including: - -1. Using a different JDBC data source other than what is configured for JPA. -2. Leveraging multiple JDBC data sources for different Elide models. - -For these complex configurations, we must configure Elide using the Aggregation Store's Hjson configuration language. -The following configuration file illustrates two data sources. Each data source configuration includes: - -1. A name that will be referenced in our Analytic models (effectively binding them to a data source). -2. A JDBC URL -3. A JDBC driver -4. A user name -5. An [Elide SQL Dialect](#dialects). This can either be the name of an Elide supported dialect _or_ it can be the fully - qualified class name of an implementation of an Elide dialect. -6. A map of driver specific properties. - -``` -{ - dbconfigs: - [ - { - name: Presto Data Source - url: jdbc:presto://localhost:4443/testdb - driver: com.facebook.presto.jdbc.PrestoDriver - user: guestdb2 - dialect: PrestoDB - } - { - name: Hive Data Source - url: jdbc:hive2://localhost:4444/dbName - driver: org.apache.hive.jdbc.HiveDriver - user: guestmysql - dialect: com.paiondata.elide.datastores.aggregation.queryengines.sql.dialects.impl.HiveDialect - propertyMap: - { - sslEnabled : true - } - } - ] -} -``` - -By default, Elide uses HikariCP's DataSource for JDBC connection pool. A custom `DataSourceConfiguration` can be -configured by the following override: - - - - - Create a `@Configuration` class that defines our custom implementation as a `@Bean`. - - ```java - @Configuration - public class ElideConfiguration { - @Bean - public DataSourceConfiguration dataSourceConfiguration() { - return new DataSourceConfiguration() { - @Override - public DataSource getDataSource(DBConfig dbConfig, DBPasswordExtractor dbPasswordExtractor) { - HikariConfig config = new HikariConfig(); - - config.setJdbcUrl(dbConfig.getUrl()); - config.setUsername(dbConfig.getUser()); - config.setPassword(dbPasswordExtractor.getDBPassword(dbConfig)); - config.setDriverClassName(dbConfig.getDriver()); - dbConfig.getPropertyMap().forEach((k, v) -> config.addDataSourceProperty(k, v)); - - return new HikariDataSource(config); - } - }; - } - } - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public DataSourceConfiguration getDataSourceConfiguration() { - return new DataSourceConfiguration() { - @Override - public DataSource getDataSource(DBConfig dbConfig, DBPasswordExtractor dbPasswordExtractor) { - HikariConfig config = new HikariConfig(); - - config.setJdbcUrl(dbConfig.getUrl()); - config.setUsername(dbConfig.getUser()); - config.setPassword(dbPasswordExtractor.getDBPassword(dbConfig)); - config.setDriverClassName(dbConfig.getDriver()); - dbConfig.getPropertyMap().forEach((k, v) -> config.addDataSourceProperty(k, v)); - - return new HikariDataSource(config); - } - }; - } - } - ``` - - - - -#### Data Source Passwords - -Data source passwords are provided out of band by implementing a `DBPasswordExtractor`: - -```java -public interface DBPasswordExtractor { - String getDBPassword(DBConfig config); -} -``` - -A custom `DBPasswordExtractor` can be configured by the following override: - - - - - Create a `@Configuration` class that defines our custom implementation as a `@Bean`. - - ```java - @Configuration - public class ElideConfiguration { - @Bean - public DBPasswordExtractor dbPasswordExtractor() { - return new DBPasswordExtractor() { - @Override - public String getDBPassword(DBConfig config) { - return StringUtils.EMPTY; - } - }; - } - } - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneAnalyticSettings getAnalyticProperties() { - return new ElideStandaloneAnalyticSettings() { - @Override - public DBPasswordExtractor getDBPasswordExtractor() { - return new DBPasswordExtractor() { - @Override - public String getDBPassword(DBConfig config) { - return StringUtils.EMPTY; - } - }; - } - }; - } - } - ``` - - - - -#### Dialects - -A dialect must be configured for Elide to correctly generate analytic SQL queries. Elide supports the following dialects -out of the box: - -| Friendly Name | Class | -| ------------- |-------------------------------------------------------------------------------------------| -| H2 | com.paiondata.elide.datastores.aggregation.queryengines.sql.dialects.impl.H2Dialect | -| Hive | com.paiondata.elide.datastores.aggregation.queryengines.sql.dialects.impl.HiveDialect | -| PrestoDB | com.paiondata.elide.datastores.aggregation.queryengines.sql.dialects.impl.PrestoDBDialect | -| Postgres | com.paiondata.elide.datastores.aggregation.queryengines.sql.dialects.impl.PostgresDialect | -| MySQL | com.paiondata.elide.datastores.aggregation.queryengines.sql.dialects.impl.MySQLDialect | -| Druid | com.paiondata.elide.datastores.aggregation.queryengines.sql.dialects.impl.DruidDialect | - -If not leveraging Hjson configuration, a default dialect can be configured for analytic queries: - - - - - Configure in `application.yaml`. - - ```yaml - elide: - aggregation-store: - default-dialect: H2 - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneAnalyticSettings getAnalyticProperties() { - return new ElideStandaloneAnalyticSettings() { - @Override - public String getDefaultDialect() { - return "Hive"; - } - }; - } - } - ``` - - - - -### Model Configuration - -#### Concepts - -Elide exposes a virtual semantic model of tables and columns that represents a data warehouse. The virtual semantic -model can be mapped to one or more physical databases, tables, and columns through configuration by a data analyst. The -analyst maps virtual tables and columns to fragments of native SQL queries that are later assembled into complete SQL -statements at query time. - -Analytic models are called **Tables** in Elide. They are made up of: - -1. **Metrics** - Numeric columns that can be aggregated, filtered on, and sorted on. -2. **Dimensions** - Columns that can be grouped on, filtered on, and sorted on. -3. **TimeDimension** - A type of **Dimension** that represents time. Time dimensions are tied to grain (a period) and a - timezone. -4. **Columns** - The supertype of **Metrics**, **Dimensions**, and **TimeDimensions**. All columns share a set of common - metadata. -5. **Joins** - Even though Elide analytic models are flat (there are no relationships to other models), individual model - columns can be sourced from multiple physical tables. **Joins** provide Elide the information it needs to join other - database tables at query time to compute a given column. -6. **Namespace** - Every **Table** maps to one **Namespace** or the *default* **Namespace** if undefined. **Namespaces** - group related tables together that share a common API prefix. - -Other concepts include: - -1. **Arguments** - **Tables** and **Columns** can optionally have **Arguments**. They represent parameters that are - supplied by the client to change how the column or table SQL is generated. -2. **Table Source** - **Columns** and **Arguments** can optionally include metadata about their distinct legal values. **Table Source** references another **Column** in a different **Table** where the values are stored. - -#### Example Configuration - - - - - ```hjson - { - tables: [{ - name: PlayerStats - table: playerStats - dbConnectionName: Presto Data Source - friendlyName: Player Stats - description: - ''' - A long description - ''' - category: Sports - cardinality : large - readAccess : '(user AND member) OR (admin.user AND NOT guest user)' - filterTemplate : createdOn>={{start}};createdOn<{{end}} - isFact : true - tags: ['Game', 'Player'] - joins: [ - { - name: playerCountry - to: PlayerCountry - kind: toOne - type: left - definition: '{{playerCountry.$id}} = {{$country_id}}' - } - ] - measures : [ - { - name : highScore - type : INTEGER - definition: 'MAX({{$highScore}})' - friendlyName: High Score - } - ] - dimensions : [ - { - name : name - type : TEXT - definition : '{{$name}}' - cardinality : large - }, - { - name : countryCode - type : TEXT - definition : '{{playerCountry.isoCode}}' - friendlyName: Country Code - }, - { - name : gameType - type : TEXT - definition : '{{$game_type}}' - friendlyName: Game Type - }, - { - name : gameOn - type : TIME - definition : '{{$game_on}}' - grains: - [ - { - type: MONTH - sql: PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy-MM'), 'yyyy-MM') - }, - { - type: DAY - sql: PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy-MM-dd'), 'yyyy-MM-dd') - }, - { - type: SECOND - sql: PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy-MM-dd HH:mm:ss'), 'yyyy-MM-dd HH:mm:ss') - } - ] - }, - { - name : createdOn - type : TIME - definition : '{{$created_on}}' - grains: - [{ - type : DAY - sql : ''' - PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy-MM-dd'), 'yyyy-MM-dd') - ''' - }] - }, - { - name : updatedOn - type : TIME - definition : '{{$updated_on}}' - grains: - [{ - type : MONTH - sql : ''' - PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy-MM'), 'yyyy-MM') - ''' - }] - } - ] - }] - } - ``` - - - - - ```java - @Include - @VersionQuery(sql = "SELECT COUNT(*) from playerStats") - @FromTable(name = "playerStats", dbConnectionName = "Presto Data Source") - @TableMeta(description = "A long description", category = "Sports", tags = {"Game", "Player"}, filterTemplate = "createdOn>={{start}};createdOn<{{end}}", size = CardinalitySize.LARGE, friendlyName = "Player Stats") - @ReadPermission(expression = "(user AND member) OR (admin.user AND NOT guest user)") - public class PlayerStats { - - public static final String DATE_FORMAT = "PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy-MM-dd'), 'yyyy-MM-dd')"; - public static final String YEAR_MONTH_FORMAT = "PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy-MM'), 'yyyy-MM')"; - - @Id - private String id; - - @MetricFormula("MAX({{$highScore}})") - @ColumnMeta(friendlyName = "High Score") - private long highScore; - - @ColumnMeta(size = CardinalitySize.LARGE) - private String name; - - @Join("{{$country_id}} = {{playerCountry.$id}}", type = JoinType.LEFT) - private Country playerCountry; - - @DimensionFormula("{{playerCountry.isoCode}}") - @ColumnMeta(friendlyName = "Country Code") - private String countryCode; - - @DimensionFormula("{{$game_type}}") - @ColumnMeta(friendlyName = "Game Type") - private String gameType; - - @Temporal(grains = { - @TimeGrainDefinition(grain = TimeGrain.DAY, expression = DATE_FORMAT), - @TimeGrainDefinition(grain = TimeGrain.MONTH, expression = YEAR_MONTH_FORMAT) - }, timeZone = "UTC") - @DimensionFormula("{{$game_on}}") - private Time gameOn; - - @Temporal(grains = { @TimeGrainDefinition(grain = TimeGrain.DAY, expression = DATE_FORMAT) }, timeZone = "UTC") - @DimensionFormula("{{$created_on}}") - private Time createdOn; - - @Temporal(grains = { @TimeGrainDefinition(grain = TimeGrain.MONTH, expression = YEAR_MONTH_FORMAT) }, timeZone = "UTC") - @DimensionFormula("{{$updated_on}}") - private Time updatedOn; - } - ``` - - - - -#### Handlebars Templates - -There are a number of locations in the model configuration that require a SQL fragment. These include: - -- Column definitions -- Table query definitions -- Table join expressions - -SQL fragments cannot refer to physical database tables or columns directly by name. Elide generates SQL queries at -runtime, and these queries reference tables and columns by aliases that are also generated. Without the correct alias, -the generated SQL query will be invalid. Instead, physical table and column names should be substituted with -[handlebars](https://handlebarsjs.com/guide/) template expressions. - -All SQL fragments support handlebars template expressions. The handlebars context includes the following fields we can -reference in our templated SQL: - -1. `{{$columnName}}` - Expands to the correctly aliased, physical database column name for the current Elide model. -2. `{{columnName}}` - Expands another column in the current Elide model. -3. `{{joinName.column}}` - Expands to a column in another Elide model joined to the current model through the referenced - join. -4. `{{joinName.$column}}` - Expands to the correctly aliased, physical database column name for another Elide model - joined to the current model through the referenced join. -5. `{{$$table.args.argumentName}}` - Expands to a table argument passed by the client or extracted from the client query - filter through a table [filterTemplate](#filter-templates). -6. `{{$$column.args.argumentName}}` - Expands to a column argument passed by the client or extracted from the client - query filter through a column [filterTemplate](#filter-templates). $$column always refers to the current column that - is being expanded. -7. `{{$$column.expr}}` - Expands to a column's SQL fragment. $$column always refers to the current column that is being - expanded. - -Join names can be linked together to create a path from one model to another model's column through a set of joins. For -example the handlebar expression: `{{join1.join2.join3.column}}` references a column that requires three separate joins. - -The templating engine also supports a custom handlebars helper that can reference another column and provide overridden -column arguments: - -1. `{{sql column='columnName[arg1:value1][arg2:value2]'}}` - Expands to a column in the current Elide model with - argument values explicitly set. -2. `{{sql from='joinName' column='columnName'}}` - Identical to `{{joinName.columnName}}`. -3. `{{sql from='joinName' column='$columnName'}}` - Identical to `{{joinName.$columnName}}`. -4. `{{sql from='joinName' column='columnName[arg1:value1]'}}` - Identical to `{{joinName.columnName}}` but passing - 'value1' for the column argument, 'arg1'. - -The helper takes two arguments: - -1. **column** - The column to expand. Optional column arguments (`[argumentName:argumentValue]`) can be appended after - the column name. -2. **from** - An optional argument containing the join name where to source the column from. If not present, the column - is sourced from the current model. - -#### Tables - -Tables must source their columns from somewhere. There are three, mutually exclusive options: - -1. Tables can source their columns from a physical table by its name. -2. Tables can source their columns from a SQL subquery. -3. Tables can extend (override or add columns to) an existing Table. More details can be found [here](#inheritance). - -These options are configured via the 'table', 'sql', and 'extend' [properties](#table-properties). - -##### Table Properties - -Tables include the following simple properties: - -| Hjson Property | Explanation | Hjson Value | Annotation/Java Equivalent | -| --------------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|-----------------------------------------------------------| -| name | The name of the elide model. It will be exposed through the API with this name. | tableName | `@Include(name="tableName")` | -| version | If leveraging Elide API versions, the API version associated with this model. | 1.0 | `@ApiVersion(version="1.0")` | -| friendlyName | The friendly name for this table. Unicode characters are supported. | 'Player Stats' | `@TableMeta(friendlyName="Player Stats")` | -| description | A description of the table. | 'A description for tableName' | `@TableMeta(description="A description for tableName")` | -| category | A free-form text category for the table. | 'Some Category' | `@TableMeta(category="Some Category")` | -| tags | A list of free-form text labels for the table. | ['label1', 'label2'] | `@TableMeta(tags={"label1","label2"})` | -| cardinality | tiny, small, medium, large, huge - A hint about the number of records in the table. | small | `@TableMeta(size=CardinalitySize.SMALL)` | -| dbConnectionName | The name of the physical data source where this table can be queried. This name must match a data source configuration name. | MysqlDB | `@FromTable(dbConnectionName="MysqlDB")` | -| schema | The database schema where the physical data resides | schemaName | `@FromTable(name=schemaName.tableName)` | -| table | Exactly one of _table_, _sql_, and _extend_ must be provided. Provides the name of the physical base table where data will be sourced from. | tableName | `@FromTable(name=tableName)` | -| sql | Exactly one of _table_, _sql_, and _extend_ must be provided. Provides a SQL subquery where the data will be sourced from. | 'SELECT foo, bar FROM blah;' | `@FromSubquery(sql="SELECT foo, bar FROM blah;")` | -| extend | Exactly one of _table_, _sql_, and _extend_ must be provided. This model extends or inherits from another analytic model. | tableName | class Foo extends Bar | -| readAccess | An elide permission rule that governs read access to the table. | 'member and admin.user' | `@ReadPermission(expression="member and admin.user")` | -| filterTemplate | An RSQL filter expression template that either must directly match the client provided filter or be conjoined with logical 'and' to the client provided filter. | countryIsoCode==\{\{code\}\} | @TableMeta(filterTemplate="countryIsoCode==\{\{code\}\}") | -| hidden | The table is not exposed through the API. | true | `@Exclude` | -| isFact | Is the table a fact table. Models annotated using FromTable or FromSubquery or TableMeta or configured through Hjson default to true unless marked otherwise. Yavin will use this flag to determine which tables can be used to build reports. | true | `@TableMeta(isFact=false)` | -| namespace | The namepsace this table belongs to. If none is provided, the default namespace is presumed. | SalesNamespace | `@Include(name="namespace")` on the Java package. | -| hints | A list of optimizer hints to enable for this particular table. This is an [experimental feature](#query-optimization). | ['AggregateBeforeJoin'] | @TableMeta(hints="AggregateBeforeJoin") | - -Tables also include: - -- A list of [columns](#columns) including measures, dimensions, and time dimensions. -- A list of [joins](#joins). -- A list of [arguments](#arguments). - - - - - ```hjson - { - tables: - [ - { - namespace: SalesNamespace - name: orderDetails - friendlyName: Order Details - description: Sales orders broken out by line item. - category: revenue - tags: [Sales, Revenue] - cardinality: large - isFact: true - filterTemplate: 'recordedDate>={{start}};recordedDate<{{end}}' - - #Instead of table, could also specify either 'sql' or 'extend'. - table: order_details - schema: revenue - dbConnectionName: SalesDBConnection - hints: [AggregateBeforeJoin] - - readAccess: guest user - - arguments: [] - joins: [] - measures: [] - dimensions: [] - } - ] - } - ``` - - - - - - ```java - @Include(name = "SalesNamespace") - package example; - - import com.paiondata.elide.annotation.Include; - ``` - - - - - - ```java - @Include(name = "orderDetails") //Tells Elide to expose this model in the API. - @VersionQuery(sql = "SELECT COUNT(*) from playerStats") //Used to detect when the cache is stale. - @FromTable( //Could also be @FromSubquery - name = "revenue.order_details", - dbConnectionName = "SalesDBConnection" - ) - @TableMeta( - friendlyName = "Order Details", - description = "Sales orders broken out by line item.", - category = "revenue", - tags = {"Sales", "Revenue"}, - size = CardinalitySize.LARGE, - isFact = true, - filterTemplate = "recordedDate>={{start}};recordedDate<{{end}}", - hints = {"AggregateBeforeJoin"}, - ) - @ReadPermission(expression = "guest user") - public class OrderDetails extends ParameterizedModel { //ParameterizedModel is a required base class if any columns take arguments. - //... - } - ``` - - - - -#### Columns - -Columns are either measures, dimensions, or time dimensions. They all share a number of -[common properties](#column-properties). The most important properties are: - -1. The name of the column. -2. The data type of the column. -3. The definition of the column. - -Column definitions are [templated, native SQL fragments](#handlebars-templates). Columns definitions can include -references to other column definitions or physical column names that are expanded at query time. Column expressions can -be defined in Hjson or Java: - - - - - ```hjson - { - measures : [ - { - name : highScore - type : INTEGER - definition: 'MAX({{$highScore}})' - } - ] - dimensions : [ - { - name : name - type : TEXT - definition : '{{$name}}' - }, - { - name : countryCode - type : TEXT - definition : '{{playerCountry.isoCode}}' - } - ] - } - ``` - - - - - - ```java - // A Dimension - @DimensionFormula("CASE WHEN {{$name}} = 'United States' THEN true ELSE false END") - private boolean inUsa; - - // A metric - @MetricFormula("{{wins}} / {{totalGames}} * 100") - private float winRatio; - ``` - - - - -##### Column Properties - -Columns include the following properties: - -| Hjson Property | Explanation | Example Hjson Value | Annotation/Java Equivalent | -| --------------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|----------------------------------------------------------------------------------------| -| name | The name of the column. It will be exposed through the API with this name. | columnName | String columnName; | -| friendlyName | The friendly name for this column to be displayed in the UI. | 'Country Code' | `@ColumnMeta(friendlyName = "Country Code")` | -| description | A description of the column. | 'A description for columnA' | `@ColumnMeta(description="A description for columnA")` | -| category | A free-form text category for the column. | 'Some Category' | `@ColumnMeta(category="Some Category")` | -| tags | A list of free-form text labels for the column. | ['label1', 'label2'] | `@ColumnMeta(tags={"label1","label2"})` | -| cardinality | tiny, small, medium, large, huge - A hint about the dimension's cardinality. | small | `@ColumnMeta(size=CardinalitySize.SMALL)` | -| readAccess | An elide permission rule that governs read access to the column. | 'admin.user' | `@ReadPermission(expression="admin.user")` | -| definition | A SQL fragment that describes how to generate the column. | MAX(\{\{sessions\}\}) | @DimensionFormula("CASE WHEN \{\{name\}\} = 'United States' THEN true ELSE false END") | -| type | The data type of the column. One of 'INTEGER', 'DECIMAL', 'MONEY', 'TEXT', 'COORDINATE', 'BOOLEAN' or a fully qualified java class name (dimensions only). | 'BOOLEAN' | String columnName; | -| hidden | The column is not exposed through the API. | true | `@Exclude` | - -Non-time dimensions include the following, mutually exclusive properties that describe the set of discrete, legal values -(for type-ahead search or other usecases) : - -| Hjson Property | Explanation | Example Hjson Value | Annotation/Java Equivalent | -| --------------------- |----------------------------------------------------------------------------------|---------------------------------------------------|------------------------------------------------------------| -| values | An optional enumerated list of dimension values for small cardinality dimensions | ['Africa', 'Asia', 'North America'] | `@ColumnMeta(values = {"Africa", "Asia", "North America")` | -| tableSource | The semantic table and column names where to find the values | See the section on [Table Source](#table-source). | See the section on [Table Source](#table-source). | - -##### Time Dimensions And Time Grains - -Time dimensions represent time and include one or more time grains. The time grain determines how time is represented as -text in query filters and query results. Supported time grains include: - -| Grain | Text Format | -| ------------ | --------------------- | -| SECOND | "yyyy-MM-dd HH:mm:ss" | -| MINUTE | "yyyy-MM-dd HH:mm" | -| HOUR | "yyyy-MM-dd HH" | -| DAY | "yyyy-MM-dd" | -| WEEK | "yyyy-MM-dd" | -| ISOWEEK | "yyyy-MM-dd" | -| MONTH | "yyyy-MM" | -| QUARTER | "yyyy-MM" | -| YEAR | "yyyy" | - -When defining a time dimension, a native SQL expression may be provided with the grain to convert the underlying column -(represented as `{{$$column.expr}}`) to its expanded SQL definition: - - - - - ```hjson - { - name : createdOn - type : TIME - definition : "FORMATDATETIME({{$createdOn}}, 'yyyy-MM')" - grains: - [{ - type : MONTH - sql : ''' - PARSEDATETIME({{$$column.expr}}, 'yyyy-MM') - ''' - }] - } - ``` - - - - - - ```java - public static final String DATE_FORMAT = "PARSEDATETIME({{$$column.expr}}, 'yyyy-MM')"; - - @Temporal(grains = {@TimeGrainDefinition(grain = TimeGrain.MONTH, expression = DATE_FORMAT)}, timeZone = "UTC") - @DimensionFormula("FORMATDATETIME({{$createdOn}}, 'yyyy-MM')") - private Date createdOn; - ``` - - - - -Elide would expand the above example to this SQL fragment: -`PARSEDATETIME(FORMATDATETIME(createdOn, 'yyyy-MM'), 'yyyy-MM')`. - -Time grain definitions are optional and default to type 'DAY' with a native SQL expression of `{{$\$column.expr}}`. - -#### Joins - -Table joins allow column expressions to reference fields from other tables. At query time, if a column requires a join, -the join will be added to the generated SQL query. Each table configuration can include zero or more join definitions: - - - - - ```hjson - joins: [ - { - name: playerCountry - to: country - kind: toOne - type: left - definition: '{{$country_id}} = {{playerCountry.$id}}' # 'playerCounty' here is the join name. - }, - { - name: playerTeam - to: team - kind: toMany - type: full - definition: '{{$team_id}} = {{playerTeam.$id}}' # 'playerTeam' here is the joinName. - } - ] - ``` - - - - - - ```java - private Country country; - private Team team; - - //'country' here is the the join/field name. - @Join("{{$country_id}} = {{country.$id}}", type = JoinType.LEFT) - public Country getCountry() { - return country; - } - - //'team' here is the the join/field name. - @Join("{{$team_id}} = {{team.$id}}", type = JoinType.FULL) - public Team getTeam() { - return team; - } - ``` - - - - -##### Join Properties - -Each join definition includes the following properties: - -| Hjson Property | Explanation | -| --------------------- |---------------------------------------------------------------------------------------------------| -| name | A unique name for the join. The name can be referenced in column definitions. | -| namespace | The namepsace the join table belongs to. If none is provided, the default namespace is presumed. | -| to | The name of the Elide model being joined against. This can be a semantic model or a CRUD model. | -| kind | 'toMany' or 'toOne' (Default: toOne) | -| type | 'left', 'inner', 'full' or 'cross' (Default: left) | -| definition | A templated SQL join expression. See below. | - -##### Join Definition - -Join definitions are [templated SQL expressions](#handlebars-templates) that represent the *ON* clause of a SQL -statement: - -``` -definition: "{{$orderId}} = {{delivery.$orderId}} AND {{delivery.$delivered_on }} > '1970-01-01'" -``` - -#### Arguments - -Columns and tables can both be parameterized with arguments. Arguments include the following properties: - -| Hjson Property | Explanation | -| --------------------- |------------------------------------------------------------------| -| name | The name of the argument | -| description | The argument description | -| type | The [primitive type](#column-and-argument-types) of the argument | -| values | An optional list of allowed values | -| default | An optional default value if none is supplied by the client | - -In addition, arguments can also optionally reference a [Table Source](#table-source). The properties `values` and -`tableSource` are mutually exclusive. - -#### Column And Argument Types - -Column and argument values are mapped to primitive types which are used for validation, serialization, deserialization, -and formatting. - -The following primitive types are supported: - -1. **Time** - Maps to [Elide supported time grains](#time-dimensions-and-time-grains). -2. **Integer** - Integer number. -3. **Decimal** - Decimal number. -4. **Money** - A decimal number that represents money. -5. **Text** - A text string. -6. **Coordinate** - A text representation of latitude, longitude or both. -7. **Boolean** - true or false. -8. **Id** - Represents the ID of the model. For analytic models, this is the row number and not an actual primary key. - -Input values (filter values, column arguments, or table arguments) are validated by: - -1. Type coercion to the underlying Java data type. -2. Regular expression matching using the - [following rules](https://github.com/paion-data/elide/blob/master/elide-datastore/elide-datastore-aggregation/src/main/java/com/paiondata/elide/datastores/aggregation/metadata/enums/ValueType.java). - -#### Table Source - -Table sources contain additional metadata about where distinct legal values of a column or argument can be found. This -metadata is intended to aid presentation layers with search suggestions. - -| Hjson Property | Explanation | -| --------------------- |------------------------------------------------------------------------------------------------------------------------------| -| table | The table where the distinct values can be located. | -| namespace | The namespace that qualifies the table. If not provided, the default namespace is presumed. | -| column | The column in the table where the distinct values can be located | -| suggestionColumns | Zero or more additional columns that should be searched in conjunction with the primary column to locate a particular value. | - - - - - ```hjson - dimensions : [ - { - name : countryNickname - type : TEXT - definition : '{{country.nickName}}' - tableSource : { - table: country - column: nickName - suggestionColumns: [name, description] - } - } - ] - ``` - - - - - ```java - @DimensionFormula("{{country.nickName}}") - @ColumnMeta( - tableSource = @TableSource(table = "country", column = "nickName", suggestionColumn = {"name", "description"}) - ) - private String countryNickName; - ``` - - - - -#### Namespaces - -Namespaces organize a set of related tables together so that they can share: - -- Package level metadata like name, friendly name, and description. -- Default read permission rules for every table in the namespace. -- A common API prefix that is prepended to each model name in the namespace (`NamespaceName_ModelName`). - -While, namespaces are optional, all tables belong to one and only one namespace. If no namespace is defined, the table -will belong to the 'default' namespace. The default namespace does not have an API prefix. - - - - - ```hjson - { - namespaces: - [ - { - name: SalesNamespace - description: Namespace for Sales Schema Tables - friendlyName: Sales - readAccess: Admin or SalesTeam - } - ] - } - ``` - - - - - ```java - @Include( - name = "SalesNamespace", - description = "Namespaces for Sales Schema Tables", - friendlyName = "Sales" - ) - @ReadPermission(expression = "Admin or SalesTeam") - package example; - - import com.paiondata.elide.annotation.Include; - import com.paiondata.elide.annotation.ReadPermission; - ``` - - - - -#### Inheritance - -Tables can extend another existing Table. The following actions can be performed: - -* New columns can be added. -* Existing columns can be modified. -* [Table properties](#table-properties) can be modified. - -The Table properties listed below can be inherited without re-declaration. Any [Table property](#table-properties) not -listed below, has to be re-declared. - -* `dbConnectionName` -* `schema` -* `table` -* `sql` - -Unlike [Table properties](#table-properties), [Column properties](#column-properties) are not inherited. When overriding -a Column in an extended Table, the column properties have to be redefined. - -##### Hjson inheritance v.s. Java inheritance - -Hjson inheritance and Java inheritance differ in one key way. Hjson inheritance allows the type of a measure or -dimension to be changed in the subclassed model. Changing the type of an inherited measure or dimension in Java might -generate a compilation error. - -##### Example Extend Configuration - -The sample below uses the [Example Configuration](#example-configuration) as its parent model. Let's assume we are a -club that exposes the Player Stats from the intra-squad practice games and the tournament games to coaches using the -PlayerStats model. We want to expose the data from the same persistent store to the general public with the following -differences: - -- Exclude the intra-squad games from `highScore` calculation. -- Modify the Grain of `game_on` column from `DAY` to `YEAR`. -- Accessible by Admins and Guest users. - -To avoid the compilation error highlighted [above](#hjson-inheritance-vs-Java-inheritance), we will have to write the -new JVM class with all the columns and properties instead of inheriting unchanged ones from the Parent model. With the -Hjson `extend`, it will be a few lines of simple changes to inherit from the Parent model without duplication as -highlighted in the example below. - - - - - ```hjson - { - tables: [{ - name: TournamentPlayerStats - extend: PlayerStats - readAccess : 'admin.user OR guest user' - measures : [ - { - name : highScore - type : INTEGER - definition: MAX(CASE WHEN {{gameType}} = 'tournament' THEN {{highScore}}) ELSE NULL END) - } - ], - dimensions : [ - { - name : gameOn - type : TIME - definition : '{{$game_on}}' - # Change Type from MONTH, DAY & SECOND to YEAR & MONTH - grains: - [ - { - type: YEAR - sql: PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy'), 'yyyy') - }, - { - type: MONTH - sql: PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy-MM'), 'yyyy-MM') - } - ] - } - ] - }] - } - ``` - - - - - ```java - @Include - @VersionQuery(sql = "SELECT COUNT(*) from playerStats") - @ReadPermission(expression = "admin.user OR guest user") - @FromTable(name = "playerStats", dbConnectionName = "Presto Data Source") - public class TournamentPlayerStats { - - public static final String DATE_FORMAT = "PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy-MM-dd'), 'yyyy-MM-dd')"; - public static final String YEAR_MONTH_FORMAT = "PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy-MM'), 'yyyy-MM')"; - public static final String YEAR_FORMAT = "PARSEDATETIME(FORMATDATETIME({{$$column.expr}}, 'yyyy'), 'yyyy')"; - - @Id - private String id; - - // Change formula to filter on Tournament Games - @MetricFormula("MAX(CASE WHEN {{gameType}} = 'tournament' THEN {{highScore}}) ELSE NULL END)") - @ColumnMeta(friendlyName = "High Score") - private long highScore; - - @ColumnMeta(size = CardinalitySize.LARGE) - private String name; - - @Join("{{$country_id}} = {{playerCountry.$id}}", type = JoinType.LEFT) - private Country playerCountry; - - @DimensionFormula("{{playerCountry.isoCode}}") - @ColumnMeta(friendlyName = "Country Code") - private String countryCode; - - @DimensionFormula("{{$game_type}}") - @ColumnMeta(friendlyName = "Game Type") - private String gameType; - - @Temporal(grains = { @TimeGrainDefinition(grain = TimeGrain.MONTH, expression = YEAR_MONTH_FORMAT) }, timeZone = "UTC") - @DimensionFormula("{{$updated_on}}") - private Time updatedOn; - - @Temporal(grains = { @TimeGrainDefinition(grain = TimeGrain.DAY, expression = DATE_FORMAT) }, timeZone = "UTC") - @DimensionFormula("{{$created_on}}") - private Time createdOn; - - // Change types of gameOn from Day & Month to Day, Month & Year - @Temporal(grains = { - @TimeGrainDefinition(grain = TimeGrain.DAY, expression = DATE_FORMAT), - @TimeGrainDefinition(grain = TimeGrain.MONTH, expression = YEAR_MONTH_FORMAT) - @TimeGrainDefinition(grain = TimeGrain.YEAR, expression = YEAR_FORMAT) - }, timeZone = "UTC") - @DimensionFormula("{{$game_on}}") - private Time gameOn; - } - ``` - - - - -We can use Java's inheritance, if the goal does not involve changing the type of columns. Hjson `extend` will still require a few lines of simple changes. - - - - - ```hjson - { - tables: [{ - name: TournamentPlayerStats - extend: PlayerStats - readAccess : 'admin.user OR guest user' - measures : [ - { - name : highScore - type : INTEGER - definition: MAX(CASE WHEN {{gameType}} = 'tournament' THEN {{highScore}}) ELSE NULL END) - } - ] - }] - } - ``` - - - - - ```java - @Include - @ReadPermission(expression = "admin.user OR guest user") - public class TournamentPlayerStats extends PlayerStats { - - // Change formula to filter on Tournament Games - @MetricFormula("MAX(CASE WHEN {{gameType}} = 'tournament' THEN {{highScore}}) ELSE NULL END)") - private long highScore; - } - ``` - - - - -### Security Configuration - -The semantics of security are described [here](#security). - -HJSON has limited support for security definitions. Currently, only role based access controls -([user checks](security#user-chekcs)) can be defined in HJSON. For more elaborate rules, the Elide security checks must -be written in code. - -A list of available user roles can be defined in HJSON in the `security.hjson` file: - -``` -{ - roles : [ - admin.user - guest user - member - user - ] -} -``` - -Each role defined generates an Elide [user check](security#user-chekcs) that extends -**RoleMemberCheck** defined in -[Role](https://github.com/paion-data/elide/blob/master/elide-core/src/main/java/com/paiondata/elide/core/security/checks/prefab/Role.java). - -These roles can then be referenced in security rules applied to entire tables or individual columns in their respective -Hjson configuration: - -`readAccess = 'member OR guest user'` - -The `readAccess` table and column attribute can also reference Elide checks that are compiled with our application to -implement row level security or other more complex security rules. - -### Variable Substitution - -To avoid repeated configuration blocks, all Hjson files (table, security, and data source) support variable -substitution. Variables are defined in the variables.hjson file: - -``` -{ - foo: [1, 2, 3] - bar: blah - hour: hour_replace - measure_type: MAX - name: PlayerStats - table: player_stats -} -``` - -The file format is a simple mapping from the variable name to a JSON structure. At server startup, Elide will replace -any variable name surrounded by `<%` and `%>` tags with the corresponding JSON structure. - -### Caching - -The Aggregation data store supports a configurable caching strategy to cache query results. More details can be found in -the [performance section](performance#aggregationdatastore-cache). - -#### Bypassing Cache - -Elide JAX-RS endpoints (elide-standalone) and Spring controllers (Spring) support a Bypass Cache header ('bypasscache') -that can be set to `true` for caching to be disabled on a per query basis. If no bypasscache header is specified by the -client or a value other than `true` is used, caching is enabled by default. - -Security --------- - -Elide analytic models differ from CRUD models in some important ways. In a client query on a CRUD model backed by JPA, -all model fields are hydrated (in some cases with lazy proxies) regardless of what fields the client requests. -In an analytic query, only the model fields requested are hydrated. Checks which can execute in memory on the Elide -server ([Operation](security#operation-checks) & [Filter Expression](security#filter-expression-checks) checks) may -examine fields that are not hydrated and result in errors for analytic queries. To avoid this scenario, the Aggregation -Store implements its own permission executor with different restrictions and semantics. - -The aggregation store enforces the following model permission restrictions: - -- [Operation checks](security#operation-checks) are forbidden. -- [Filter Expression checks](security#filter-expression-checks) may only decorate the model but not its fields. -- [User checks](security#user-chekcs) are allowed anywhere. - -Unlike CRUD models, model 'read' permissions are not interpreted as field permission defaults. Model and field -permissions are interpreted independently. - -Elide performs the following authorization steps when reading records: - -1. Determine if the database query can be avoided (by only evaluating checks on the user principal). -2. Filter records in the database (by evaluating only filter expression checks). -3. Filter records in memory (by evaluating all checks on each record returned from the database). -4. Verify the client has permission to filter on the fields in the client's filter expression (by evaluating field level - permissions). -5. Prune fields from the response that the client cannot see (by evaluating field level permissions). - -The aggregation store will prune rows returned in the response (steps 1-3) by evaluating the following expression: - -``` -(entityRule AND (field1Rule OR field2Rule ... OR fieldNRule) -``` - -Step 4 and 5 simply evaluates the user checks on each individual field. - -Experimental Features ---------------------- - -### Configuration Validation - -All Hjson configuration files are validated by a JSON schema. The schemas for each file type can be found here: - -1. [Table Config](https://github.com/paion-data/elide/blob/master/elide-model-config/src/main/resources/elideTableSchema.json) -1. [Data Source Config](https://github.com/paion-data/elide/blob/master/elide-model-config/src/main/resources/elideDBConfigSchema.json) -1. [Security Config](https://github.com/paion-data/elide/blob/master/elide-model-config/src/main/resources/elideSecuritySchema.json) -1. [Variable File](https://github.com/paion-data/elide/blob/master/elide-model-config/src/main/resources/elideVariableSchema.json) - -Hjson configuration files can be validated against schemas using a command-line utility following these steps: - -1. Build your Elide project to generate a Fat JAR. Make sure to include a Fat JAR build configuration in your POM file. - - `mvn clean install` - -2. Using the generated JAR for validation: - - `java -cp elide-*-example.jar com.paiondata.elide.modelconfig.validator.DynamicConfigValidator --help` - `java -cp elide-*-example.jar com.paiondata.elide.modelconfig.validator.DynamicConfigValidator --configDir ` - -3. The config directory needs to adhere to this [file layout](#file-layout). - -### Query Optimization - -Some queries run faster if aggregation is performed prior to joins (for dense joins). Others my run faster if -aggregation is performed after joins (for sparse joins). By default, Elide generates queries that first aggregatoin and -then join. Elide includes an experimental optimizer that will rewrite the queries to aggregate first and then join. -This can be enabled at the table level by providing the hint, 'AggregateBeforeJoin' in the table configuration. - -### Filter Templates - -A filter template is a RSQL filter expression that must match (in whole or in part) the client's query (or the client -query will be rejected). Filter templates can be added to either table or column definitions. At the table level, the -filter template must match every query against the table. At the column level, the template is only required to match -if the client query explicitly requests the particular column. - -#### Variable extraction - -A filter template can optionally contain a template variable on the right hand side of any predicate. These variables -are assigned to the values provided in the client query filter and added to the table arguments (for table filter -templates) or the column arguments (for column filter templates). For example, the following filterTemplate would add -the variables 'start' and 'end' to the table arguments: - -``` -{ - tables: - [ - { - name: orderDetails - filterTemplate : deliveryTime>={{start}};deliveryTime<{{end}} - - ... -``` - -#### Matching - -A filter templates matches a client query if one of the two conditions holds: - -- The filter template exactly matches the client filter. -- The filter template exactly matches part of the client filter that is conjoined with logical 'and' to the remainder of - the client filter. - -For example, the client RSQL filter `lowScore>100;(highScore>=100;highScore<999)` matches the template -`highScore>={{low}};highScore<{{high}}`. diff --git a/docs/docs/crud/elide/audit.md b/docs/docs/crud/elide/audit.md deleted file mode 100644 index fe3f4e58..00000000 --- a/docs/docs/crud/elide/audit.md +++ /dev/null @@ -1,429 +0,0 @@ ---- -sidebar_position: 9 -title: Logging & Audit -description: Configuring logging & audit ---- - -Logging -------- - -Elide emits a number of useful log messages that can aid in debugging. This section will cover common configurations to -capture Elide's most useful messages. It will also cover common logging tasks outside Elide including HTTP -request/response logging, request tracing, and database query logging. All examples use Spring Boot configured with -logback. However, most of the concepts apply regardless of the logging framework used. - -### Elide JPQL/HQL Logging - -When using the JPA or Hibernate datastores, Elide generates -[HQL/JPQL](https://docs.oracle.com/html/E13946_04/ejb3_langref.html) queries that are sent to the -[ORM](https://en.wikipedia.org/wiki/Object-relational_mapping) layer. These queries are similar to SQL but they use the -model names instead of physical table names. - -To enable logging to see these queries, set the following property (based on the data store) to DEBUG: - -```xml - - -``` - -```xml - - -``` - -```xml - - -``` - -This will enable logs similar to: - -```text -Query Hash: 1839872383 HQL Query: SELECT example_models_ArtifactGroup FROM example.models.ArtifactGroup AS example_models_ArtifactGroup -``` - -### Query Latency Logging - -To get information about how long Elide JPQL or analytic queries are taking, we can enable timings: - -```xml - - -``` - -This will enable logs similar to: - -```text -Query Hash: 1839872383 Time spent: 14 -``` - -Not the query hash matches the JPQL log statement. The time spent is given in milliseconds. - -### Elide Error Response Logging - -To get extra information why a particular error was returned to a client, enable the following properties to DEBUG: - -```xml - - - -``` - -This is particularly helpful to understand what permissions in a complex permission rule have passed, failed, or were -not evaluated. For example, the following indicates that _'User is Admin'_ permission rule failed: - -```text -ForbiddenAccessException: Message=CreatePermission: CREATE PERMISSION WAS INVOKED ON PersistentResource{type=post, id=2} -WITH CHANGES ChangeSpec { resource=PersistentResource{type=post, id=2}, field=abusiveContent, original=false, -modified=true} FOR EXPRESSION [FIELD((User is Admin FAILED))] Mode=Optional[ALL_CHECKS] Expression=[Optional -[CREATE PERMISSION WAS INVOKED ON PersistentResource{type=post, id=2} WITH CHANGES ChangeSpec { -resource=PersistentResource{type=post, id=2}, field=abusiveContent, original=false, modified=true} FOR EXPRESSION -[FIELD((User is Admin FAILED))]]] -``` - -### Elide Error Response Entity Bodies - -It is also possible to return these verbose messages as an entity body in HTTP requests that failed due to -Authorization: - -```HTTP -HTTP/1.1 403 Forbidden -Date: Sat, 14 Dec 2019 03:33:08 GMT -Content-Type: application/vnd.api+json -Content-Length: 291 -Server: Jetty(9.4.24.v20191120) - -{ - "errors": [ - "CreatePermission: CREATE PERMISSION WAS INVOKED ON PersistentResource{type=post, id=2} WITH CHANGES ChangeSpec - { resource=PersistentResource{type=post, id=2}, field=abusiveContent, original=false, modified=true} FOR - EXPRESSION [FIELD((User is Admin FAILED))]" - ] -} -``` - -By default these descriptions are disabled. They can be turned on in Elide Settings - -#### Elide Standalone - -If using [Elide standalone][elide-standalone], override the following function in `ElideStandaloneSettings` and enable -verbose errors: - -```java -@Override -public boolean verboseErrors() { - return true; -} -``` - -#### Elide Spring Boot - -If using [Elide spring boot][elide-spring], set the following setting in application.yml: - -```yaml -elide: - verbose-errors: true -``` - -### Hibernate SQL Logging - -We can configure Hibernate to display the SQL commands it runs including the parameters it binds to prepared statements: - -```xml - - - -``` - -This will produce logs like: - -```text -select products0_.group_name as group_na4_1_0_, products0_.name as name1_1_0_, products0_.name as name1_1_1_, -products0_.commonName as commonNa2_1_1_, products0_.description as descript3_1_1_, products0_.group_name as -group_na4_1_1_ from ArtifactProduct products0_ where products0_.group_name=? -binding parameter [1] as [VARCHAR] - [com.paiondata.elide] -``` - -Be sure to configure Hibernate to show SQL in the JDBC configuration as well: - -#### Spring Boot Application YAML - -```yaml -spring: - jpa: - show-sql: true -``` - -#### Elide Standalone Settings - -```java -@Override -public Properties getDatabaseProperties() { - Properties options = new Properties(); - ... - - options.put("hibernate.show_sql", "true"); - return options; -} -``` - -### HTTP Request & Response Logging - -Sometimes it is useful to log the actual HTTP request and response bodies (be careful in production if the entity bodies -contain sensitive data). This example requires spring boot and logback-access-spring-boot-starter: - -```xml - - net.rakugakibox.spring.boot - logback-access-spring-boot-starter - ${logback-acccess-version} - -``` - -The actual logging of the requests and responses is performed by Logback's -[TeeFilter](http://logback.qos.ch/recipes/captureHttp.html). To add the servlet filter, we must provide the -`FilterRegistrationBean` as follows: - -```java -@Configuration -public class FilterConfiguration { - - @Bean - public FilterRegistrationBean requestResponseFilter() { - final FilterRegistrationBean filterRegBean = new FilterRegistrationBean<>(); - TeeFilter filter = new TeeFilter(); - filterRegBean.setFilter(filter); - filterRegBean.addUrlPatterns("/*"); - filterRegBean.setName("Elide Request Response Filter"); - filterRegBean.setAsyncSupported(Boolean.TRUE); - return filterRegBean; - } -} -``` - -Finally, configure logback access by creating a `logback-access-spring.xml` file in our classpath. This one writes logs -to a rotating file (the location is defined in the application yaml `logging.path`): - -```xml - - - - - - - ${logDir}/access.log - - ${logDir}/archived/access_%d{yyyy-MM-dd}.log - 30 - 100MB - - - %t{yyyy-MM-dd:HH:mm:ss Z} %remoteIP %user %requestURL %statusCode %bytesSent %elapsedTime %header{X-B3-TraceId} %requestContent %responseContent - - - - - -``` - -The pattern extracts the following fields from the HTTP request & response: - -| Field Name | Explanation | -|-----------------------------|-------------------------------------------| -| `%t{yyyy-MM-dd:HH:mm:ss Z}` | The date and time of the log | -| remoteIP | The remote IP address | -| requestURL | The request URL | -| statusCode | The HTTP status code of the response | -| bytesSent | Response content length | -| elapsedTime | Time in milliseconds to serve the request | -| `%header{X-B3-TraceId}` | Tracing Header used to track requests | -| requestContent | The request entity body | -| responseContent | The response entity body | - -The 'X-B3-TraceId' header can be used to match request tracing in the server logs. An example access log would look -like: - -```console -2019-12-14:15:48:53 -0600 0:0:0:0:0:0:0:1 - GET /api/v1/group HTTP/1.1 200 496 385 0000000000000005 {"data":[ -{"type":"group","id":"com.example.repository","attributes":{"commonName":"Example Repository","description":"The code -for this project"},"relationships":{"products":{"data":[]}}},{"type":"group","id":"com.paiondata.elide","attributes": -{"commonName":"Elide","description":"The magical library powering this project"},"relationships":{"products":{"data":[ -{"type":"product","id":"elide-core"},{"type":"product","id":"elide-standalone"},{"type":"product", -"id":"elide-datastore-hibernate5"}]}}}]} -``` - -### Request Tracing & Server Logs - -This example uses [Spring Cloud Sleuth](https://cloud.spring.io/spring-cloud-sleuth/reference/html/) without -[Zipkin](https://zipkin.io/) integration: - -```xml - - org.springframework.cloud - spring-cloud-starter-sleuth - ${spring-cloud-sleuth-version} - -``` - -Cloud Sleuth will use [logback MDC logging](http://logback.qos.ch/manual/mdc.html) to pass (if provided in headers) or -set a number of unique identifiers that can be added to log statements to trace requests. These headers -('X-B3-TraceId' and 'X-B3-SpanId') can also be logged in the access log to get the complete picture of a request. - -The following logback-spring.xml file can be added to your classpath. It does the following: - -1. Logs to the console and a rotating file. -2. Turns on Elide, JPQL, and Hibernate logging. -3. Logs the time, thread identifier, request trace identifier (X-B3-TraceId), log level, log class, and finally the log - message. - -```xml - - - - - - - - - %d{dd-MM-yyyy HH:mm:ss.SSS} %magenta([%thread]) [${springAppName}, %X{X-B3-TraceId:-}] %highlight(%-5level) %logger{36}.%M - %msg%n - - - - - ${logDir}/server.log - - %d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] [${springAppName}, %X{X-B3-TraceId:-}] %-5level %logger{36}.%M - %msg%n - - - ${logDir}/archived/server_%d{yyyy-MM-dd}.log - 30 - 100MB - - - - - - - - - - - - - - - - - - - -``` - -Result log files will look like: - -```console -14-12-2019 15:48:53.329 [qtp1863374262-22] [Elide, d426047505ceef4e] DEBUG c.y.e.d.j.p.EntityManagerWrapper.logQuery - -HQL Query: SELECT example_models_ArtifactGroup FROM example.models.ArtifactGroup AS example_models_ArtifactGroup -``` - -### Analytic Query Logging - -Analytic queries are logged by the `AggregationDataStore` directly. To log the generated SQL and other information, -enable the following property to DEBUG: - -```xml - - -``` - -Result log files will look like: - -```text -30-10-2020 16:23:12.301 [task-1] [Elide, 1f7de407f8554500] DEBUG c.y.e.d.a.core.Slf4jQueryLogger.log - QUERY ACCEPTED: -{"id":"5c9a1f64-09fa-451c-87f7-c0bcb2b76135","user":"Unknown","apiVersion":"","path":"/downloads","headers":{}} - -30-10-2020 16:23:12.327 [task-1] [Elide, 1f7de407f8554500] DEBUG c.y.e.d.a.core.Slf4jQueryLogger.log - QUERY RUNNING: -{"id":"5c9a1f64-09fa-451c-87f7-c0bcb2b76135","queries":["SELECT SUM(dynamicconfig_models_Downloads.downloads) AS -downloads,dynamicconfig_models_Downloads_artifactProduct.name AS product,dynamicconfig_models_Downloads_artifactGroup. -name AS groupy,dynamicconfig_models_Downloads.date AS date FROM downloads AS dynamicconfig_models_Downloads LEFT JOIN -ArtifactProduct AS dynamicconfig_models_Downloads_artifactProduct ON dynamicconfig_models_Downloads.product_id = -dynamicconfig_models_Downloads_artifactProduct.name LEFT JOIN ArtifactGroup AS -dynamicconfig_models_Downloads_artifactGroup ON dynamicconfig_models_Downloads.group_id = -dynamicconfig_models_Downloads_artifactGroup.name GROUP BY dynamicconfig_models_Downloads_artifactProduct.name, -dynamicconfig_models_Downloads_artifactGroup.name, dynamicconfig_models_Downloads.date LIMIT 500 OFFSET 0"], -"isCached":false} - -30-10-2020 16:23:12.338 [task-1] [Elide, 1f7de407f8554500] DEBUG c.y.e.d.a.core.Slf4jQueryLogger.log - QUERY COMPLETE: -{"id":"5c9a1f64-09fa-451c-87f7-c0bcb2b76135","status":200,"error":null} -``` - -Audit ------ - -Elide provides an Audit mechanism that assigns semantic meaning to CRUD operations for the purposes of logging and -audit. For example, we may want to log when users change their password or when an account is locked. Both actions are -mutations on a user entity that update different fields. Audit can assign these actions to parameterized, human readable -logging statements that can be logged to a file, written to a database, etc. - -### Core Concepts - -A model's **lineage** is the path taken through the entity relationship graph to reach it. A model and every prior model -in its lineage are fully accessible to parameterize audit logging in Elide. - -### Annotations - -Elide audits operations on classes and class fields marked with the `Audit` annotation. - -The `Audit` annotation takes several arguments: - -1. The CRUD action performed (CREATE, DELETE, or UPDATE). -2. An operation code which uniquely identifies the semantic meaning of the action. -3. The statement to be logged. This is a template string that allows '{}' variable substitution. -4. An ordered list of [Unified Expression Language](https://uel.java.net/) expressions that are used to substitute '{}' - in the log statement. Elide binds the model that is being audited and every model in its lineage to variables that - are accessible to the UEL expressions. The variable names map to model's type (typically the class name). - -### Example - -Let's say we have a simple _user_ entity with a _password_ field. We want to audit whenever the password is changed. The -user is accessed via the URL path '/company/53/user/21'. We could annotate this action as follows: - -```java -@Entity -@Include -public class User { - - @Audit(action = Audit.Action.UPDATE, - operation = 572, - logStatement = "User {0} from company {1} changed password.", - logExpressions = {"${user.userid}", "${company.name}"}) - private String password; - private String userid; -} -``` - -Elide binds the `User` object to the variable name _user_ and the `Company` object to the variable name _company_. The -`Company` object is bound because it belongs to the `User` object's lineage. - -### Customizing Logging - -Customizing audit functionality in Elide requires two steps: - -1. Define audit annotations on JPA entity classes and fields. -2. Provide a Logger implementation to customize the handling of audit triggers. The default logger simply logs to - [slf4j](http://www.slf4j.org/). - -### Logger Implementation - -A customized logger extends the following abstract class: - -```java -public abstract class AuditLogger { - - public void log(LogMessage message); - public abstract void commit() throws IOException; -} -``` - -[elide-spring]: https://github.com/paion-data/elide/tree/master/elide-spring/elide-spring-boot-autoconfigure -[elide-standalone]: https://github.com/paion-data/elide/tree/master/elide-standalone diff --git a/docs/docs/crud/elide/clientapis/_category_.json b/docs/docs/crud/elide/clientapis/_category_.json deleted file mode 100644 index 959d3332..00000000 --- a/docs/docs/crud/elide/clientapis/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Client APIs", - "position": 8, - "link": { - "type": "generated-index", - "description": "JSON API and GraphQL Client APIs" - } -} diff --git a/docs/docs/crud/elide/clientapis/asyncapi.mdx b/docs/docs/crud/elide/clientapis/asyncapi.mdx deleted file mode 100644 index 6eb75527..00000000 --- a/docs/docs/crud/elide/clientapis/asyncapi.mdx +++ /dev/null @@ -1,800 +0,0 @@ ---- -sidebar_position: 6 -title: Async API -description: Async support for both JSON API and GraphQL ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Overview --------- - -Elide APIs are designed for synchronous request and response behavior. The time allowed to service a synchronous -response can be limited by proxy servers and gateways. Analytic queries can often take longer than these limits and -result in a server timeout. Elide's asynchronous API decouples the submission of a request and the delivery of the -response into separate client calls. Some of the features available are: - -- Queries are run in a background thread that posts the results into a persistence store. -- Results can be retrieved as an embedded response or URL for downloading later. -- Supported downloading formats - CSV and JSON. -- Queries can be configured to execute synchronously before switching to asynchronous mode if not finished by a client - provided threshold. -- Queries that are interrupted due to an application crash/reboot are automatically flagged as TIMEDOUT. -- Persisted queries and results are deleted after a configurable threshold. - -Design ------- - -### API - -The Async API supports two different query abstractions built using standard Elide models (AsyncQuery and TableExport): - -- Any read-only API request in JSON-API or GraphQL can be submitted by creating an AsyncQuery object. After creation, - the client can poll the AsyncQuery object asynchronously for status updates. Once complete, the AsyncQuery object - stores the query result as an embedded attribute. -- For large response bodies, Elide supports a streaming abstraction called TableExport. It works similarly to the - AsyncQuery model but with three important differences: - - 1. TableExport can only be leveraged when: - - - The query is a simple read. - - The model being queried has no relationships. - - Only one model is being queried in the request. - - 2. Upon successful completion, the TableExport model includes a separate URL attribute that references the query - results for streaming downloads. - 3. The results of the GraphQL or JSON-API query are converted into a simple, flat JSON or CSV format. - -Example API requests and responses can be found [here](asyncapi#running). - -### Threading - -Each Elide instance runs a scheduler that is responsible for executing these requests in background threads. New async -requests are initially marked in the QUEUED state. The requests are picked for execution as the threads become -available. Upon completion, the background thread posts the query status and results to a persistent store. The size of -the thread pool can be configured as mentioned [here](asyncapi#additional-configuration). - -### Async After - -The Async requests can be configured to execute synchronously before switching to asynchronous mode. The requests not -finished synchronously by the client provided threshold are handed off to a separate thread for posting the results once -complete. The default value for `async-after` is 10 seconds. Setting `async-after` to 0 will execute the request in -asynchronous mode upon submission. - -### Background Cleaner - -Each Elide instance will also run a scheduler for maintenance and cleanup. - -- It cleans up requests and results stored in the persistent store. -- It scans and flags the queries that failed due to an application crash/reboot automatically as TIMEDOUT. -- It is responsible for the graceful cancellation of async requests. - - - The user can update the status of the async requests to CANCELLED. The cleaner polls the AsyncQuery and TableExport - models to find any new requests that were CANCELLED and tries to terminate the transaction associated with that - execution and change the status to CANCEL_COMPLETE. - - Long-running transactions that exceed the run time threshold are terminated. Any background thread executing the - original request is interrupted. - -- The retention, polling interval, and max run time thresholds can be configured during application startup. -- This scheduler can be disabled by setting `cleanup.enabled` to false as mentioned - [here](asyncapi#enable-the-async-api). - -### TableExport Results Download - -Elide has built-in support for streaming the results of a TableExport request through the export endpoint. Upon -successful completion, the TableExport model includes a separate URL attribute where results can be downloaded from. - -Enabling the end-point, timeouts, path, download attachment extensions, etc. can be configured during application -startup as mentioned [here](asyncapi#additional-configuration). - -### Supported Query Types - -Below are the supported values for query type in asynchronous calls: - -* GRAPHQL_V1_0 -* JSONAPI_V1_0 - -### Supported Result Types - -Elide can transform the results into a pre-selected format while persisting them via the -[ResultStorageEngine](asyncapi#overriding-the-resultstorageengine). Below are the supported formats for Table Export -results: - -- JSON -- CSV - -### Query Status - -Below are the different states of an asynchronous request: - -| Status | Description | -| --------------- | ------------------------------------------------------------------------------------- | -| QUEUED | Request is submitted and waiting to be picked up for execution. | -| PROCESSING | Request has been picked up for execution. | -| COMPLETE | Request has completed. | -| CANCELLED | The client has requested to cancel a running request. | -| TIMEDOUT | Request did not finish within the configured maximum run time. | -| FAILURE | Request not completed due to one or more failures encountered by the scheduler. | -| CANCEL_COMPLETE | Request has been canceled by the background cleaner. | - -Malformed or invalid queries provided in the Async request will finish with COMPLETE status and the actual error message -will be available in the `result` property of AsyncQuery and TableExport models. - -Security --------- - -The Async API models (AsyncQuery and TableExport) have a simple permission model: Only the principal who submitted a -query and principals which belong to an administrative role are allowed to retrieve its status or results. Principals -can be assigned roles when constructing the Elide [user](security#user) object. - -Enable the Async API --------------------- - -By default the async API is disabled. The elide models (AsyncQuery and TableExport) needed to support the Async API are -JPA [models][demo-schema] that are mapped to a specific database schema. This schema must be created in our target -database. Feel free to modify the query/result column sizes if needed. - -| Name | Description | Default | -| ----------------------------------|----------------------------------------------------------------------------------------------------------------------------| -----------| -| `elide.async.enabled` | Enable the Async API feature. | `false` | -| `elide.async.cleanup.enabled` | Enable cleaning up of Async API requests history, update the status of interrupted/timedout requests, and cancel requests. | `false` | - - - - - Configure in `application.yaml`. - - If we rely on Spring to autodiscover the entities which are placed in the same package/sub-package as the application - class with `@SpringBootApplication` annotation, we will have to add the `@EntityScan` annotation to that application - class for those entities to be discovered after async is enabled. - - ```yaml - elide: - async: - enabled: true - cleanup: - enabled: true - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneAsyncSettings getAsyncProperties() { - ElideStandaloneAsyncSettings asyncProperties = new ElideStandaloneAsyncSettings() { - @Override - public boolean enabled() { - return true; - } - - @Override - public boolean enableCleanup() { - return true; - } - } - return asyncProperties; - } - } - ``` - - - - -### Additional Configuration - -These additional configuration settings control timeouts, cleanup, export end-point, resultStorageEngine and the sizes of thread pools. - -| Name | Description | Default Value -|---------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------- -| `elide.async.enabled` | Whether or not the async feature is enabled. | `false` -| `elide.async.thread-pool-size` | Default thread pool size. | `5` -| `elide.async.max-async-after` | Default maximum permissible time to wait synchronously for the query to complete before switching to asynchronous mode. | `10s` -| `elide.async.cleanup.enabled` | Whether or not the cleanup is enabled. | `false` -| `elide.async.cleanup.query-max-run-time` | Maximum query run time. | `3600s` -| `elide.async.cleanup.query-retention-duration` | Retention period of async query and results before being cleaned up. | `7d` -| `elide.async.cleanup.query-cancellation-interval` | Polling interval to identify async queries that should be canceled. | `300s` -| `elide.async.export.enabled` | Whether or not the controller is enabled. | `false` -| `elide.async.export.path` | The URL path prefix for the controller. | `/export` -| `elide.async.export.append-file-extension` | Enable Adding Extension to table export attachments. | `false` -| `elide.async.export.storage-destination` | Storage engine destination. | `/tmp` -| `elide.async.export.format.csv.write-header` | Generates the header in a CSV formatted export. | `true` - -These additional configuration settings are only applicable for Elide’s Standalone module. When using Spring, please -configure the TaskExecutor used by Spring MVC for executing and managing the asynchronous requests. - -| Name | Description | Default Value -|-------------------------------|-------------------------------------------------------------------------------|------------------------------------------------- -| `exportAsyncResponseTimeout` | Default timeout for TableExport's result download end-point. | `30s` | -| `exportAsyncResponseExecutor` | Executor for executing TableExport's result download request asynchronously. | A java.util.concurrent.ExecutorService instance | - - - - - Configure in `application.yaml`. - - ```yaml - elide: - async: - thread-pool-size: 10 - max-async-after: 30s - cleanup: - enabled: true - query-max-run-time: 120s - query-retention-duration: 10d - query-cancellation-check-interval: 600s - export: - enabled: true - path: /export - storage-destination: /tmp - format: - csv: - write-header: true - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneAsyncSettings getAsyncProperties() { - ElideStandaloneAsyncSettings asyncProperties = new ElideStandaloneAsyncSettings() { - @Override - public Integer getThreadSize() { - return 10; - } - - @Override - public Duration getQueryMaxRunTime() { - return Duration.ofSeconds(120L); - } - - @Override - public Duration getQueryRetentionDuration() { - return Duration.ofDays(10L); - } - - @Override - public Duration getQueryCancellationCheckInterval() { - return Duration.ofSeconds(600L); - } - - @Override - public Duration getMaxAsyncAfter() { - return Duration.ofSeconds(30L); - } - - @Override - public String getExportApiPathSpec() { - return "/export/*"; - } - - @Override - boolean enableExport() { - return false; - } - - @Override - public boolean appendFileExtension() { - return true; - } - - @Override - public boolean csvWriteHeader() { - return false; - } - - @Override - public String getStorageDestination() { - return "/tmp"; - } - - @Override - public Duration getExportAsyncResponseTimeout() { - return Duration.ofSeconds(30L); - } - - @Override - public ExecutorService getExportAsyncResponseExecutor() { - return enableExport() ? Executors.newFixedThreadPool(getThreadSize() == null ? 6 : getThreadSize()) : null; - } - } - return asyncProperties; - } - } - ``` - - - - -### Running - -After configuring and starting our service, the following commands illustrate how to make asynchronous requests. Don't -forget to change `localhost:8080` accordingly. The example below makes use of the models and sample data that the -liquibase migrations added through our example is [available here][elide-demo]. - -#### Submitting query - - - - ```curl - curl -X POST http://localhost:8080/api/v1/asyncQuery/ \ - -H"Content-Type: application/vnd.api+json" -H"Accept: application/vnd.api+json" \ - -d'{ - "data": { - "type": "asyncQuery", - "id": "ba31ca4e-ed8f-4be0-a0f3-12088fa9263d", - "attributes": { - "query": "/group?sort=commonName&fields%5Bgroup%5D=commonName,description", - "queryType": "JSONAPI_V1_0", - "status": "QUEUED" - } - } - }' - ``` - - - ```curl - curl -g -X POST -H"Content-Type: application/json" \ - -H"Accept: application/json" "http://localhost:8080/graphql/api/v1" \ - -d'{ - "query" : "mutation { asyncQuery(op: UPSERT, data: {id: \"bb31ca4e-ed8f-4be0-a0f3-12088fb9263e\", query: \"{\\\"query\\\":\\\"{ group { edges { node { name } } } }\\\",\\\"variables\\\":null}\", queryType: GRAPHQL_V1_0, status: QUEUED}) { edges { node { id query queryType status result {completedOn responseBody contentLength httpStatus recordCount} } } } }" - }' - ``` - - - ```curl - curl -X POST http://localhost:8080/api/v1/tableExport/ \ - -H"Content-Type: application/vnd.api+json" -H"Accept: application/vnd.api+json" \ - -d'{ - "data": { - "type": "tableExport", - "id": "ba31ca4e-ed8f-4be0-a0f3-12088fa9263f", - "attributes": { - "query": "/group?sort=commonName&fields%5Bgroup%5D=commonName,description", - "queryType": "JSONAPI_V1_0", - "status": "QUEUED", - "resultType": "CSV" - } - } - }' - ``` - - - ```curl - curl -g -X POST -H"Content-Type: application/json" \ - -H"Accept: application/json" "http://localhost:8080/graphql/api/v1" \ - -d'{ - "query" : "mutation { tableExport(op: UPSERT, data: {id: \"bb31ca4e-ed8f-4be0-a0f3-12088fb9263g\", query: \"{\\\"query\\\":\\\"{ group { edges { node { name } } } }\\\",\\\"variables\\\":null}\", queryType: GRAPHQL_V1_0, resultType: CSV, status: QUEUED}) { edges { node { id query queryType resultType status result {completedOn url message httpStatus recordCount} } } } }" - }' - ``` - - - -Here are the respective responses: - - - - ```json - { - "data":{ - "type":"asyncQuery", - "id":"ba31ca4e-ed8f-4be0-a0f3-12088fa9263d", - "attributes":{ - "asyncAfterSeconds":10, - "principalName":null, - "createdOn":"2020-04-08T23:29Z", - "query":"/group?sort=commonName&fields%5Bgroup%5D=commonName,description", - "queryType":"JSONAPI_V1_0", - "status":"COMPLETE", - "updatedOn":"2020-04-08T23:29Z", - "result":{ - "recordCount":2, - "httpStatus":200, - "completedOn":"2020-04-08T23:29Z", - "contentLength":282, - "responseBody":"{\"data\":[{\"type\":\"group\",\"id\":\"com.paiondata.elide\",\"attributes\":{\"commonName\":\"Elide\",\"description\":\"The magical library powering this project\"}},{\"type\":\"group\",\"id\":\"com.example.repository\",\"attributes\":{\"commonName\":\"Example Repository\",\"description\":\"The code for this project\"}}]}" - } - } - } - } - ``` - - - ```json - { - "data":{ - "asyncQuery":{ - "edges":[ - { - "node":{ - "id":"bb31ca4e-ed8f-4be0-a0f3-12088fb9263e", - "query":"{\"query\":\"{ group { edges { node { name } } } }\",\"variables\":null}", - "queryType":"GRAPHQL_V1_0", - "status":"COMPLETE", - "result":{ - "completedOn":"2020-04-08T21:25Z", - "responseBody":"{\"data\":{\"group\":{\"edges\":[{\"node\":{\"name\":\"com.example.repository\"}},{\"node\":{\"name\":\"com.paiondata.elide\"}},{\"node\":{\"name\":\"elide-demo\"}}]}}}", - "contentLength":109, - "httpStatus":200, - "recordCount":2 - } - } - } - ] - } - } - } - ``` - - - ```json - { - "data":{ - "type":"tableExport", - "id":"ba31ca4e-ed8f-4be0-a0f3-12088fa9263f", - "attributes":{ - "asyncAfterSeconds":10, - "principalName":null, - "createdOn":"2020-04-08T23:29Z", - "query":"/group?sort=commonName&fields%5Bgroup%5D=commonName,description", - "queryType":"JSONAPI_V1_0", - "resultType":"CSV", - "status":"COMPLETE", - "updatedOn":"2020-04-08T23:29Z", - "result":{ - "recordCount":2, - "httpStatus":200, - "completedOn":"2020-04-08T23:29Z", - "url":"http://localhost:8080/export/ba31ca4e-ed8f-4be0-a0f3-12088fa9263f", - "message":null - } - } - } - } - ``` - - - ```json - { - "data":{ - "asyncQuery":{ - "edges":[ - { - "node":{ - "id":"bb31ca4e-ed8f-4be0-a0f3-12088fb9263g", - "query":"{\"query\":\"{ group { edges { node { name } } } }\",\"variables\":null}", - "queryType":"GRAPHQL_V1_0", - "resultType":"CSV", - "status":"COMPLETE", - "result":{ - "completedOn":"2020-04-08T21:25Z", - "url":"http://localhost:8080/export/bb31ca4e-ed8f-4be0-a0f3-12088fb9263g", - "message":null, - "httpStatus":200, - "recordCount":2 - } - } - } - ] - } - } - } - ``` - - - -### Retrieving status and result - -Long-running queries in the QUEUED or PROCESSING state may not return with the `result` property populated in the -responses above. The client can poll the AsyncQuery and TableExport objects asynchronously for status updates. - - - - ```curl - curl -X GET http://localhost:8080/api/v1/asyncQuery/ba31ca4e-ed8f-4be0-a0f3-12088fa9263d \ - -H"Content-Type: application/vnd.api+json" -H"Accept: application/vnd.api+json" - ``` - - - ```curl - curl -g -X POST -H"Content-Type: application/json" -H"Accept: application/json" \ - "http://localhost:8080/graphql/api/v1" \ - -d'{ - "query" : "{ asyncQuery (ids: \"bb31ca4e-ed8f-4be0-a0f3-12088fb9263e\") { edges { node { id query queryType status result {completedOn responseBody contentLength httpStatus recordCount}} } } }" - }' - ``` - - - ```curl - curl -X GET http://localhost:8080/api/v1/tableExport/ba31ca4e-ed8f-4be0-a0f3-12088fa9263f \ - -H"Content-Type: application/vnd.api+json" -H"Accept: application/vnd.api+json" - ``` - - - ```curl - curl -g -X POST -H"Content-Type: application/json" -H"Accept: application/json" \ - "http://localhost:8080/graphql/api/v1" \ - -d'{ - "query" : "{ tableExport (ids: \"bb31ca4e-ed8f-4be0-a0f3-12088fb9263g\") { edges { node { id query queryType resultType status result {completedOn url message httpStatus recordCount}} } } }" - }' - ``` - - - -Here are the respective responses: - - - - ```json - { - "data":{ - "type":"asyncQuery", - "id":"ba31ca4e-ed8f-4be0-a0f3-12088fa9263d", - "attributes":{ - "asyncAfterSeconds":10, - "principalName":null, - "createdOn":"2020-04-08T21:25Z", - "query":"/group?sort=commonName&fields%5Bgroup%5D=commonName,description", - "queryType":"JSONAPI_V1_0", - "status":"COMPLETE", - "updatedOn":"2020-04-08T21:25Z", - "result":{ - "recordCount":2, - "httpStatus":200, - "completedOn":"2020-04-08T23:29Z", - "contentLength":282, - "responseBody":"{\"data\":[{\"type\":\"group\",\"id\":\"com.paiondata.elide\",\"attributes\":{\"commonName\":\"Elide\",\"description\":\"The magical library powering this project\"}},{\"type\":\"group\",\"id\":\"com.example.repository\",\"attributes\":{\"commonName\":\"Example Repository\",\"description\":\"The code for this project\"}}]}" - } - } - } - } - ``` - - - ```json - { - "data":{ - "asyncQuery":{ - "edges":[ - { - "node":{ - "id":"bb31ca4e-ed8f-4be0-a0f3-12088fb9263e", - "query":"{\"query\":\"{ group { edges { node { name } } } }\",\"variables\":null}", - "queryType":"GRAPHQL_V1_0", - "status":"COMPLETE", - "result":{ - "completedOn":"2020-04-08T21:25Z", - "responseBody":"{\"data\":{\"group\":{\"edges\":[{\"node\":{\"name\":\"com.example.repository\"}},{\"node\":{\"name\":\"com.paiondata.elide\"}},{\"node\":{\"name\":\"elide-demo\"}}]}}}", - "contentLength":109, - "httpStatus":200, - "recordCount":2 - } - } - } - ] - } - } - } - ``` - - - ```json - { - "data":{ - "type":"tableExport", - "id":"ba31ca4e-ed8f-4be0-a0f3-12088fa9263f", - "attributes":{ - "asyncAfterSeconds":10, - "principalName":null, - "createdOn":"2020-04-08T21:25Z", - "query":"/group?sort=commonName&fields%5Bgroup%5D=commonName,description", - "queryType":"JSONAPI_V1_0", - "resultType":"CSV", - "status":"COMPLETE", - "updatedOn":"2020-04-08T21:25Z", - "result":{ - "recordCount":2, - "httpStatus":200, - "completedOn":"2020-04-08T23:29Z", - "url":"http://localhost:8080/export/ba31ca4e-ed8f-4be0-a0f3-12088fa9263f", - "message":null - } - } - } - } - ``` - - - ```json - { - "data":{ - "asyncQuery":{ - "edges":[ - { - "node":{ - "id":"bb31ca4e-ed8f-4be0-a0f3-12088fb9263e", - "query":"{\"query\":\"{ group { edges { node { name } } } }\",\"variables\":null}", - "queryType":"GRAPHQL_V1_0", - "resultType":"CSV", - "status":"COMPLETE", - "result":{ - "completedOn":"2020-04-08T21:25Z", - "url":"http://localhost:8080/export/bb31ca4e-ed8f-4be0-a0f3-12088fb9263g", - "message":null, - "httpStatus":200, - "recordCount":2 - } - } - } - ] - } - } - } - ``` - - - -### Downloading the TableExport results - -The TableExport request will return a URL to download the results as shown in the example response below. - - - - ```json - { - "result":{ - "completedOn":"2020-04-08T21:25Z", - "url":"http://localhost:8080/export/bb31ca4e-ed8f-4be0-a0f3-12088fb9263g", - "message":null, - "httpStatus":200, - "recordCount":2 - } - } - ``` - - - ```json - [ - { - "commonName":"Elide", - "description":"The magical library powering this project" - }, - { - "commonName":"Example Repository", - "description":"The code for this project" - } - ] - ``` - - - ```csv - "commonName", "description" - "Elide", "The magical library powering this project" - "Example Repository", "The code for this project" - ``` - - - -Overriding the AsyncApiDao --------------------------- - -The Async API interacts with the persistence layer through an abstraction - the AsyncApiDao, for status updates, query -cleanup, etc. This can be customized by providing our own implementation. Elide provides a default implementation of -[AsyncApiDao][default-async-api-dao]. - - - - - Create a `@Configuration` class that defines our custom implementation as a `@Bean`. - - ```java - @Configuration - public class ElideConfiguration { - /** - * Configure the AsyncApiDao used by async requests. - * @return an AsyncApiDao object. - */ - @Bean - public AsyncApiDao asyncApiDao() { - return new CustomAsyncApiDao(); - } - } - ``` - - - - - Override ElideStandaloneSettings. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneAsyncSettings getAsyncProperties() { - ElideStandaloneAsyncSettings asyncProperties = new ElideStandaloneAsyncSettings() { - /** - * Implementation of AsyncApiDao to use. - * @return AsyncApiDao type object. - */ - @Override - public AsyncApiDao getAsyncApiDao() { - return new CustomAsyncApiDao(); - } - } - return asyncProperties; - } - } - ``` - - - - -Overriding the ResultStorageEngine ----------------------------------- - -Table exports leverage a reactive abstraction (ResultStorageEngine) for streaming results to and from a persistence -backend. This can be customized by providing our own implementation. Elide provides default implementation of -[ResultStorageEngine][file-result-storage-engine]. - - - - - Create a `@Configuration` class that defines our custom implementation as a `@Bean`. - - ```java - @Configuration - public class ElideConfiguration { - /** - * Configure the ResultStorageEngine used by TableExport requests. - * @return a ResultStorageEngine object. - */ - @Bean - public ResultStorageEngine resultStorageEngine() { - return new CustomResultStorageEngine(); - } - } - ``` - - - - - Override ElideStandaloneSettings. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneAsyncSettings getAsyncProperties() { - ElideStandaloneAsyncSettings asyncProperties = new ElideStandaloneAsyncSettings() { - /** - * Implementation of ResultStorageEngine to use. - * @return ResultStorageEngine type object. - */ - @Override - public ResultStorageEngine getResultStorageEngine() { - return new CustomResultStorageEngine(); - } - } - return asyncProperties; - } - } - ``` - - - - -Internals ---------- - -![Error loading elide-async-design.png](img/elide-async-design.png) - -[demo-schema]: asyncapi#enable-the-async-api -[file-result-storage-engine]: https://github.com/paion-data/elide/blob/master/elide-async/src/main/java/com/paiondata/elide/async/service/storageengine/FileResultStorageEngine.java -[default-async-api-dao]: https://github.com/paion-data/elide/blob/master/elide-async/src/main/java/com/paiondata/elide/async/service/dao/DefaultAsyncApiDao.java -[elide-demo]: asyncapi#running diff --git a/docs/docs/crud/elide/clientapis/graphql-federation.md b/docs/docs/crud/elide/clientapis/graphql-federation.md deleted file mode 100644 index 2cdc0a7a..00000000 --- a/docs/docs/crud/elide/clientapis/graphql-federation.md +++ /dev/null @@ -1,287 +0,0 @@ ---- -sidebar_position: 5 -title: GraphQL Federation -description: Beta support for GraphQL Federation ---- - -What is GraphQL Federation --------------------------- - -GraphQL Federation lets us declaratively combine multiple GraphQL APIs into a single, federated graph. This federated -graph enables clients to interact with multiple APIs through a single request. - -A client makes a request to the single entry point of the federated graph called the router. The router intelligently -orchestrates and distributes the request across your APIs and returns a unified response. For a client, the request and -response cycle of querying the router looks the same as querying any GraphQL server. - -![Error loading graphql-federation.png](img/graphql-federation.png) - -### Benefits of Federation - -#### Microservices Architecture - -GraphQL Federation lets API teams operate in a -[microservices architecture](https://www.atlassian.com/microservices/microservices-architecture/microservices-vs-monolith) -while exposing a unified GraphQL API to clients. Understanding these concepts can help us get the most out of -federation. - -#### Preserve Client Simplicity and Performance - -A client may need to make multiple requests when interacting with multiple non-federated GraphQL APIs. This can happen -when an organization adopting GraphQL has multiple teams developing APIs independently. Each team sets up a GraphQL API -that provides the data used by that team. For example, a travel app may have separate GraphQL APIs for users, flights, -and hotels: - -![Error loading multiple-graphql-apis.png](img/multiple-graphql-apis.png) - -With a single federated graph, we preserve a powerful advantage of GraphQL over traditional REST APIs: the ability to -fetch all the data we need in a single request. - -![Error loading multiple-graphql-apis-federated.png](img/multiple-graphql-apis-federated.png) - -The router intelligently calls all the APIs it needs to complete requests rather than simply forwarding them. For -performance and security reasons, clients should only query the router, and only the router should query the constituent -APIs. No client-side configuration is required. - -#### Design Schemas at Scale - -Some alternative approaches to combining GraphQL APIs impose limits on our schema, like adding namespaces or -representing relationships with IDs instead of types. With these approaches, our individual GraphQL API schemas may look -unchanged—but the resulting federated schema that clients interact with is more complex. Subsequently, it requires us to -make frontend as well as backend changes. - -With GraphQL Federation, clients can interact with the federated schema as if it were a monolith. Consumers of our API -shouldn't know or care that it's implemented as microservices. - -GraphQL Federation in Elide ---------------------------- - -Elide supports GraphQL Federation. This feature needs to be enabled to be used. - -### Enabling GraphQL Federation - -```yaml -elide: - graphql: - federation: - enabled: true -``` - -### Schema Introspection Queries - -When GraphQL Federation is enabled, Elide will respond to enhanced introspection queries with `Query._service` with the -GraphQL schemas generated by Elide. - -```graphql -query { - _service { - sdl - } -} -``` - -Elide does not have any built in measures to control which clients can execute the introspection queries. These queries -should typically be restricted only to the federated graph routers. - -### Implementing Federated Graphs - -Elide generates its GraphQL schema programatically and cannot be used to define federated entities. - -This will need to be done in another subgraph implementation using a different subgraph library, for instance Spring -GraphQL. - -#### Extending an Elide entity - -The Elide entity can be extended with additional entities from the subgraph using the `@extends` directive. The -configurations are done in the subgraph and not in Elide. - -In the following example the `Group` entity from Elide is being extended to provide the additional `GroupReview` entity -provided by the subgraph. - -```graphql -type Group @key(fields: "name") @extends { - name: DeferredID! @external - groupReviews: [GroupReview!]! -} -``` - -Note that Elide uses a custom scalar `DeferredID` instead of `ID` which will need to be registered with the subgraph. - -The following query is an example that starts from the `Group` entity on Elide and references the `GroupReview` entity -on the subgraph. - -```graphql -query { - group { - edges { - node { - commonName - groupReviews { - stars - text - } - } - } - } -} -``` - -After the router queries the `Group` entity on Elide, it will also make another query to this subgraph to get the -`GroupReview` entity. - -The router will use the following query on the subgraph to add the additional fields of `GroupReview` to the `Group` -entity. - -```graphql -query { - _entities(representations: [{__typename: "Group", name: "com.paiondata.elide"}]) { - ... on Group { - stars - text - } - } -} -``` - -For Spring GraphQL the representations can be configured as shown below. - -The mapping for the representations to the `Group` is configured in the entity data fetcher for instance in -`com.example.reviews.config.GraphQLConfiguration`. - -```java -DataFetcher entityDataFetcher = env -> { - List> representations = env.getArgument(_Entity.argumentName); - return representations.stream().map(representation -> { - // Assume only a single id key and no composite keys - String idKey = representation.keySet().stream().filter(key -> !"__typename".equals(key)).findFirst() - .orElse(null); - String id = (String) representation.get(idKey); - if (GROUP_TYPE.equals(representation.get("__typename"))) { - return new Group(id); - } - return null; - }).toList(); -}; -``` - -#### Including Elide entities - -The subgraph entity can include Elide entities as Elide supports the `@key` directive. The following is the schema that -Elide generates for the `Group` entity. - -```graphql -type Group @key(fields : "name") { - commonName: String - description: String - name: DeferredID - products(after: String, data: [ProductInput], filter: String, first: String, ids: [String], op: ElideRelationshipOp = FETCH, sort: String): ProductConnection -} -``` - -The following is the schema of `GroupReview` on the subgraph. - -```graphql -type GroupReview { - id: ID!, - text: String - stars: Int! - group: Group -} -``` - -The following query is an example that starts from the `GroupReview` entity on subgraph and references the `Group` -entity on Elide. - -```graphql -query { - groupReviews { - id - stars - text - group { - name - commonName - } - } -} -``` - -After calling to retrieve the `GroupReview` entites on the subgraph, the router calls Elide with the following query. - -```graphql -query { - _entities(representations: [{__typename: "Group", name: "com.paiondata.elide"}]) { - ... on Group { - name - commonName - } - } -} -``` - -Elide will determine the projection in `GraphQLEntityProjectionMaker`. - -The `EntitiesDataFetcher` will fetch a list of `NodeContainer`. - -```java -public class EntitiesDataFetcher implements DataFetcher> { - - ... -} -``` - -The `EntityTypeResolver` will map the `NodeContainer` to the appropriate `GraphQLObjectType`. - -#### Defining the DeferredID scalar - -Elide uses a custom scalar `DeferredID` instead of `ID`. - -This needs to be registered with the subgraph implementation. - -The following is the schema definition. - -```graphql -scalar DeferredID -``` - -For Spring GraphQL this can be configured as shown below. - -The following is the Java code for the `GraphQLScalarType`. - -```java -public class GraphQLScalars { - public static GraphQLScalarType DEFERRED_ID = GraphQLScalarType.newScalar().name("DeferredID") - .description("The DeferredID scalar type represents a unique identifier.") - .coercing(new Coercing() { - @Override - public String serialize(Object o) { - return o.toString(); - } - - @Override - public String parseValue(Object o) { - return o.toString(); - } - - @Override - public String parseLiteral(Object o) { - if (o instanceof StringValue stringValue) { - return stringValue.getValue(); - } - if (o instanceof IntValue intValue) { - return intValue.getValue().toString(); - } - return o.toString(); - } - }).build(); -} -``` - -The following is the Java code for registering the scalar in `GraphQLConfiguration`. - -```java -@Bean -public GraphQlSourceBuilderCustomizer graphqlSourceBuilderCustomizer() { - return schemaResourceBuilder -> schemaResourceBuilder - .configureRuntimeWiring(runtimeWiring -> runtimeWiring.scalar(GraphQLScalars.DEFERRED_ID)); -} diff --git a/docs/docs/crud/elide/clientapis/graphql-subscriptions.mdx b/docs/docs/crud/elide/clientapis/graphql-subscriptions.mdx deleted file mode 100644 index 068c5dbf..00000000 --- a/docs/docs/crud/elide/clientapis/graphql-subscriptions.mdx +++ /dev/null @@ -1,524 +0,0 @@ ---- -sidebar_position: 4 -title: GraphQL Subscriptions ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -5-Minute Overview ------------------ - -We'll be leveraging the [elide-spring-boot-example][example-project] project to illustrate how to use subscriptions. - -### Decorating an Elide Model - -A subscription can be made from any Elide model simply by annotating it with `Subscription`: - -```java -@Data // Lombok -@Subscription -@Include(name = "group") -public class ArtifactGroup { - @Id - private String name; - - @SubscriptionField - private String commonName; - - @SubscriptionField - private String description; - - @SubscriptionField - @OneToMany(mappedBy = "group") - private List products; -} -``` - -The subscription annotation takes zero or more *operations* which correspond to different topics. - -```java -@Include -@Subscription(operation = { CREATE, UPDATE, DELETE }); -class Book { - ... -} -``` - -Whenever a model is manipulated (created, deleted, or updated), elide will post a JSON serialized model to a JMS topic -for that operation. Only the fields decorated with `@SubscriptionField` will be serialized (and hence available to be -consumed in the subscription). It is also possible to define [custom operations](#custom-subscriptions) that are -triggered by your service business logic. - -### Running Queries - -Elide subscriptions are implemented using websockets. Websockets require a protocol to send and receive messages. -Elide supports the [graphql-ws](https://github.com/enisdenjo/graphql-ws) protocol. This protocol works both with the -[Apollo GraphQL](https://www.apollographql.com/docs/react/data/subscriptions/) client as well as [Graphiql] -(https://github.com/graphql/graphiql). If we run the [example project][example-project], it comes bundled with Graphiql. - -Elide's subscription API is similar to its API for queries and mutations, but there are some notable differences: - -1. Each Elide model annotated with `Subscription` is a root field in the GraphQL schema. -2. Each root model supports a topic (ADDED, DELETED, UPDATED) variable and an optional [filter variable](#filtering). -3. Responses are not wrapped in 'edges' and 'node' because there is no pagination. -4. The API is read only. - -#### Simple Query - -Query for newly added 'groups' returning their name and description: - -``` -subscription { group(topic : ADDED) { name description } } -``` - -The response will look like: - -```json -{ - "data": { - "group": { - "name": "new group", - "description": "foo" - } - } -} - -``` - -If there are errors, they will get reported in an errors field: - -```json -{ - "data": { - "group": { - "name": "new group", - "commonName": "", - "nope": null - } - }, - "errors": [ - { - "message": "Exception while fetching data (/group/nope) : ReadPermission Denied", - "locations": [ - { - "line": 2, - "column": 53 - } - ], - "path": [ - "group", - "nope" - ], - "extensions": { - "classification": "DataFetchingException" - } - } - ] -} -``` - -#### Filtering - -All elide subscriptions support RSQL filtering that is identical to -[filtering for queries and mutations](graphql#filtering). The following query filters artifact group creation events by -the name 'com.paiondata.elide': - -``` -subscription { group(topic : ADDED, filter: "name='com.paiondata.elide'") { name description } } -``` - -Security --------- - -Elide honors [ReadPermission annotations](security#read) for all subscription fields in the model. Subscriptions are -automatically filtered by any [FilterExpressionChecks](security#filter-expression-checks). Client requests to -unauthorized fields will result in errors returned in the subscription response payload. - -See the section on [Authentication](#authentication) for details on how to build an Elide user principal. - -Configuration -------------- - -## JMS Message Broker - -Elide leverages JMS to post and consume subscription messages. The example project runs an embedded (in-memory) broker. -We will want to replace this with a dedicated broker in production. - - - - - Configure in `application.yaml`. - - ```yaml - spring: - activemq: - broker-url: 'vm://embedded?broker.persistent=false,useShutdownHook=false' - in-memory: true - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneSubscriptionSettings getSubscriptionProperties() { - return new ElideStandaloneSubscriptionSettings() { - public ConnectionFactory getConnectionFactory() { - - // Create, configure, and return a JMS connection factory.... - - } - }; - } - } - ``` - - - - -### Global Settings - -Elide subscriptions support the following, global configuration settings: - -1. **enabled** - Turn on/off the subscription websocket. -2. **path** - The HTTP root path of the subscription websocket. -3. **idleTimeout** - How long in milliseconds the websocket can remain idle before the server closes it. -4. **maxMessageSize** - Maximum size in bytes of any message sent to the websocket (or it will be closed in error). -5. **maxSubscriptions** - The maximum number of concurrent subscriptions per websocket. -6. **connectionTimeout** - The time in milliseconds for the client to initiate a connection handshake before the server - closes the socket. - - - - - Configure in `application.yaml`. - - ```yaml - elide: - graphql: - subscription: - enabled: true - path: /subscription - idle-timeout: 30000ms - max-message-size: 10000 - max-subscriptions: 30 - connection-timeout: 5000ms - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneSubscriptionSettings getSubscriptionProperties() { - return new ElideStandaloneSubscriptionSettings() { - - @Override - public boolean enabled() { - return true; - } - - @Override - public String getPath() { - return "/subscription"; - } - - @Override - public Duration getConnectionTimeout() { - return Duration.ofMillis(5000L); - } - - @Override - public Integer getMaxSubscriptions() { - return 30; - } - - @Override - public Integer getMaxMessageSize() { - return 10000; - } - - @Override - public Duration getIdleTimeout() { - return Duration.ofMillis(300000L); - } - }; - } - } - ``` - - - - -### Authentication - -There is no well-defined standard for how user credentials are passed via websockets. Instead, Elide allows developers -to provide a function that maps a -[JSR-356 Session](https://jakarta.ee/specifications/websocket/2.0/apidocs/jakarta/websocket/session) to and Elide -[User object](security#user). The session contains the HTTP request headers, path parameter, query parameters, and -websocket parameters that can be leveraged to construct a user principal. - - - - - Create a `@Configuration` class that defines our custom implementation as a `@Bean`. - - ```java - @Configuration - public class ElideConfiguration { - @Bean - public SubscriptionWebSocket.UserFactory userFactory() { - return new CustomUserFactory(); - } - } - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneSubscriptionSettings getSubscriptionProperties() { - return new ElideStandaloneSubscriptionSettings() { - - @Override - public SubscriptionWebSocket.UserFactory getUserFactory() { - return new CustomUserFactory(); - } - }; - } - } - ``` - - - - -### JMS Message Settings - -We can override some of the default settings for messages published to JMS topics by overriding the following -bean: - - - - - Create a `@Configuration` class that defines our custom implementation as a `@Bean`. - - ```java - @Configuration - public class ElideConfiguration { - @Bean - public SubscriptionScanner subscriptionScanner(Elide elide, ConnectionFactory connectionFactory) { - SubscriptionScanner scanner = SubscriptionScanner.builder() - - // Things you may want to override... - .deliveryDelay(Message.DEFAULT_DELIVERY_DELAY) - .messagePriority(Message.DEFAULT_PRIORITY) - .timeToLive(Message.DEFAULT_TIME_TO_LIVE) - .deliveryMode(Message.DEFAULT_DELIVERY_MODE) - - // Things you probably don't care about... - .scanner(elide.getScanner()) - .dictionary(elide.getElideSettings().getDictionary()) - .connectionFactory(connectionFactory) - .mapper(elide.getMapper().getObjectMapper()) - .build(); - - scanner.bindLifecycleHooks(); - - return scanner; - } - } - ``` - - - - -Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneSubscriptionSettings getSubscriptionProperties() { - return new ElideStandaloneSubscriptionSettings() { - - @Override - public SubscriptionScanner subscriptionScanner(Elide elide, ConnectionFactory connectionFactory) { - SubscriptionScanner scanner = SubscriptionScanner.builder() - - // Things you may want to override... - .deliveryDelay(Message.DEFAULT_DELIVERY_DELAY) - .messagePriority(Message.DEFAULT_PRIORITY) - .timeToLive(Message.DEFAULT_TIME_TO_LIVE) - .deliveryMode(Message.DEFAULT_DELIVERY_MODE) - - // Things you probably don't care about... - .scanner(elide.getScanner()) - .dictionary(elide.getElideSettings().getDictionary()) - .connectionFactory(connectionFactory) - .mapper(elide.getMapper().getObjectMapper()) - .build(); - - scanner.bindLifecycleHooks(); - - return scanner; - } - }; - } - } - ``` - - - - -Custom Subscriptions --------------------- - -While Elide makes it easy to subscribe to model manipulations (create, update, and delete), it is also possible to add a -subscription topic for another event tied to our business logic. A custom subscription is simply an Elide model -annotated with the `@Subscription` annotation that explicitly sets the list of operations to empty: - -```java -@Include - -//This is a custom subscription -@Subscription(operations = {}) -@Data -@AllArgsConstructor -@NoArgsConstructor -public class Chat { - - - @Id - long id; - - @SubscriptionField - String message; -} -``` - -To publish to our subscription, we can create a lifecycle hook on another model that posts `Chat` messages. - - - - - Create a `@Configuration` class that defines our custom implementation as a `@Bean`. - - ```java - @Data // Lombok - @Include - @LifeCycleHookBinding( - hook = ChatBotCreateHook.class, - operation = LifeCycleHookBinding.Operation.CREATE, - phase = LifeCycleHookBinding.TransactionPhase.POSTCOMMIT - ) - public class ChatBot { - - @Id - long id; - - String name; - } - ``` - - - - - ```java - @Data // Lombok - public class ChatBotCreateHook implements LifeCycleHook { - - @Inject - ConnectionFactory connectionFactory; - - @Override - public void execute( - LifeCycleHookBinding.Operation operation, - LifeCycleHookBinding.TransactionPhase phase, - ChatBot bot, - RequestScope requestScope, - Optional changes) { - - NotifyTopicLifeCycleHook publisher = new NotifyTopicLifeCycleHook<>( - connectionFactory, - new ObjectMapper(), - JMSContext::createProducer - ); - - publisher.publish(new Chat(1, "Hello!"), CHAT); - publisher.publish(new Chat(2, "How is your day?"), CHAT); - publisher.publish(new Chat(3, "My name is " + bot.getName()), CHAT); - } - } - ``` - - - - -Recommendations ---------------- - -Even though the [example project][example-project] runs GraphQL queries, mutations, and subscriptions in the same -service, it is highly recommended that subscriptions run as a _separate service_. Because websockets are long-lived and -stateful, they impose different resource constraints and performance characteristics from queries and mutations. - -Running websockets as a standalone service is as simple as disabling JSON-API and GraphQL HTTP endpoints: - - - - - Configure in `application.yaml`. - - ```yaml - elide: - json-api: - enabled: false - graphql: - enabled: false - subscription: - enabled: true - ``` - - - - - Override `ElideStandaloneSettings`. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public boolean enableJsonApi() { - return false; - } - - @Override - public boolean enableGraphQL() { - return false; - } - - @Override - public ElideStandaloneSubscriptionSettings getSubscriptionProperties() { - return new ElideStandaloneSubscriptionSettings() { - - @Override - public boolean enabled() { - return true; - } - }; - } - } - ``` - - - - -[example-project]: https://github.com/yahoo/elide-spring-boot-example diff --git a/docs/docs/crud/elide/clientapis/graphql.mdx b/docs/docs/crud/elide/clientapis/graphql.mdx deleted file mode 100644 index 37e52a2a..00000000 --- a/docs/docs/crud/elide/clientapis/graphql.mdx +++ /dev/null @@ -1,1309 +0,0 @@ ---- -sidebar_position: 3 -title: GraphQL Queries & Mutations -description: Querying webservice using GraphQL ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -[GraphQL](https://qubitpi.github.io/graphql.github.io/) is a language specification published by Facebook for -constructing graph APIs. The specification provides great flexibility in API expression, but also little direction for -best practices for common mutation operations. For example, it is silent on how to: - -- create a new object and add it to an existing collection in the same operation. -- create a set of related, composite objects (a subgraph) and connect it to an existing, persisted graph. -- differentiate between deleting an object vs disassociating an object from a relationship (but not deleting it). -- change the composition of a relationship to something different. -- reference a newly created object inside other mutation operations. -- perform any combination of the above edits together so that they can happen atomically in a single request. - -Elide offers an opinionated GraphQL API that addresses exactly how to do these things in a uniform way across our entire -data model graph. - -JSON Envelope -------------- - -Elide accepts GraphQL queries embedded in HTTP POST requests. It follows the -[convention defined by GraphQL](https://qubitpi.github.io/graphql.github.io/learn/serving-over-http/) for serving over -HTTP. Namely, ever GraphQL query is wrapped in a JSON envelope object with one required attribute and two optional -attributes: - -1. *query* - _Required_. Contains the actual graphQL query. -2. *operationName* - Used if multiple operations are present in the same query. -3. *variables* - Contains a json object of key/value pairs where the keys map to variable names in the query and the - values map to the variable values. - -For example: - -```json -{ - "query": "mutation myMutation($bookName: String $authorName: String) {book(op: UPSERT data: {id:2,title:$bookName}) {edges {node {id title authors(op: UPSERT data: {id:2,name:$authorName}) {edges {node {id name}}}}}}}", - "variables": { - "authorName": "John Steinbeck", - "bookName": "Grapes of Wrath" - } -} -``` - -The response is also a JSON payload: - -```json -{ - "data": { ... }, - "errors": [ ... ] -} -``` - -The 'data' field contains the graphQL response object, and the 'errors' field (only present when they exist) contains -one or more errors encountered when executing the query. Note that it is possible to receive a 200 HTTP OK from Elide -but also have errors in the query. - -API Structure -------------- - -GraphQL splits its schema into two kinds of objects: - -1. **Query objects** which are used to compose queries and mutations -2. **Input Objects** which are used to supply input data to mutations - -The schema for both kinds of objects are derived from the entity relationship graph (defined by the JPA data model). -Both contain a set of attributes and relationships. Attributes are properties of the entity. Relationships are links to -other entities in the graph. - -### Input Objects - -Input objects just contain attributes and relationship with names directly matching the property names in the JPA -annotated model: - -
![GraphQL Input Object UML](img/graphql_input_object_uml.png)
- -### Query Objects - -Query Objects are more complex than Input Objects since they do more than simply describe data; they must -support filtering, sorting, and pagination. Elide's GraphQL structure for queries and mutations is depicted below: - -
![GraphQL Query Object UML](img/graphql_query_object_uml.png)
- -Every GraphQL schema must define a root document which represents the root of the graph. In Elide, entities can be -marked if they are directly navigable from the root of the graph. Elide's GraphQL root documents consist of -_relationships_ to these rootable entities. Each root relationship is named by its pluralized type name in the GraphQL -root document. - -All other non-rootable entities in our schema must be referenced through traversal of the relationships in the entity -relationship graph. - -Elide models relationships follow -[Relay's Connection pattern](https://qubitpi.github.io/graphql.github.io/learn/pagination/#connection-specification). -Relationships are a collection of graph _edges_. Each edge contains a graph _node_. The _node_ is an instance of a data -model which in turn contains its own attributes and set of relationships. - -#### Relationship Arguments - -In GraphQL, any property in the schema can take arguments. Relationships in Elide have a standard set of arguments that -either constrain the edges fetched from a relationship or supply data to a mutation: - -1. The **ids** parameter is a collection of node identifiers. It is used to select one or more nodes from a - relationship. -2. The **filter** parameter is used to build [RSQL](https://github.com/jirutka/rsql-parser) filter predicates that - select zero or more nodes from a relationship. -3. The **sort** parameter is used to order a relationship's edges by one or more node attributes. -4. The parameters **after** and **first** are used to paginate a relationship across multiple API requests. -5. The **op** argument describes the operation to perform on the relationship. When not provided, this argument - defaults to a FETCH operation which simply reads the collection of edges. -6. The **data** parameter is provided for operations that mutate the collection (UPSERT, UPDATE, and REPLACE), It - contains a list of input objects that match the data type of the relationship. Each _data_ object can be a complex - subgraph which contains other objects through nested relationships. - -Entity attributes generally do not take arguments. - -#### Relationship Operations - -Elide GraphQL relationships support six operations which can be broken into two groups: data operations and id -operations. The operations are separated into those that accept a _data_ argument and those that accept an _ids_ -argument. Operations that edit or manipulate data are restricted to GraphQL Mutation queries: - -| Operation | Data | Ids | Mutation Support | Query Support | -| --------- |------|-----|------------------|---------------| -| Upsert | ✓ | X | ✓ | X | -| Update | ✓ | X | ✓ | X | -| Fetch | X | ✓ | ✓ | ✓ | -| Replace | ✓ | X | ✓ | X | -| Remove | X | ✓ | ✓ | X | -| Delete | X | ✓ | ✓ | X | - -1. The **FETCH** operation retrieves a set of objects. When a list of ids is specified, it will only extract the set of - objects within the relationship with matching ids. If no ids are specified, then the entire collection of objects - will be returned to the caller. -2. The **DELETE** operation fully deletes an object from the system. -3. The **REMOVE** operation removes a specified set (qualified by the _ids_ argument) of objects from a relationship. - This allows the caller to remove relationships between objects without being forced to fully delete the referenced - objects. **REMOVE** is ony supported in GraphQL mutation requests. -4. The **UPSERT** operation behaves much like SQL's MERGE. Namely, if the object already exists (based on the provided - id) then it will be updated. Otherwise, it will be created. In the case of updates, attributes that are not specified - are left unmodified. If the _data_ argument contains a complex subgraph of nested objects, nested objects will also - invoke **UPSERT**. -5. The **UPDATE** operation behaves much like SQL's UPDATE. Namely, if the object already exists (based on the provided - id) then it will be updated. Attributes that are not specified are left unmodified. If the _data_ argument contains a - complex subgraph of nested objects, nested objects will also invoke **UPDATE**. -6. The **REPLACE** operation is intended to replace an entire relationship with the set of objects provided in the - _data_ argument. **REPLACE** can be thought of as an **UPSERT** followed by an implicit **REMOVE** of everything else - that was previously in the collection that the client has authorization to see & manipulate. - -#### Map Data Types - -GraphQL has no native support for a map data type. If a JPA data model includes a map, Elide translates this to a list -of key/value pairs in the GraphQL schema. - -Making Calls ------------- - -All calls must be HTTP `POST` requests made to the root endpoint. This specific endpoint will depend on where you mount -the provided servlet. For example, if the servlet is mounted at `/graphql`, all requests should be sent as: - -```text -POST https://yourdomain.com/graphql -``` - -## Example Data Model - -All subsequent query examples are based on the following data model including `Book`, `Author`, and `Publisher`: - - - - ```java - @Entity - @Table(name = "book") - @Include(rootLevel = true) - public class Book { - @Id private long id; - private String title; - private String genre; - private String language; - @ManyToMany - private Set authors; - @ManyToOne - private Publisher publisher; - } - ``` - - - ```java - @Entity - @Table(name = "author") - @Include(rootLevel = false) - public class Author { - @Id private long id; - private String name; - @ManyToMany - private Set books; - } - ``` - - - ```java - @Entity - @Table(name = "publisher") - @Include(rootLevel = false) - public class Publisher { - @Id private long id; - private String name; - @OneToMany - private Set books; - } - ``` - - - -Filtering ---------- - -Elide supports filtering relationships for any _FETCH_ operation by passing a -[RSQL](https://github.com/jirutka/rsql-parser) expression in the _filter_ parameter for the relationship. RSQL is a -query language that allows conjunction (and), disjunction (or), and parenthetic grouping of boolean expressions. It is a -superset of the [FIQL language](https://tools.ietf.org/html/draft-nottingham-atompub-fiql-00). FIQL defines all String -comparison operators to be case insensitive. Elide overrides this behavior making all operators case sensitive by -default. For case insensitive queries, Elide introduces new operators. - -RSQL predicates can filter attributes: - -- In the relationship model itself -- In another model joined to the relationship model through to-one or to-many relationships -- Inside an object or nested object hierarchy - -To join across relationships or drill into nested objects, the attribute name is prefixed by one or more relationship or -field names separated by period ('.'). For example, 'author.books.price.total' references all of the author's books with -a price having a particular total value. - -### Operators - -The following RSQL operators are supported: - -- `=in=` : Evaluates to true if the attribute exactly matches any of the values in the list. (Case Sensitive) -- `=ini=`: Evaluates to true if the attribute exactly matches any of the values in the list. (Case Insensitive) -- `=out=` : Evaluates to true if the attribute does not match any of the values in the list. (Case Sensitive) -- `=outi=` : Evaluates to true if the attribute does not match any of the values in the list. (Case Insensitive) -- `==ABC*` : Similar to SQL `like 'ABC%`. (Case Sensitive) -- `==*ABC` : Similar to SQL `like '%ABC`. (Case Sensitive) -- `==*ABC*` : Similar to SQL `like '%ABC%`. (Case Sensitive) -- `=ini=ABC*` : Similar to SQL `like 'ABC%`. (Case Insensitive) -- `=ini=*ABC` : Similar to SQL `like '%ABC`. (Case Insensitive) -- `=ini=*ABC*` : Similar to SQL `like '%ABC%`. (Case Insensitive) -- `=isnull=true` : Evaluates to true if the attribute is null -- `=isnull=false` : Evaluates to true if the attribute is not null -- `=lt=` : Evaluates to true if the attribute is less than the value. -- `=gt=` : Evaluates to true if the attribute is greater than the value. -- `=le=` : Evaluates to true if the attribute is less than or equal to the value. -- `=ge=` : Evaluates to true if the attribute is greater than or equal to the value. -- `=isempty=` : Determines if a collection is empty or not. -- `=between=` : Determines if a model attribute is `>=` and `<=` the two provided arguments. -- `=notbetween=` : Negates the between operator. -- `=hasmember=` : Determines if a collection contains a particular element. -- `=hasnomember=` : Determines if a collection does not contain a particular element. - -The operators 'hasmember' and 'hasnomember' can be applied to collections (book.awards) or across to-many relationships -(book.authors.name). - -#### FIQL Default Behaviour - -By default, the FIQL operators `=in=`, `=out=`, `==` are case sensitive. This can be reverted to case insensitive by -changing the case sensitive strategy: - -```java - @Bean - @ConditionalOnMissingBean - public Elide initializeElide(EntityDictionary dictionary, - DataStore dataStore, ElideConfigProperties settings) { - - ElideSettingsBuilder builder = new ElideSettingsBuilder(dataStore) - .withEntityDictionary(dictionary) - .withDefaultMaxPageSize(settings.getMaxPageSize()) - .withDefaultPageSize(settings.getPageSize()) - .withGraphQLDialect(new RSQLFilterDialect(dictionary), new CaseSensitivityStrategy.FIQLCompliant()) - .withAuditLogger(new Slf4jLogger()) - .withISO8601Dates("yyyy-MM-dd'T'HH:mm'Z'", TimeZone.getTimeZone("UTC")); - - return new Elide(builder.build()); - } -``` - -### Attribute arguments. - -Some data stores like the Aggregation Store support parameterized model attributes. Parameters can be included in a -filter predicate with the following syntax: - -```text -field[arg1:value1][arg2:value2] -``` - -Argument values must be URL encoded. There is no limit to the number of arguments provided in this manner. - -### Examples - -- Filter books by title equal to 'abc' _and_ genre starting with 'Science' _and_ whose total price is greater than - 100.00: `"title=='abc';genre=='Science*';price.total>100.0` -- Filter books with a publication date greater than a certain time _or_ the genre is _not_ 'Literary Fiction' - or 'Science Fiction': `publishDate>1454638927411,genre=out=('Literary Fiction','Science Fiction')` -- Filter books by the publisher name contains XYZ: `publisher.name==*XYZ*` - -Pagination ----------- - -Any relationship can be paginated by providing one or both of the following parameters: - -1. **first** - The number of items to return per page. -2. **after** - The number of items to skip. - -### Relationship Metadata - -Every relationship includes information about the collection (in addition to a list of edges) -that can be requested on demand: - -1. **endCursor** - The last record offset in the current page (exclusive). -2. **startCursor** - The first record offset in the current page (inclusive). -3. **hasNextPage** - Whether or not more pages of data exist. -4. **totalRecords** - The total number of records in this relationship across all pages. - -These properties are contained within the _pageInfo_ structure: - -```graphql -{ - pageInfo { - endCursor - startCursor - hasNextPage - totalRecords - } -} -``` - -Sorting -------- - -Any relationship can be sorted by attributes in: - -- The relationship model -- Another model joined to the relationship model through to-one relationships - -To join across relationships, the attribute name is prefixed by one or more relationship names separated by period ('.') - -It is also possible to sort in either ascending or descending order by prepending the attribute expression with a '+' or -'-' character. If no order character is provided, sort order defaults to ascending. - -A relationship can be sorted by multiple attributes by separating the attribute expressions by commas: ','. - -Model Identifiers ------------------ - -Elide supports three mechanisms by which a newly created entity is assigned an ID: - -1. The ID is assigned by the client and saved in the data store. -2. The client doesn't provide an ID and the data store generates one. -3. The client provides an ID which is replaced by one generated by the data store. When using _UPSERT_, the client must - provide an ID to identify objects which are both created and added to collections in other objects. However, insome - instances the server should have ultimate control over the ID that is assigned. - -Elide looks for the JPA `GeneratedValue` annotation to disambiguate whether or not the data store generates an ID for a -given data model. If the client also generated an ID during the object creation request, the data store ID overrides the -client value. - -### Matching newly created objects to IDs - -When using _UPSERT_, Elide returns object entity bodies (containing newly assigned IDs) in the order in which they were -created - assuming all the entities were newly created (and not mixed with entity updates in the request). The client -can use this order to map the object created to its server assigned ID. - -FETCH Examples --------------- - -### Fetch All Books - -Include the id, title, genre, & language in the result. - - - - ```graphql - { - book { - edges { - node { - id - title - genre - language - } - } - } - } - ``` - - - ```json - { - "book":{ - "edges":[ - { - "node":{ - "id":"1", - "title":"Libro Uno", - "genre":null, - "language":null - } - }, - { - "node":{ - "id":"2", - "title":"Libro Dos", - "genre":null, - "language":null - } - }, - { - "node":{ - "id":"3", - "title":"Doctor Zhivago", - "genre":null, - "language":null - } - } - ] - } - } - ``` - - - -### Fetch Single Book - -Fetches book 1. The response includes the id, title, and authors. For each author, the response includes its id & name. - - - - ```graphql - { - book(ids: ["1"]) { - edges { - node { - id - title - authors { - edges { - node { - id - name - } - } - } - } - } - } - } - ``` - - - ```json - { - "book":{ - "edges":[ - { - "node":{ - "id":"1", - "title":"Libro Uno", - "authors":{ - "edges":[ - { - "node":{ - "id":"1", - "name":"Mark Twain" - } - } - ] - } - } - } - ] - } - } - ``` - - - -### Filter All Books - -Fetches the set of books that start with 'Libro U'. - - - - ```graphql - { - book(filter: "title==\"Libro U*\"") { - edges { - node { - id - title - } - } - } - } - ``` - - - ```json - { - "book":{ - "edges":[ - { - "node":{ - "id":"1", - "title":"Libro Uno" - } - } - ] - } - } - ``` - - - -### Paginate All Books - -Fetches a single page of books (1 book per page), starting at the 2nd page. -Also requests the relationship metadata. - - - - ```graphql - { - book(first: "1", after: "1") { - edges { - node { - id - title - } - } - pageInfo { - totalRecords - startCursor - endCursor - hasNextPage - } - } - } - ``` - - - ```json - { - "book":{ - "edges":[ - { - "node":{ - "id":"2", - "title":"Libro Dos" - } - } - ], - "pageInfo":{ - "totalRecords":3, - "startCursor":"1", - "endCursor":"2", - "hasNextPage":true - } - } - } - ``` - - - -### Sort All Books - -Sorts the collection of books first by their publisher id (descending) and then by the book id (ascending). - - - - ```graphql - { - book(sort: "-publisher.id,id") { - edges { - node { - id - title - publisher { - edges { - node { - id - } - } - } - } - } - } - } - ``` - - - ```json - { - "book":{ - "edges":[ - { - "node":{ - "id":"3", - "title":"Doctor Zhivago", - "publisher":{ - "edges":[ - { - "node":{ - "id":"2" - } - } - ] - } - } - }, - { - "node":{ - "id":"1", - "title":"Libro Uno", - "publisher":{ - "edges":[ - { - "node":{ - "id":"1" - } - } - ] - } - } - }, - { - "node":{ - "id":"2", - "title":"Libro Dos", - "publisher":{ - "edges":[ - { - "node":{ - "id":"1" - } - } - ] - } - } - } - ] - } - } - ``` - - - -### Schema Introspection - -Fetches the entire list of data types in the GraphQL schema. - - - - ```graphql - { - __schema { - types { - name - } - } - } - ``` - - - ```json - { - "__schema":{ - "types":[ - { - "name":"root" - }, - { - "name":"noshare" - }, - { - "name":"__edges__noshare" - }, - { - "name":"__node__noshare" - }, - { - "name":"id" - }, - { - "name":"__pageInfoObject" - }, - { - "name":"Boolean" - }, - { - "name":"String" - }, - { - "name":"Long" - }, - { - "name":"com.paiondata.elide.graphql.RelationshipOp" - }, - { - "name":"noshareInput" - }, - { - "name":"ID" - }, - { - "name":"book" - }, - { - "name":"__edges__book" - }, - { - "name":"__node__book" - }, - { - "name":"authorInput" - }, - { - "name":"example.AddressInputInput" - }, - { - "name":"example.Author$AuthorType" - }, - { - "name":"bookInput" - }, - { - "name":"publisherInput" - }, - { - "name":"pseudonymInput" - }, - { - "name":"author" - }, - { - "name":"__edges__author" - }, - { - "name":"__node__author" - }, - { - "name":"example.Address" - }, - { - "name":"publisher" - }, - { - "name":"__edges__publisher" - }, - { - "name":"__node__publisher" - }, - { - "name":"pseudonym" - }, - { - "name":"__edges__pseudonym" - }, - { - "name":"__node__pseudonym" - }, - { - "name":"__Schema" - }, - { - "name":"__Type" - }, - { - "name":"__TypeKind" - }, - { - "name":"__Field" - }, - { - "name":"__InputValue" - }, - { - "name":"__EnumValue" - }, - { - "name":"__Directive" - }, - { - "name":"__DirectiveLocation" - } - ] - } - } - ``` - - - -UPSERT Examples ---------------- - -### Create and Add New Book to an Author - -Creates a new book and adds it to Author 1. The author's id and list of newly created books is returned in the response. -For each newly created book, only the title is returned. - - - - ```graphql - mutation { - author(ids: ["1"]) { - edges { - node { - id - books(op: UPSERT, data: {title: "Book Numero Dos"}) { - edges { - node { - title - } - } - } - } - } - } - } - ``` - - - ```json - { - "author":{ - "edges":[ - { - "node":{ - "id":"1", - "books":{ - "edges":[ - { - "node":{ - "title":"Book Numero Dos" - } - } - ] - } - } - } - ] - } - } - ``` - - - -### Update the Title of an Existing Book - -Updates the title of book 1 belonging to author 1. The author's id and list of updated books is returned in the -response. For each updated book, only the title is returned. - - - - - ```graphql - mutation { - author(ids: ["1"]) { - edges { - node { - id - books(op:UPSERT, data: {id: "1", title: "abc"}) { - edges { - node { - id - title - } - } - } - } - } - } - } - ``` - - - - - ```json - { - "author":{ - "edges":[ - { - "node":{ - "id":"1", - "books":{ - "edges":[ - { - "node":{ - "id":"1", - "title":"abc" - } - } - ] - } - } - } - ] - } - } - ``` - - - - -UPDATE Examples ---------------- - -Updates author 1's name and simultaneously updates the titles of books 2 and 3. - - - - ```graphql - mutation { - author(op: UPDATE, data: { id: "1", name: "John Snow", books: [ { id: "3", title: "updated again"}, { id: "2", title: "newish title"}]}) { - edges { - node { - id - name - books(ids: ["3"]) { - edges { - node { - title - } - } - } - } - } - } - } - ``` - - - ```json - { - "author":{ - "edges":[ - { - "node":{ - "id":"1", - "name":"John Snow", - "books":{ - "edges":[ - { - "node":{ - "title":"updated again" - } - } - ] - } - } - } - ] - } - } - ``` - - - -DELETE Examples ---------------- - -Deletes books 1 and 2. The id and title of the remaining books are returned in the response. - - - - ```graphql - mutation { - book(op:DELETE, ids: ["1", "2"]) { - edges { - node { - id - title - } - } - } - } - ``` - - - ```json - { - "book":{ - "edges":[ - - ] - } - } - ``` - - - - -REMOVE Example --------------- - -Removes books 1 and 2 from author 1. Author 1 is returned with the remaining books. - - - - ```graphql - mutation { - author(ids: ["1"]) { - edges { - node { - books(op:REMOVE, ids: ["1", "2"]) { - edges { - node { - id - title - } - } - } - } - } - } - } - ``` - - - ```json - { - "author":{ - "edges":[ - { - "node":{ - "books":{ - "edges":[ - - ] - } - } - } - ] - } - } - ``` - - - -REPLACE Example ---------------- - -Replaces the set of authors for _every_ book with the set consisting of: - -- An existing author (author 1) -- A new author - -The response includes the complete set of books (id & title) and their new authors (id & name). - - - - ```graphql - mutation { - book { - edges { - node { - id - title - authors(op: REPLACE, data: [ { name: "My New Author"}, { id: "1"}]) { - edges { - node { - id - name - } - } - } - } - } - } - } - ``` - - - ```json - { - "book":{ - "edges":[ - { - "node":{ - "id":"1", - "title":"Libro Uno", - "authors":{ - "edges":[ - { - "node":{ - "id":"3", - "name":"My New Author" - } - }, - { - "node":{ - "id":"1", - "name":"Mark Twain" - } - } - ] - } - } - }, - { - "node":{ - "id":"2", - "title":"Libro Dos", - "authors":{ - "edges":[ - { - "node":{ - "id":"4", - "name":"My New Author" - } - }, - { - "node":{ - "id":"1", - "name":"Mark Twain" - } - } - ] - } - } - }, - { - "node":{ - "id":"3", - "title":"Doctor Zhivago", - "authors":{ - "edges":[ - { - "node":{ - "id":"5", - "name":"My New Author" - } - }, - { - "node":{ - "id":"1", - "name":"Mark Twain" - } - } - ] - } - } - } - ] - } - } - ``` - - - -Type Serialization/Deserialization ----------------------------------- - -Type coercion between the API and underlying data model has common support across JSON-API and GraphQL and is covered -[here](clientapis#type-coercion). - -Custom Error Responses ----------------------- - -Configuring custom error responses is documented [here](clientapis#custom-error-responses). - -Field Description in GraphQL Schema Introspection -------------------------------------------------- - -Elide supports showing field descriptions in [schema introspection](https://graphql.org/learn/introspection/), such as -the one shown on the right side of the Documentation Explorer of [GraphiQL](https://github.com/graphql/graphiql): - -![Error loading field-description.png](./img/field-description.png) - -This will be very helpful for our GraphQL API users. We can accomplish such using -[`@GraphQLDescription` annotation] -(https://github.com/Enigmatis/graphql-java-annotations/blob/master/src/main/java/graphql/annotations/annotationTypes/GraphQLDescription.java) -in data model. Here is an example: - -```java -@Entity -@Table(name = "book") -@Include(rootLevel = true) -public class Book { - - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - public long id; - - /** - * The book title. - */ - @GraphQLDescription("The title of the book") - public String title; - - /** - * Book authors. - */ - @OneToMany - @GraphQLDescription("All co-authors of this book") - public Set authors; -} -``` - -The resource bindings for Jersey and Spring are listed below: - -:::tip[What's the point of customizer?] - -Elide has the notion of "customizer" that comes into play when configuring Elide instance programmatically. It is very -similar to the established _Builder_ pattern. The difference is that builder gives the explicit control while -customizers grants implicit control over dependencies. - -The customizers allow us to customize defaults. If we just create a builder then it is expected that we fully configure -it by ourselves, which backs off all defaults, but typically it usually results in a lot of copy and paste of the -defaults with the added changes. With customizer, we only need to write out the code for those added changes. - -::: - - - - ```java - import graphql.annotations.annotationTypes.GraphQLDescription; - - /** - * A binder factory builds a custom binder for the Jersey application. - *

- * The factory makes the component object instance that will eventually be passed to - * {@link org.glassfish.jersey.server.ResourceConfig#register(Object)}. - */ - public class BinderFactory { - - /** - * Custom GraphQLFieldDefinitionCustomizer that uses {@link GraphQLDescription} as a source of field documentation. - */ - private static class GraphQLFieldDefinitionCustomizer extends AnnotationGraphQLFieldDefinitionDescriptionCustomizer { - - private static final GraphQLFieldDefinitionCustomizer INSTANCE = new GraphQLFieldDefinitionCustomizer(); - - /** - * Custom GraphQLFieldDefinitionCustomizer that uses {@link GraphQLDescription} as a source of field documentation. - */ - private GraphQLFieldDefinitionCustomizer() { - super(GraphQLDescription.class, GraphQLDescription::value); - } - } - - /** - * Builds a hk2 Binder instance. - *

- * This binder should bind all relevant resources for runtime dependency injection. - * - * @param injector A standard HK2 service locator - * - * @return a binder instance that will be registered by putting as a parameter to - * {@link org.glassfish.jersey.server.ResourceConfig#register(Object)} - */ - @NotNull - public Binder buildBinder(final ServiceLocator injector) { - return new AbstractBinder() { - @NotNull - private ElideSettings buildElideSettings() { - final EntityDictionary entityDictionary = buildEntityDictionary(injector); - return new ElideSettingsBuilder() - .settings( - GraphQLSettingsBuilder - .withDefaults(entityDictionary) - .graphqlFieldDefinitionCustomizer(GraphQLFieldDefinitionCustomizer.INSTANCE) - ) - // more settings ... - .build(); - } - }; - } - } - ``` - - - The `GraphQLFieldDefinition` can be customized by setting a `GraphQLFieldDefinitionCustomizer` to the - `GraphQLSettingsBuilder`. - - ```java - import graphql.annotations.annotationTypes.GraphQLDescription; - - @Configuration - public class ElideConfiguration { - @Bean - GraphQLSettingsBuilderCustomizer graphqlSettingsBuilderCustomizer() { - return graphqlSettings -> graphqlSettings.graphqlFieldDefinitionCustomizer( - ((fieldDefinition, parentClass, attributeClass, attribute, fetcher, entityDictionary) -> { - GraphQLDescription description = entityDictionary.getAttributeOrRelationAnnotation(parentClass, - GraphQLDescription.class, attribute); - if (description != null) { - fieldDefinition.description(description.value()); - } - })); - } - } - ``` - - diff --git a/docs/docs/crud/elide/clientapis/img/elide-async-design.drawio b/docs/docs/crud/elide/clientapis/img/elide-async-design.drawio deleted file mode 100644 index 4e98808a..00000000 --- a/docs/docs/crud/elide/clientapis/img/elide-async-design.drawio +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/docs/crud/elide/clientapis/img/elide-async-design.png b/docs/docs/crud/elide/clientapis/img/elide-async-design.png deleted file mode 100644 index 1a36ad9b..00000000 Binary files a/docs/docs/crud/elide/clientapis/img/elide-async-design.png and /dev/null differ diff --git a/docs/docs/crud/elide/clientapis/img/field-description.png b/docs/docs/crud/elide/clientapis/img/field-description.png deleted file mode 100644 index 88228b4f..00000000 Binary files a/docs/docs/crud/elide/clientapis/img/field-description.png and /dev/null differ diff --git a/docs/docs/crud/elide/clientapis/img/graphql-federation.png b/docs/docs/crud/elide/clientapis/img/graphql-federation.png deleted file mode 100644 index c05f9b2e..00000000 Binary files a/docs/docs/crud/elide/clientapis/img/graphql-federation.png and /dev/null differ diff --git a/docs/docs/crud/elide/clientapis/img/graphql_input_object_uml.png b/docs/docs/crud/elide/clientapis/img/graphql_input_object_uml.png deleted file mode 100644 index 5b52689a..00000000 Binary files a/docs/docs/crud/elide/clientapis/img/graphql_input_object_uml.png and /dev/null differ diff --git a/docs/docs/crud/elide/clientapis/img/graphql_query_object_uml.png b/docs/docs/crud/elide/clientapis/img/graphql_query_object_uml.png deleted file mode 100644 index 2ccde9ee..00000000 Binary files a/docs/docs/crud/elide/clientapis/img/graphql_query_object_uml.png and /dev/null differ diff --git a/docs/docs/crud/elide/clientapis/img/multiple-graphql-apis-federated.png b/docs/docs/crud/elide/clientapis/img/multiple-graphql-apis-federated.png deleted file mode 100644 index f00c80fc..00000000 Binary files a/docs/docs/crud/elide/clientapis/img/multiple-graphql-apis-federated.png and /dev/null differ diff --git a/docs/docs/crud/elide/clientapis/img/multiple-graphql-apis.png b/docs/docs/crud/elide/clientapis/img/multiple-graphql-apis.png deleted file mode 100644 index b73f920c..00000000 Binary files a/docs/docs/crud/elide/clientapis/img/multiple-graphql-apis.png and /dev/null differ diff --git a/docs/docs/crud/elide/clientapis/index.mdx b/docs/docs/crud/elide/clientapis/index.mdx deleted file mode 100644 index aa02f8a3..00000000 --- a/docs/docs/crud/elide/clientapis/index.mdx +++ /dev/null @@ -1,336 +0,0 @@ ---- -sidebar_position: 1 -title: Client APIs -description: JSON API and GraphQL Client APIs ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Supported APIs --------------- - -Elide supports the two most widely adopted standards for graph APIs: - -- [JSON-API](jsonapi) -- [GraphQL](graphql) - -:::tip - -Graph APIs are an evolution of web service APIs that serve and manipulate data for mobile & web applications. They have -a number of characteristics that make them well suited to this task: - -1. Most notably, they present a **data model** as an entity relationship graph and an **accompanying schema**. - - - A well-defined model allows for a consistent view of the data and a centralized way to manipulate an instance of - the model or to cache it. - - The schema provides powerful introspection capabilities that can be used to build tools to help developers - understand and navigate the model. - -2. The API allows the client to **fetch or mutate as much or as little information in single roundtrip** between client - and server. This also shrinks payload sizes and simplifies the process of schema evolution. -3. There is a **well-defined standard** for the API that fosters a community approach to development of supporting - tools & best practices. -::: - -Common Concepts ---------------- - -All Elide APIs share a common set of concepts: - -1. The API exposes a set of related data models as an entity relationship graph. -2. All models have a unique identifier. -3. Models have attributes and relationships. - - - Relationships are links to other models. They can be traversed in the API. If the relationship represents a - collection, it can be sorted, filtered, and paginated. - - Attributes are properties of the model. They can be simple or complex (objects or collections). - -4. Filtering, sorting, and pagination share common languages and expressions. -5. Text and numeric representation of complex attributes is common. -6. API versioning works in the same manner. -7. Custom error responses have the same configuration mechanism. - -### API Versioning - -Elide allows multiple versions of the same models to coexist and for clients to request a particular instance. Elide -JAX-RS endpoints (elide-standalone) and Spring controllers (Spring) support an API version that can be set to match the -model annotation (`@ApiVersion`) version. - -If no version is specified by the client, Elide only exposes the models that lack an `@ApiVersion` annotation. - -OpenAPI endpoints (JSON-API) and GraphQL schemas are also scoped by the `ApiVersion` header. They only return the -schema corresponding to the requested API version. - -Elide includes implementations for the following API Versioning Strategies - -- Path -- Header -- Parameters -- Media Type Profile - -This can be customized by implementing and registering a `com.paiondata.elide.core.request.route.RouteResolver`. - -The default in Elide Spring Boot uses the Path strategy. The Path strategy is the only one that is supported when -integrating with Springdoc as the other strategies are difficult to document with OpenAPI. - -This can be configured using `application.yaml`. - -```yaml -elide: - api-versioning-strategy: - path: - enabled: false - header: - enabled: true - header-name: - - ApiVersion -``` - -The default in Elide Standalone now accepts all the strategies. - -This can be configured by overriding `ElideStandaloneSettings`. - -```java -public abstract class Settings implements ElideStandaloneSettings { - @Override - public RouteResolver getRouteResolver() { - new HeaderRouteResolver("ApiVersion"); - } -} -``` - -Details of how to version Elide models can be found [here](../data-model#api-versions). Details of how to configure -versioned OpenAPI documents can be found [here](../openapi#api-versions). - -### Type Coercion - -Elide attempts to deserialize and coerce fields in the client payload into the underlying type defined in the data -model. Similarly, Elide will serialize the data model fields into the text format defined by the schema of the client -payload. - -Beyond primitive, numeric, and String types, Elide can serialize and deserialize complex and user defined types. - -#### User Type Registration - -To register a new type for serialization and deserialization, define a `Serde` (short for Serializer/Deserializer): - -```java -/** - * Bidirectional conversion from one type to another. - * - * @param The serialized type - * @param The deserialized type - */ -public interface Serde { - - /** - * Deserialize an instance of type S to type T. - * @param val The thing to deserialize - * @return The deserialized value - */ - T deserialize(S val); - - /** - * Serializes an instance of type T as type S. - * @param val The thing to serialize - * @return The serialized value - */ - S serialize(T val); -} -``` - -At startup, Elide will automatically discover any `Serde` classes annotated with `ElideTypeConverter`: - -```java -@ElideTypeConverter(type = OffsetDateTime.class, name = "OffsetDateTime") -public class OffsetDateTimeSerde implements Serde { - - @Override - public OffsetDateTime deserialize(String val) { - return OffsetDateTime.parse(val, DateTimeFormatter.ISO_OFFSET_DATE_TIME); - } - - @Override - public String serialize(OffsetDateTime val) { - return val.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); - } -} -``` - -#### Date Coercion - -Elide has built-in support for either: - -- Epoch based dates (serialized as a long) -- [ISO8601](https://www.iso.org/iso-8601-date-and-time-format.html) based dates (serialized as a String - `yyyy-MM-dd'T'HH:mm'Z') - -##### Spring Boot Configuration - -[Elide Spring Boot][elide-spring] is configured by default to use ISO8601 dates. - -This can be toggled by creating a `ElideSettingsBuilderCustomizer` bean: - -```java -@Configuration -public class ElideConfiguration { - @Bean - ElideSettingsBuilderCustomizer elideSettingsBuilderCustomizer() { - return builder -> builder.serdes(serdes -> serdes.withEpochDates()); - } -} -``` - -##### Elide Standalone Configuration - -[Elide Standalone][elide-standalone] defaults to ISO8601 dates. This can be toggled by overriding the following binding - -```java -/** - * Whether Dates should be ISO8601 strings (true) or epochs (false). - * @return - */ -public boolean enableISO8601Dates() { - return true; -} -``` - -##### Elide Library Configuration - -The following date serdes can be registered: - -1. [ISO8601 Serde](https://github.com/paion-data/elide/blob/master/elide-core/src/main/java/com/paiondata/elide/core/utils/coerce/converters/ISO8601DateSerde.java) -2. [Epoch Serde](https://github.com/paion-data/elide/blob/master/elide-core/src/main/java/com/paiondata/elide/core/utils/coerce/converters/EpochToDateConverter.java) - -#### UUID Coercion - -Elide has built in support for converting between String and UUIDs. The conversion leverages `UUID.fromString`. - -#### Enum Coercion - -Elide has built in support for converting between Strings or Integers to enumeration types (by name or value -respectively). - -### Custom Error Responses - -For normal error handling, Elide throws runtime exceptions which are mapped to error responses. We can override any -error response in Elide by providing a custom `ExceptionMapper`: - -```java -/** - * Maps an exception to an {@link ElideErrorResponse}. - * - * @param exception type - * @param response body type - */ -@FunctionalInterface -public interface ExceptionMapper { - - /** - * Map the exception to an {@link ElideErrorResponse}. - * - * @param exception the exception to map. - * @param errorContext the error context - * @return the mapped ElideErrorResponse or null if you do not want to map this error - */ - @Nullable - ElideErrorResponse toErrorResponse(E exception, ErrorContext errorContext); -} -``` - -The mapper returns a `ElideErrorResponse` which allows the developer complete control over the error objects returned -in the 'errors' array for both JSON-API and GraphQL. - -```java -public class InvalidEntityBodyExceptionMapper implements ExceptionMapper { - public ElideErrorResponse toErrorResponse( - InvalidEntityBodyException exception, - ErrorContext errorContext - ) { - return ElideErrorResponse.badRequest() - .errors(errors -> errors - // Add the first error - .error(error -> error - .message(errorContext.isVerbose() ? exception.getMessage() : "Invalid entity body") - .attribute("code", "InvalidEntityBody") - .attribute("body", "")) - // Add the second error - .error(error -> error - .message("Item 1 cannot be empty") - .attribute("code", "NotEmpty") - .attribute("item", "1")) - // Add the third error - .error(error -> error - .message("Item 2 cannot be null") - .attribute("code", "NotNull") - .attribute("item", "2"))); - } -} -``` - -The `ElideErrors` will be mapped to the corresponding `JsonApiErrors` and `GraphQLErrors`. The -[`JsonApiError`](https://github.com/paion-data/elide/blob/master/elide-core/src/main/java/com/paiondata/elide/jsonapi/serialization/JsonApiErrorSerializer.java) -and -[`GraphQLError`](https://github.com/paion-data/elide/blob/master/elide-graphql/src/main/java/com/paiondata/elide/graphql/serialization/GraphQLErrorSerializer.java) -are what is serialized as a response. - -This mapping of `ElideErrors` happens in the -[`DefaultJsonApiExceptionHandler`](https://github.com/paion-data/elide/blob/master/elide-core/src/main/java/com/paiondata/elide/jsonapi/DefaultJsonApiExceptionHandler.java) -and -[`DefaultGraphQLExceptionHandler`](https://github.com/paion-data/elide/blob/master/elide-graphql/src/main/java/com/paiondata/elide/graphql/DefaultGraphQLExceptionHandler.java) -using the `JsonApiErrorMapper` and `GraphQLErrorMapper`. - -We can configure a custom `ExceptionMapper` as follows: - - - - - Create a `@Configuration` class that defines our custom implementation as a `@Bean`. In the following example the - `InvalidEntityBodyExceptionMapper` is the custom implementation. - - ```java - @Configuration - public class ElideConfiguration { - @Bean - public ExceptionMapper exceptionMapper() { - return new InvalidEntityBodyExceptionMapper(); - } - } - ``` - - - - - Override ElideStandaloneSettings. In the following example the `InvalidEntityBodyExceptionMapper` is the custom - implementation being registered. - - ```java - public abstract class Settings implements ElideStandaloneSettings { - @Override - public ExceptionMappers getExceptionMappers() { - return BasicExceptionMappers.builder().register(new InvalidEntityBodyExceptionMapper()).build(); - } - } - ``` - - - - -The following is the relationship between `ElideError` and `JsonApiError` and `GraphQLError`. - -|Elide Error |JsonApi Error |GraphQL Error | -|-----------------------|-----------------|--------------------| -|`message` |`details` |`message` | -|`attributes` |`meta` |`extensions` | -|`attributes.id` |`id` |`extensions.id` | -|`attributes.status` |`status` |`extensions.status` | -|`attributes.code` |`code` |`extensions.code` | -|`attributes.title` |`title` |`extensions.title` | -|`attributes.source` |`source` |`extensions.source` | -|`attributes.links` |`links` |`extensions.links` | -|`attributes.path` |`meta.path` |`path` | -|`attributes.locations` |`meta.locations` |`locations` | - -[elide-spring]: https://github.com/paion-data/elide/tree/master/elide-spring/elide-spring-boot-autoconfigure -[elide-standalone]: https://github.com/paion-data/elide/tree/master/elide-standalone diff --git a/docs/docs/crud/elide/clientapis/jsonapi.mdx b/docs/docs/crud/elide/clientapis/jsonapi.mdx deleted file mode 100644 index 59f2a512..00000000 --- a/docs/docs/crud/elide/clientapis/jsonapi.mdx +++ /dev/null @@ -1,1233 +0,0 @@ ---- -sidebar_position: 2 -title: JSON API -description: Querying webservice using JSON API ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -[JSON-API](https://qubitpi.github.io/json-api/) is a specification for building REST APIs for CRUD (create, read, -update, and delete) operations. Similar to GraphQL, it - -- It allows the client to control what is returned in the response payload. -- It provides a mechanism in the form of extensions (the - [_Atomic Operations Extension and JSON Patch Extension_](#bulk-writes-and-complex-mutations)) that allows multiple - mutations to the graph to occur in a single request. - -Unlike GraphQL, the JSON-API specification spells out exactly how to perform common CRUD operations including complex -graph mutations. JSON-API has no standardized schema introspection. However, Elide adds this capability to any service -by exporting an [OpenAPI](https://www.openapis.org) document (formerly known as [Swagger](https://swagger.io)). - -The [JSON-API specification](https://qubitpi.github.io/json-api/format/) is the best reference for understanding -JSON-API. The following sections describe commonly used JSON-API features as well as Elide additions for filtering, -pagination, sorting, and generation of OpenAPI documents. - -Hierarchical URLs ------------------ - -Elide generally follows the [JSON-API recommendations](https://qubitpi.github.io/json-api/recommendations/) for URL -design. - -There are a few caveats given that Elide allows developers control over how entities are exposed: - -1. Some entities may only be reached through a relationship to another entity. Not every entity is _rootable_. -2. The root path segment of URLs are by default the name of the class (lowercase). This can be overridden. -3. Elide allows relationships to be nested arbitrarily deep in URLs. -4. Elide currently requires all individual entities to be addressed by ID within a URL. For example, consider a model - with an article and a singular author which has a singular address. While unambiguous, the following is *not* - allowed: `/articles/1/author/address`. Instead, the author must be fully qualified by ID: - `/articles/1/author/34/address` - -Model Identifiers ------------------ - -Elide supports three mechanisms by which a newly created entity is assigned an ID: - -1. The ID is assigned by the client and saved in the data store. -2. The client doesn't provide an ID and the data store generates one. -3. The client provides an ID which is replaced by one generated by the data store. When using the Atomic Operations - Extension or JSON Patch Extension, the client must provide an ID or Local ID to identify objects which are both - created and added to collections in other objects. However, in some instances the server should have ultimate control - over the ID that is assigned. - -Elide looks for the JPA `GeneratedValue` annotation to disambiguate whether or not the data store generates an ID for a -given data model. If the client also generated an ID during the object creation request, the data store ID overrides the -client value. - -### Matching Newly Created Objects to IDs - -When using the Atomic Operations Extension or JSON Patch Extension, Elide returns object entity bodies (containing newly -assigned IDs) in the order in which they were created. The client can use this order to map the object created to its -server assigned ID. - -Sparse Fields -------------- - -JSON-API allows the client to limit the attributes and relationships that should be included in the response payload -for any given entity. The _fields_ query parameter specifies the type (data model) and list of fields that should be -included. - -For example, to fetch the book collection but only include the book titles: - - - - ```text - /book?fields[book]=title - ``` - - - ```json - { - "data":[ - { - "attributes":{ - "title":"The Old Man and the Sea" - }, - "id":"1", - "type":"book" - }, - { - "attributes":{ - "title":"For Whom the Bell Tolls" - }, - "id":"2", - "type":"book" - }, - { - "attributes":{ - "title":"Enders Game" - }, - "id":"3", - "type":"book" - } - ] - } - ``` - - - -More information about sparse fields can be found -[here](https://qubitpi.github.io/json-api/format/#fetching-sparse-fieldsets). - -Compound Documents ------------------- - -JSON-API allows the client to fetch a primary collection of elements but also include their relationships or their -relationship's relationships (arbitrarily nested) through compound documents. The _include_ query parameter specifies -what relationships should be expanded in the document. - -The following example fetches the book collection but also includes all of the book authors. Sparse fields are used -to limit the book and author fields in the response: - - - - ```text - /book?include=authors&fields[book]=title,authors&fields[author]=name - ``` - - - ```json - { - "data":[ - { - "attributes":{ - "title":"The Old Man and the Sea" - }, - "id":"1", - "relationships":{ - "authors":{ - "data":[ - { - "id":"1", - "type":"author" - } - ] - } - }, - "type":"book" - }, - { - "attributes":{ - "title":"For Whom the Bell Tolls" - }, - "id":"2", - "relationships":{ - "authors":{ - "data":[ - { - "id":"1", - "type":"author" - } - ] - } - }, - "type":"book" - }, - { - "attributes":{ - "title":"Enders Game" - }, - "id":"3", - "relationships":{ - "authors":{ - "data":[ - { - "id":"2", - "type":"author" - } - ] - } - }, - "type":"book" - } - ], - "included":[ - { - "attributes":{ - "name":"Ernest Hemingway" - }, - "id":"1", - "type":"author" - }, - { - "attributes":{ - "name":"Orson Scott Card" - }, - "id":"2", - "type":"author" - } - ] - } - ``` - - - -More information about compound documents can be found -[here](https://qubitpi.github.io/json-api/format/#document-compound-documents). - -Filtering ---------- - -JSON-API is agnostic to filtering strategies. The only recommendation is that servers and clients _should_ prefix -filtering query parameters with the word 'filter'. - -Elide supports multiple filter dialects and the ability to add new ones to meet the needs of developers or to evolve -the platform should JSON-API standardize them. Elide's primary dialect is -[RSQL](https://github.com/jirutka/rsql-parser). - -### RSQL - -[RSQL](https://github.com/jirutka/rsql-parser) is a query language that allows conjunction (and), disjunction (or), and -parenthetic grouping of Boolean expressions. It is a superset of the -[FIQL language](https://tools.ietf.org/html/draft-nottingham-atompub-fiql-00). - -Because RSQL is a superset of FIQL, FIQL queries should be properly parsed. RSQL primarily adds more friendly lexer -tokens to FIQL for conjunction and disjunction: 'and' instead of ';' and 'or' instead of ','. RSQL also adds a richer -set of operators. FIQL defines all String comparison operators to be case insensitive. Elide overrides this behavior -making all operators case sensitive by default. For case insensitive queries, Elide introduces new operators. - -#### Filter Syntax - -Filter query parameters either look like: - -1. `filter[TYPE]` where 'TYPE' is the name of the data model/entity. These are type specific filters and only apply to - filtering collections of the given type. -2. `filter` with no type specified. This is a global filter and can be used to filter across relationships (by - performing joins in the persistence layer). - -Any number of typed filter parameters can be specified provided the 'TYPE' is different for each parameter. There can -only be a single global filter for the entire query. Typed filters can be used for any collection returned by Elide. -Global filters can only be used to filter root level collections. - -The value of any query parameter is a RSQL expression composed of predicates. Each predicate contains an attribute of -the data model or a related data model, an operator, and zero or more comparison values. - -Filter attributes can be: - -- In the data model itself -- In another related model traversed through to-one or to-many relationships -- Inside an object or nested object hierarchy - -To join across relationships or drill into nested objects, the attribute name is prefixed by one or more relationship or -field names separated by period ('.'). For example, 'author.books.price.total' references all of the author's books with -a price having a particular total value. - -#### Typed Filter Examples - -Return all the books written by author '1' with the genre exactly equal to 'Science Fiction': - -```text -/author/1/book?filter[book]=genre=='Science Fiction' -``` - -Return all the books written by author '1' with the genre exactly equal to 'Science Fiction' _and_ the title starts with -'The' _and_ whose total price is greater than 100.00: - -```text -/author/1/book?filter[book]=genre=='Science Fiction';title==The*;price.total>100.00 -``` - -Return all the books written by author '1' with the publication date greater than a certain time _or_ the genre _not_ -being 'Literary Fiction' or 'Science Fiction': - -```text -/author/1/book?filter[book]=publishDate>1454638927411,genre=out=('Literary Fiction','Science Fiction') -``` - -Return all the books whose title contains 'Foo'. Include all the authors of those books whose name does not equal -'Orson Scott Card': - -```text -/book?include=authors&filter[book]=title==*Foo*&filter[author]=name!='Orson Scott Card' -``` - -#### Global Filter Examples - -Return all the books with an author whose name is 'Null Ned' and whose title is 'Life with Null Ned': - -```text -/book?filter=authors.name=='Null Ned';title=='Life with Null Ned' -``` - -#### Operators - -The following RSQL operators are supported: - -|Operator | Description | -|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -|`=in=` | Evaluates to true if the attribute exactly matches any of the values in the list. (Case Sensitive) | -|`=ini=` | Evaluates to true if the attribute exactly matches any of the values in the list. (Case Insensitive) | -|`=out=` | Evaluates to true if the attribute does not match any of the values in the list. (Case Sensitive) | -|`=outi=` | Evaluates to true if the attribute does not match any of the values in the list. (Case Insensitive) | -|`==ABC*` | Similar to SQL `like 'ABC%'`. (Case Sensitive) | -|`==*ABC` | Similar to SQL `like '%ABC'`. (Case Sensitive) | -|`==*ABC*` | Similar to SQL `like '%ABC%'`. (Case Sensitive) | -|`=ini=ABC*` | Similar to SQL `like 'ABC%'`. (Case Insensitive) | -|`=ini=*ABC` | Similar to SQL `like '%ABC'`. (Case Insensitive) | -|`=ini=*ABC*` | Similar to SQL `like '%ABC%'`. (Case Insensitive) | -|`=isnull=true` | Evaluates to true if the attribute is `null`. | -|`=isnull=false` | Evaluates to true if the attribute is not `null`. | -|`=lt=` | Evaluates to true if the attribute is less than the value. | -|`=gt=` | Evaluates to true if the attribute is greater than the value. | -|`=le=` | Evaluates to true if the attribute is less than or equal to the value. | -|`=ge=` | Evaluates to true if the attribute is greater than or equal to the value. | -|`=isempty=` | Determines if a collection is empty or not. | -|`=between=` | Determines if a model attribute is `>=` and `<=` the two provided arguments. | -|`=notbetween=` | Negates the between operator. | -|`=hasmember=` | Determines if a collection contains a particular element. This can be used to evaluate that an attribute across a to-many association has a `null` value present by using `=hasmember=null`. | -|`=hasnomember=` | Determines if a collection does not contain a particular element. | -|`=subsetof=` | Determines if a collection is a subset of the values in the list. Meaning all the elements of the collection are in the provided values. Note that an empty set is a subset of every set. | -|`=notsubsetof=` | Determines if a collection is not a subset of the values in the list. | -|`=supersetof=` | Determines if a collection is a superset of the values in the list. Meaning all the elements in the provided values are in the collection. | -|`=notsupersetof=` | Determines if a collection is not a superset of the values in the list. | - -The operators `hasmember`, `hasnomember`, `subsetof`, `notsubsetof`, `supersetof`, `notsupersetof` can be applied to collections (book.awards) or across to-many relationships (book.authors.name). - -##### FIQL Default Behaviour - -By default, the FIQL operators `=in=`, `=out=`, `==` are case sensitive. This can be reverted to case insensitive by -changing the case sensitive strategy: - -```java -@Configuration -public class ElideConfiguration { - @Bean - public JsonApiSettingsBuilderCustomizer jsonApiSettingsBuilderCustomizer() { - return builder -> builder - .joinFilterDialect(new RSQLFilterDialect(dictionary), new CaseSensitivityStrategy.FIQLCompliant()) - .subqueryFilterDialect(new RSQLFilterDialect(dictionary), new CaseSensitivityStrategy.FIQLCompliant()); - } -} -``` - -#### Values & Type Coercion - -Values are specified as URL encoded strings. Elide will type coerce them into the appropriate primitive data type for -the attribute filter. - -#### Attribute arguments. - -Some data stores like the Aggregation Store support parameterized model attributes. Parameters can be included in a -filter predicate with the following syntax: - -`field[arg1:value1][arg2:value2]` - -Argument values must be URL encoded. There is no limit to the number of arguments provided in this manner. - -Pagination ----------- - -Elide supports: - -1. paginating a collection by row offset and limit. -2. paginating a collection by page size and number of pages. -3. returning the total size of a collection visible to the given user. -4. returning a _meta_ block in the JSON-API response body containing metadata about the collection or individual - resources. -5. A simple way to control: - - - the availability of metadata - - the number of records that can be paginated - -### Syntax - -Elide allows pagination of the primary collection being returned in the response via the _page_ query parameter. - -The _rough_ BNF syntax for the _page_ query parameter is: - -```text - ::= - "page" "[" "size" "]" "=" - | "page" "[" "number" "]" "=" - | "page" "[" "limit" "]" "=" - | "page" "[" "offset" "]" "=" - | "page" "[" "totals" "]" -``` - -Legal combinations of the _page_ query params include: - -1. size -2. number -3. size & number -4. size & number & totals -5. offset -6. limit -7. offset & limit -8. offset & limit & totals - -### Meta Block - -Whenever a _page_ query parameter is specified, Elide will return a _meta_ block in the JSON-API response that contains: - -1. The page _number_ -2. The page size or _limit_ -3. The total number of pages (_totalPages_) in the collection -4. The total number of records (_totalRecords_) in the collection. - -The values for _totalPages_ and _totalRecords_ are only returned if the _page[totals]_ parameter was specified in the -query. - -### Example - -Paginate the book collection starting at the 4th record. Include no more than 2 books per page. -Include the total size of the collection in the _meta block_: - - - - ```text - /book?page[offset]=3&page[limit]=2&page[totals] - ``` - - - ```json - { - "data":[ - { - "attributes":{ - "chapterCount":0, - "editorName":null, - "genre":"Science Fiction", - "language":"English", - "publishDate":1464638927412, - "title":"Enders Shadow" - }, - "id":"4", - "relationships":{ - "authors":{ - "data":[ - { - "id":"2", - "type":"author" - } - ] - }, - "chapters":{ - "data":[ - - ] - }, - "publisher":{ - "data":null - } - }, - "type":"book" - }, - { - "attributes":{ - "chapterCount":0, - "editorName":null, - "genre":"Science Fiction", - "language":"English", - "publishDate":0, - "title":"Foundation" - }, - "id":"5", - "relationships":{ - "authors":{ - "data":[ - { - "id":"3", - "type":"author" - } - ] - }, - "chapters":{ - "data":[ - - ] - }, - "publisher":{ - "data":null - } - }, - "type":"book" - } - ], - "meta":{ - "page":{ - "limit":2, - "number":2, - "totalPages":4, - "totalRecords":8 - } - } - } - ``` - - - -Sorting -------- - -Elide supports: - -1. sorting a collection by any model attribute. -2. sorting a collection by multiple attributes at the same time in either ascending or descending order. -3. sorting a collection by an attribute of another model connected via one or more to-one relationships. - -### Syntax - -Elide allows sorting of the primary collection being returned in the response via the _sort_ query parameter. - -The _rough_ BNF syntax for the _sort_ query parameter is: - -```text - ::= "sort" "=" - - = | "," - - ::= "+|-"? - - ::= | - - ::= "." - - ::= -``` - -### Sort By ID - -The keyword _id_ can be used to sort by whatever field a given entity uses as its identifier. - -### Example - -Sort the collection of author 1's books in descending order by the book's publisher's name: - - - - ```text - /author/1/books?sort=-publisher.name - ``` - - - ```json - { - "data":[ - { - "attributes":{ - "chapterCount":0, - "editorName":null, - "genre":"Literary Fiction", - "language":"English", - "publishDate":0, - "title":"For Whom the Bell Tolls" - }, - "id":"2", - "relationships":{ - "authors":{ - "data":[ - { - "id":"1", - "type":"author" - } - ] - }, - "chapters":{ - "data":[ - - ] - }, - "publisher":{ - "data":{ - "id":"2", - "type":"publisher" - } - } - }, - "type":"book" - }, - { - "attributes":{ - "chapterCount":0, - "editorName":null, - "genre":"Literary Fiction", - "language":"English", - "publishDate":0, - "title":"The Old Man and the Sea" - }, - "id":"1", - "relationships":{ - "authors":{ - "data":[ - { - "id":"1", - "type":"author" - } - ] - }, - "chapters":{ - "data":[ - - ] - }, - "publisher":{ - "data":{ - "id":"1", - "type":"publisher" - } - } - }, - "type":"book" - } - ] - } - ``` - - - -Bulk Writes And Complex Mutations ---------------------------------- - -JSON-API supports a mechanism for [extensions](https://qubitpi.github.io/json-api/extensions/). - -Elide supports the [Atomic Operations Extension](https://qubitpi.github.io/json-api/ext/atomic/) which allows multiple -mutation operations (create, delete, update) to be bundled together in as single request. Elide also supports the older -deprecated -[JSON Patch Extension](https://github.com/json-api/json-api/blob/9c7a03dbc37f80f6ca81b16d444c960e96dd7a57/extensions/jsonpatch/index.md) -which offers similar functionality. - -Elide supports these extensions because it allows complex & bulk edits to the data model in the context of a single -transaction. - -The extensions require a different Media Type to be specified for the `Content-Type` and `Accept` headers when making -the request. - -|Extension |Media Type | -|------------------|-----------------------------------------------------------------| -|Atomic Operations | `application/vnd.api+json;ext="https://jsonapi.org/ext/atomic"` | -|JSON Patch | `application/vnd.api+json;ext=jsonpatch` | - -Elide's Atomic Operations and JSON Patch extension support requires that all resources have assigned IDs specified using -the `id` member when fixing up relationships. For newly created objects, if the IDs are generated by the server, a -client generated Local ID can be specified using the `lid` member. Client generated IDs should be a UUID as described in -[RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122.html). - -### Atomic Operations - -The following Atomic Operations request creates an author (Ernest Hemingway), some of his books, and his book publisher -in a single request: - - - - ```json - { - "atomic:operations":[ - { - "op":"add", - "data":{ - "lid":"12345678-1234-1234-1234-1234567890ab", - "type":"author", - "attributes":{ - "name":"Ernest Hemingway" - }, - "relationships":{ - "books":{ - "data":[ - { - "type":"book", - "id":"12345678-1234-1234-1234-1234567890ac" - }, - { - "type":"book", - "id":"12345678-1234-1234-1234-1234567890ad" - } - ] - } - } - } - }, - { - "op":"add", - "data":{ - "lid":"12345678-1234-1234-1234-1234567890ac", - "type":"book", - "attributes":{ - "title":"The Old Man and the Sea", - "genre":"Literary Fiction", - "language":"English" - }, - "relationships":{ - "publisher":{ - "data":{ - "type":"publisher", - "id":"12345678-1234-1234-1234-1234567890ae" - } - } - } - } - }, - { - "op":"add", - "data":{ - "lid":"12345678-1234-1234-1234-1234567890ad", - "type":"book", - "attributes":{ - "title":"For Whom the Bell Tolls", - "genre":"Literary Fiction", - "language":"English" - } - } - }, - { - "op":"add", - "href":"/book/12345678-1234-1234-1234-1234567890ac/publisher", - "data":{ - "lid":"12345678-1234-1234-1234-1234567890ae", - "type":"publisher", - "attributes":{ - "name":"Default publisher" - } - } - } - ] - } - ``` - - - ```json - { - "atomic:results":[ - { - "data":{ - "attributes":{ - "name":"Ernest Hemingway" - }, - "id":"1", - "relationships":{ - "books":{ - "data":[ - { - "id":"1", - "type":"book" - }, - { - "id":"2", - "type":"book" - } - ] - } - }, - "type":"author" - } - }, - { - "data":{ - "attributes":{ - "chapterCount":0, - "editorName":null, - "genre":"Literary Fiction", - "language":"English", - "publishDate":0, - "title":"The Old Man and the Sea" - }, - "id":"1", - "relationships":{ - "authors":{ - "data":[ - { - "id":"1", - "type":"author" - } - ] - }, - "chapters":{ - "data":[ - - ] - }, - "publisher":{ - "data":{ - "id":"1", - "type":"publisher" - } - } - }, - "type":"book" - } - }, - { - "data":{ - "attributes":{ - "chapterCount":0, - "editorName":null, - "genre":"Literary Fiction", - "language":"English", - "publishDate":0, - "title":"For Whom the Bell Tolls" - }, - "id":"2", - "relationships":{ - "authors":{ - "data":[ - { - "id":"1", - "type":"author" - } - ] - }, - "chapters":{ - "data":[ - - ] - }, - "publisher":{ - "data":null - } - }, - "type":"book" - } - }, - { - "data":{ - "attributes":{ - "name":"Default publisher" - }, - "id":"1", - "type":"publisher" - } - } - ] - } - ``` - - - - -### JSON Patch - -The following JSON Patch request creates an author (Ernest Hemingway), some of his books, and his book publisher in a -single request: - - - - ```json - [ - { - "op":"add", - "path":"/author", - "value":{ - "id":"12345678-1234-1234-1234-1234567890ab", - "type":"author", - "attributes":{ - "name":"Ernest Hemingway" - }, - "relationships":{ - "books":{ - "data":[ - { - "type":"book", - "id":"12345678-1234-1234-1234-1234567890ac" - }, - { - "type":"book", - "id":"12345678-1234-1234-1234-1234567890ad" - } - ] - } - } - } - }, - { - "op":"add", - "path":"/book", - "value":{ - "type":"book", - "id":"12345678-1234-1234-1234-1234567890ac", - "attributes":{ - "title":"The Old Man and the Sea", - "genre":"Literary Fiction", - "language":"English" - }, - "relationships":{ - "publisher":{ - "data":{ - "type":"publisher", - "id":"12345678-1234-1234-1234-1234567890ae" - } - } - } - } - }, - { - "op":"add", - "path":"/book", - "value":{ - "type":"book", - "id":"12345678-1234-1234-1234-1234567890ad", - "attributes":{ - "title":"For Whom the Bell Tolls", - "genre":"Literary Fiction", - "language":"English" - } - } - }, - { - "op":"add", - "path":"/book/12345678-1234-1234-1234-1234567890ac/publisher", - "value":{ - "type":"publisher", - "id":"12345678-1234-1234-1234-1234567890ae", - "attributes":{ - "name":"Default publisher" - } - } - } - ] - ``` - - - ```json - [ - { - "data":{ - "attributes":{ - "name":"Ernest Hemingway" - }, - "id":"1", - "relationships":{ - "books":{ - "data":[ - { - "id":"1", - "type":"book" - }, - { - "id":"2", - "type":"book" - } - ] - } - }, - "type":"author" - } - }, - { - "data":{ - "attributes":{ - "chapterCount":0, - "editorName":null, - "genre":"Literary Fiction", - "language":"English", - "publishDate":0, - "title":"The Old Man and the Sea" - }, - "id":"1", - "relationships":{ - "authors":{ - "data":[ - { - "id":"1", - "type":"author" - } - ] - }, - "chapters":{ - "data":[ - - ] - }, - "publisher":{ - "data":{ - "id":"1", - "type":"publisher" - } - } - }, - "type":"book" - } - }, - { - "data":{ - "attributes":{ - "chapterCount":0, - "editorName":null, - "genre":"Literary Fiction", - "language":"English", - "publishDate":0, - "title":"For Whom the Bell Tolls" - }, - "id":"2", - "relationships":{ - "authors":{ - "data":[ - { - "id":"1", - "type":"author" - } - ] - }, - "chapters":{ - "data":[ - - ] - }, - "publisher":{ - "data":null - } - }, - "type":"book" - } - }, - { - "data":{ - "attributes":{ - "name":"Default publisher" - }, - "id":"1", - "type":"publisher" - } - } - ] - ``` - - - -Links ------ - -JSON-API links are disabled by default. They can be enabled in `application.yaml`: - -```yaml -elide: - base-url: 'https://my-elide.com' - json-api: - enabled: true - path: /json - links: - enabled: true -``` - -The `elide.json-api.links.enabled` property switches the feature on. The `base-url` property provides the URL schema, -host, and port our clients use to connect to our service. The `path` property provides the route where the JSON-API -controller is rooted. All link URLs using the above configuration would be prefixed with 'https://my-elide.com/json'. - -If `base-url` is not provided, Elide will generate the link URL prefix using the client HTTP request. - -For Elide standalone, we can enable links by overriding `ElideStandaloneSettings` and configure the settings: - -```java -public abstract class Settings implements ElideStandaloneSettings { - @Override - public String getBaseUrl() { - return "https://elide.io"; - } - - @Override - public JsonApiSettingsBuilder getJsonApiSettingsBuilder(EntityDictionary dictionary, JsonApiMapper mapper) { - String jsonApiBaseUrl = getBaseUrl() - + getJsonApiPathSpec().replace("/*", "") - + "/"; - - return ElideStandaloneSettings.super.getJsonApiSettingsBuilder(dictionary, mapper) - .links(links -> links.enabled(true).jsonApiLinks(new DefaultJsonApiLinks(jsonApiBaseUrl))); - } -} -``` - -Enabling JSON-API links will result in payload responses that look like: - -```json -{ - "data": [ - { - "type": "group", - "id": "com.example.repository", - "attributes": { - "commonName": "Example Repository", - "description": "The code for this project" - }, - "relationships": { - "products": { - "links": { - "self": "https://elide.io/api/v1/group/com.example.repository/relationships/products", - "related": "https://elide.io/api/v1/group/com.example.repository/products" - }, - "data": [ - - ] - } - }, - "links": { - "self": "https://elide.io/api/v1/group/com.example.repository" - } - } - ] -} -``` - -We can customize the links that are returned by registering our own implementation of `JsonApiLinks` with -`ElideSettings`: - -```java -public interface JsonApiLinks { - Map getResourceLevelLinks(PersistentResource var1); - - Map getRelationshipLinks(PersistentResource var1, String var2); -} -``` - -Meta Blocks ------------ - -JSON-API supports returning non-standard information in responses inside a -[meta block](https://qubitpi.github.io/json-api/format/#document-meta). Elide supports meta blocks in three scenarios: - -1. Document meta blocks are returned for any [pagination](#pagination) request. -2. The developer can customize the Document meta block for any collection query. -3. The developer can customize a Resource meta block for any resource returned by Elide. - -### Customizing the Document Meta Block - -To customize the document meta block, add fields to the `RequestScope` object inside a -[custom data store](datastores#custom-stores): - -```java -@Override -public DataStoreIterable loadObjects(EntityProjection projection, RequestScope scope){ - //Populates the JSON-API meta block with a new field, 'key': - scope.setMetadataField("key", 123); -} -``` - -This would produce a JSON response like: - -```json -{ - "data": [ - { - "type": "widget", - "id": "1" - } - ], - "meta": { - "key": 123 - } -} - -``` - -### Customizing the Resource Meta Block - -To customize the resource meta block, the resource model class must implement the `WithMetadata` interface: - -```java -public interface WithMetadata { - - /** - * Sets a metadata property for this request. - * @param property - * @param value - */ - default void setMetadataField(String property, Object value) { //NOOP } - - /** - * Retrieves a metadata property from this request. - * @param property - * @return An optional metadata property. - */ - Optional getMetadataField(String property); - - /** - * Return the set of metadata fields that have been set. - * @return metadata fields that have been set. - */ - Set getMetadataFields(); -} -``` - -For example, the following example model implements `WithMetadata`: - -```java -@Include -public class Widget implements WithMetadata { - static Map metadata = Map.of("key", 123); - - @Id - private String id; - - @Override - public Optional getMetadataField(String property) { - return Optional.ofNullable(Widget.metadata.get(property)); - } - - @Override - public Set getMetadataFields() { - return Widget.metadata.keySet(); - } -} -``` - -The models must be populated with at least one field for the meta block to be returned in the response. These fields can -also be populated in a [custom data store](datastores#custom-stores) or [lifecycle hook](data-model#lifecycle-hooks). -This would produce a JSON response like: - -```json -{ - "data": [ - { - "type": "widget", - "id": "1", - "meta": { - "key": 123 - } - } - ] -} -``` - -Type Serialization/Deserialization ----------------------------------- - -Type coercion between the API and underlying data model has common support across JSON-API & GraphQL, and is covered -[here](clientapis#type-coercion). - -OpenAPI -------- - -OpenAPI documents can be highly customized. The steps to customize this are documented [here](openapi). - -Custom Error Responses ----------------------- - -Configuring custom error responses is documented [here](clientapis#custom-error-responses). diff --git a/docs/docs/crud/elide/configuration.md b/docs/docs/crud/elide/configuration.md deleted file mode 100644 index 1719d158..00000000 --- a/docs/docs/crud/elide/configuration.md +++ /dev/null @@ -1,313 +0,0 @@ ---- -sidebar_position: 13 -title: Configuration ---- - -Spring Boot ------------ - -The configuration for Elide Spring Boot is implemented using auto-configuration classes which reads configuration properties that can be configured using Spring Boot's -[externalized configuration](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.external-config) -feature. - -The configuration properties do not expose all the settings that can be customized. Some require overriding of the beans -being generated by the auto-configuration classes or by defining customizer beans that can make specific changes to the -auto-configured defaults. Where more than one customizer is defined they will be applied following the `@Order` -annotation. - -### Elide - -Elide uses `ElideSettings` for configuring various aspect of how Elide runs. This is auto-configured in -`ElideAutoConfiguration`. - -This can be customized by defining a `ElideSettingsBuilderCustomizer`. - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public ElideSettingsBuilderCustomizer elideSettingsBuilderCustomizer() { - return builder -> builder.auditLogger(new MyCustomAuditLogger()); - } -} -``` - -#### Custom Serializers and Deserializers - -By default serdes for the following are added to the auto-configured `SerdesBuilder`. - -| Class | Serde | -|----------------------------|------------------------------------------------------------------------| -| `java.time.Instant` | `com.paiondata.elide.core.utils.coerce.converters.InstantSerde` | -| `java.time.OffsetDateTime` | `com.paiondata.elide.core.utils.coerce.converters.OffsetDateTimeSerde` | -| `java.util.TimeZone` | `com.paiondata.elide.core.utils.coerce.converters.TimeZoneSerde` | -| `java.net.URL` | `com.paiondata.elide.core.utils.coerce.converters.URLSerde` | -| `java.util.Date` | `com.paiondata.elide.core.utils.coerce.converters.ISO8601DateSerde` | -| `java.sql.Date` | `com.paiondata.elide.core.utils.coerce.converters.ISO8601DateSerde` | -| `java.sql.Time` | `com.paiondata.elide.core.utils.coerce.converters.ISO8601DateSerde` | -| `java.sql.Timestamp` | `com.paiondata.elide.core.utils.coerce.converters.ISO8601DateSerde` | - -This can be customized by defining a `SerdesBuilderCustomizer`. If the defaults are not required the existing serdes can -be cleared. - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public SerdesBuilderCustomizer serdesBuilderCustomizer() { - return builder -> builder.clear().entry(Address.class, new AddressSerde()); - } -} -``` - -This can also be customized by defining a `ElideSettingsBuilderCustomizer`. - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public ElideSettingsBuilderCustomizer elideSettingsBuilderCustomizer() { - return builder -> builder.serdes(serdes -> serdes.entry(Address.class, new AddressSerde())); - } -} -``` - -### JSON API - -Elide uses `JsonApiSettings` for configuring JSON-API. This is auto-configured in `ElideAutoConfiguration`. - -This can be customized by defining a `JsonApiSettingsBuilderCustomizer`. The following configures the use of JSON-API -links in results. - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public JsonApiSettingsBuilderCustomizer jsonApiSettingsBuilderCustomizer() { - return builder -> builder.links(links -> links.enabled(true).jsonApiLinks(new MyCustomJsonApiLinks())); - } -} -``` - -The following configures the filter dialect. - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public JsonApiSettingsBuilderCustomizer jsonApiSettingsBuilderCustomizer(EntityDictionary entityDictionary) { - return builder -> builder - .joinFilterDialect(RSQLFilterDialect.builder().dictionary(entityDictionary).build()) - .subqueryFilterDialect(RSQLFilterDialect.builder().dictionary(entityDictionary).build()); - } -} -``` - -#### Controller - -Elide auto-configures a `JsonApiController` in `ElideAutoConfiguration`. This can be overridden by defining a bean with -the name `jsonApiController`. The custom controller does not need to extend `JsonApiController`. - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public MyCustomJsonApiController jsonApiController() { - return new MyCustomJsonApiController(); - } -} -``` - -### GraphQL - -Elide uses `GraphQLSettings` for configuring GraphQL. This is auto-configured in `ElideAutoConfiguration`. - -This can be customized by defining a `GraphQLSettingsBuilderCustomizer`. The following configures the use of Federation. - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public GraphQLSettingsBuilderCustomizer graphqlSettingsBuilderCustomizer() { - return builder -> builder.federation(federation -> federation.enabled(true)); - } -} -``` - -#### Controller - -Elide auto-configures a `GraphQLController` in `ElideAutoConfiguration`. This can be overridden by defining a bean with the name `graphqlController`. The custom controller does not need to extend `GraphQLController`. - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public MyCustomGraphQLController graphqlController() { - return new MyCustomGraphQLController(); - } -} -``` - -### Async - -Elide uses `AsyncSettings` for configuring Async. This is auto-configured in `ElideAutoConfiguration`. - -This can be customized by defining a `AsyncSettingsBuilderCustomizer`. The following configures export. - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public AsyncSettingsBuilderCustomizer asyncSettingsBuilderCustomizer() { - return builder -> builder.export(export -> export.enabled(true).path("/export")); - } -} -``` - -### API Docs - -### Controller - -Elide auto-configures a `ApiDocsController` in `ElideAutoConfiguration`. This can be overridden by defining a bean with the name `graphqlController`. The custom controller does not need to extend `ApiDocsController`. - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public MyApiDocsController apiDocsController() { - return new MyCustomApiDocsController(); - } -} -``` - -### Core Properties - -| Name | Description | Default Value | -|-------------------------------------|-------------------------------------------------------------------------------------------------------|---------------| -| `elide.base-url` | The base service URL that clients use in queries. | | -| `elide.default-page-size` | Default pagination size for collections if the client doesn't set the pagination size. | `500` | -| `elide.max-page-size` | The maximum pagination size a client can request. | `10000` | -| `elide.verbose-errors` | Turns on/off verbose error responses. | `false` | -| `elide.strip-authorization-headers` | Remove Authorization headers from RequestScope to prevent accidental logging of security credentials. | `true` | - -### API Versioning Strategy Properties - -| Name | Description | Default Value | -|-------------------------------------------------------------------|------------------------------------------------------------------------------------------------|------------------| -| `elide.api-versioning-strategy.path.enabled` | Whether or not the path based strategy is enabled. | `true` | -| `elide.api-versioning-strategy.path.version-prefix` | The version prefix to use. For instance `/v1/resource`. | `v` | -| `elide.api-versioning-strategy.header.enabled` | Whether or not the header based strategy is enabled. | `false` | -| `elide.api-versioning-strategy.header.header-name` | The header names that contains the API version. For instance `Accept-Version` or `ApiVersion`. | `Accept-Version` | -| `elide.api-versioning-strategy.parameter.enabled` | Whether or not the parameter based strategy is enabled. | `false` | -| `elide.api-versioning-strategy.parameter.parameter-name` | The parameter name that contains the API version. | `v` | -| `elide.api-versioning-strategy.media-type-profile.enabled` | Whether or not the media type profile based strategy is enabled. | `false` | -| `elide.api-versioning-strategy.media-type-profile.version-prefix` | The version prefix to use for the version. | `v` | -| `elide.api-versioning-strategy.media-type-profile.uri-prefix` | The uri prefix to use to determine the profile that contains the API version. | | - -### JSON-API Properties - -| Name | Description | Default Value | -|--------------------------------|-------------------------------------------|---------------| -| `elide.json-api.enabled` | Whether or not the controller is enabled. | `false` | -| `elide.json-api.path` | The URL path prefix for the controller. | `/` | -| `elide.json-api.links.enabled` | Turns on/off JSON-API links in the API. | `false` | - -### GraphQL Properties - -| Name | Description | Default Value | -|-----------------------------------------------------|--------------------------------------------------------------------------------------------|---------------| -| `elide.graphql.enabled` | Whether or not the controller is enabled. | `false` | -| `elide.graphql.path` | The URL path prefix for the controller. | `/` | -| `elide.graphql.federation.enabled` | Turns on/off Apollo federation schema. | `false` | -| `elide.graphql.subscription.enabled` | Whether or not the controller is enabled. | `false` | -| `elide.graphql.subscription.path` | The URL path prefix for the controller. | `/` | -| `elide.graphql.subscription.send-ping-on-subscribe` | Websocket sends a PING immediate after receiving a SUBSCRIBE. | `false` | -| `elide.graphql.subscription.connection-timeout` | Time allowed from web socket creation to successfully receiving a CONNECTION_INIT message. | `5000ms` | -| `elide.graphql.subscription.idle-timeout` | Maximum idle timeout in milliseconds with no websocket activity. | `300000ms` | -| `elide.graphql.subscription.max-subscriptions` | Maximum number of outstanding GraphQL queries per websocket. | `30` | -| `elide.graphql.subscription.max-message-size` | Maximum message size that can be sent to the websocket. | `10000` | -| `elide.graphql.subscription.publishing.enabled` | Whether Elide should publish subscription notifications to JMS on lifecycle events. | `false` | - -### API Docs Properties - -| Name | Description | Default Value | -|----------------------------------|---------------------------------------------------|---------------| -| `elide.api-docs.enabled` | Whether or not the controller is enabled. | `false` | -| `elide.api-docs.path` | The URL path prefix for the controller. | `/` | -| `elide.api-docs.version` | The OpenAPI Specification Version to generate. | `openapi-3-0` | - -### Async Properties - -| Name | Description | Default Value | -|---------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|---------------| -| `elide.async.enabled` | Whether or not the async feature is enabled. | `false` | -| `elide.async.thread-pool-size` | Default thread pool size. | `5` | -| `elide.async.max-async-after` | Default maximum permissible time to wait synchronously for the query to complete before switching to asynchronous mode. | `10s` | -| `elide.async.cleanup.enabled` | Whether or not the cleanup is enabled. | `false` | -| `elide.async.cleanup.query-max-run-time` | Maximum query run time. | `3600s` | -| `elide.async.cleanup.query-retention-duration` | Retention period of async query and results before being cleaned up. | `7d` | -| `elide.async.cleanup.query-cancellation-check-interval` | Polling interval to identify async queries that should be canceled. | `300s` | -| `elide.async.export.enabled` | Whether or not the controller is enabled. | `false` | -| `elide.async.export.path` | The URL path prefix for the controller. | `/export` | -| `elide.async.export.append-file-extension` | Enable Adding Extension to table export attachments. | `false` | -| `elide.async.export.storage-destination` | Storage engine destination. | `/tmp` | -| `elide.async.export.format.csv.write-header` | Generates the header in a CSV formatted export. | `true` | - -### Aggregation Store Properties - -| Name | Description | Default Value | -|-------------------------------------------------------------|---------------------------------------------------------------------------------------|---------------| -| `elide.aggregation-store.enabled` | Whether or not aggregation data store is enabled. | `false` | -| `elide.aggregation-store.default-dialect` | SQLDialect type for default DataSource Object. | `Hive` | -| `elide.aggregation-store.query-cache.enabled` | Whether or not to enable the query cache. | `true` | -| `elide.aggregation-store.query-cache.expiration` | Query cache expiration after write. | `10m` | -| `elide.aggregation-store.query-cache.max-size` | Limit on number of query cache entries. | `1024` | -| `elide.aggregation-store.metadata-store.enabled` | Whether or not meta data store is enabled. | `false` | -| `elide.aggregation-store.dynamic-config.enabled` | Whether or not dynamic model config is enabled. | `false` | -| `elide.aggregation-store.dynamic-config.path` | The path where the config hjsons are stored. | `/` | -| `elide.aggregation-store.dynamic-config.config-api.enabled` | Enable support for reading and manipulating HJSON configuration through Elide models. | `false` | - -### JPA Store Properties - -| Name | Description | Default Value | -|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|---------------| -| `elide.jpa-store.delegate-to-in-memory-store` | When fetching a subcollection from another multi-element collection, whether or not to do sorting, filtering and pagination in memory. | `true` | - -#### Converting Durations - -The properties indicating a duration are specified using `java.time.Duration`. For instance a configuration value of -`7d` indicates 7 days and `300s` indicates 300 seconds. - -The following are the supported units - -- `ns` for nanoseconds -- `us` for microseconds -- `ms` for milliseconds -- `s` for seconds -- `m` for minutes -- `h` for hours -- `d` for days - -Standalone ----------- - -### Settings - -The configuration for the Elide Standalone is implemented using interfaces. - -| Interface | Description | -|-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------| -| `com.paiondata.elide.standalone.config.ElideStandaloneSettings` | Elide core configuration and JSON-API, GraphQL and OpenAPI API Documentation and all other settings. | -| `com.paiondata.elide.standalone.config.ElideStandaloneSubscriptionSettings` | GraphQL Subscriptions configuration. | -| `com.paiondata.elide.standalone.config.ElideStandaloneAnalyticSettings` | Analytic configuration. | -| `com.paiondata.elide.standalone.config.ElideStandaloneAsyncSettings` | Async configuration. | diff --git a/docs/docs/crud/elide/data-model.md b/docs/docs/crud/elide/data-model.md deleted file mode 100644 index 3596a34e..00000000 --- a/docs/docs/crud/elide/data-model.md +++ /dev/null @@ -1,508 +0,0 @@ ---- -sidebar_position: 4 -title: Data Models -description: Creating CRUD data models (create, read, update, and delete) in the backend using Elide ---- - -:::note - -This page is a description on how to _create_ CRUD data models (create, read, update, and delete) in the backend -using Elide. For more information on _interacting_ with an Elide API, please see the -[API usage documentation](clientapis). More information for creating analytic models can be found [here](analytics). - -::: - -Elide generates its API entirely based on the concept of **data models**. Data models are JVM classes that represent -both a concept to our application and the _schema_ of an exposed web service endpoint. Data models are intended to be a -_view_ on top of the [data store](datastores) or the set of data stores which support our Elide-based service. - -All Elide models have an identifier field that identifies a unique instance of the model. Models are also composed of -optional attributes and relationships. **Attribute** are properties of the model. **Relationships** are simply links to -other related Elide models. Annotations are used to declare that a class is an Elide model, that a relationship exists -between two models, to denote which field is the identifier field, and to [secure the model](security). - -Annotations ------------ - -Elide has first class support for JPA (Java Persistence API) annotations. These annotations serve double duty by both: - -- describing the attributes, relationships, and id field of a model. -- provide an object relational mapping that can be used by an Elide data store to persist the model. - -Elide makes use of the following JPA annotations: `@OneToOne`, `@OneToMany`, `@ManyToOne`, `@ManyToMany`, `@Id`, -`@EmbeddedId`, and `@GeneratedValue`. - -However, JPA is not required and Elide supports its own set of annotations for describing models: - -| Annotation Purpose | JPA | Non-JPA | -|--------------------------|-----------------------------|-------------------| -| Expose a model in elide | | `@Include` | -| To One Relationship | `@OneToOne`, `@ManyToOne` | `@ToOne` | -| To Many Relationship | `@OneToMany`, `@ManyToMany` | `@ToMany` | -| Mark an identifier field | `@Id`, `@EmbeddedId` | | - -Much of the Elide per-model configuration is done via annotations. For a full description of all -Elide-supported annotations, please check out the annotation Javadoc. - -Exposing a Model as an Elide Endpoint -------------------------------------- - -After creating a proper data model, we can expose it through Elide by marking with with `@Include`. Elide -generates its API as a _graph_. This graph can only be traversed starting at a _root_ node. Rootable entities are -denoted by applying `@Include` to the top-level of the class with the **rootLevel** property unset or set to `true`. -Non-rootable entities can be accessed only as relationships through the graph. - -```java -@Entity -@Include -public class Author { - - @Id - @GeneratedValue(strategy=GenerationType.AUTO) - private Long id; - - private String name; - - @ManyToMany - private Set books; -} -``` - -```java -@Entity -@Include(rootLevel = false) -public class Book { - @Id - @GeneratedValue(strategy=GenerationType.AUTO) - private Long id; - - private String title; - - @ManyToMany - private Set authors; -} -``` - -Considering the example above, we have a full data model that exposes a specific graph. Namely, a root node of the -type `Author` and a bi-directional relationship from `Author` to `Book`. That is, one can access all `Author` -objects directly, but must go _through_ an author to see information about any specific `Book` object. - -Model Identifiers ------------------ - -Every model in Elide must have an ID. This is a requirement of both the JSON-API specification and Elide's GraphQL API. -Identifiers can be assigned by the persistence layer automatically or the client. Elide must know two things: - -1. What field is the ID of the model. This is determined by the `@Id` or `@EmbeddedId` annotation. -2. Whether the persistence layer is assigning the ID or not. This is determined by the presence or absence of the - `@GeneratedValue` annotation. - -Identifier fields in Elide are typically integers, longs, strings, or UUIDs. It is also possible to have -composite/compound ID fields composed of multiple fields. For example, the following identifier type includes three -fields that together create a primary key: - -```java -@Data -@Embeddable -@NoArgsConstructor -@AllArgsConstructor -public class Address implements Serializable { - private long number; - private String street; - private long zipCode; -} -``` - -This new compound ID type can then be referenced in an Elide model identifier like this: - -```java -@Data -@Entity -@Include(rootLevel = true) -public class Building { - @EmbeddedId - private Address address; -} -``` - -Because JSON-API requires all ID fields to be Strings, composite/compound IDs require the developer to register an -Elide `Serde` to serialize and deserialize the ID type to a String. For example, the following `Serde` will -encode/decode an `Address` as a base64 encoded string: - -```java -@ElideTypeConverter(type = Address.class, name = "Address") -public class AddressSerde implements Serde { - - private static final Pattern ADDRESS_PATTERN = - Pattern.compile("Address\\(number=(\\d+), street=([a-zA-Z0-9 ]+), zipCode=(\\d+)\\)"); - - @Override - public Address deserialize(String val) { - byte[] decodedBytes = Base64.getDecoder().decode(val); - String decodedString = new String(decodedBytes); - - Matcher matcher = ADDRESS_PATTERN.matcher(decodedString); - if (! matcher.matches()) { - throw new InvalidValueException(decodedString); - } - long number = Long.valueOf(matcher.group(1)); - String street = matcher.group(2); - long zipCode = Long.valueOf(matcher.group(3)); - - Address address = new Address(number, street, zipCode); - - return address; - } - - @Override - public String serialize(Address val) { - return Base64.getEncoder().encodeToString(val.toString().getBytes()); - } -} -``` - -More information about `Serde` and user defined types can be found [here](clientapis#type-coercion). - -Attributes vs Relationships ---------------------------- - -Elide distinguishes between attributes and relationships in a data model: - -1. *Relationships* are links from one model to another. They can be traversed directly through the API. If the - relationship represents a collection, they can also be sorted, filtered, and paginated. Relationships must be - explicitly marked with an annotation (for example - `@ToMany`) in the model and can be bidirectional or - unidirectional. -2. *Attributes* are properties of a model. They can be primitive types, objects, or collections of objects or - primitives. Attributes which are collections cannot be sorted, filtered, or paginated in the API. Complex - attributes (collections or objects) cannot be used in a filter predicate. Attributes are not marked with - annotations in Elide. - -Model Properties or Fields --------------------------- - -An Elide model can be described using properties (getter and setter functions) or fields (class member variables) but -_not_ both on the same entity. For any given entity, Elide looks at whether `@Id` or `@EmbeddedId` is a property or -field to determine the access mode (property or field) for that entity. All public properties and all fields are -exposed through the Elide API if they are not explicitly marked `@Transient` or `@Exclude`. `@Transient` allows a field -to be ignored by both Elide and an underlying persistence store while `@Exclude` allows a field to exist in the -underlying persistence layer without exposing it through the Elide API. - -Computed Attributes -------------------- - -A computed attribute is an entity attribute whose value is computed in code rather than fetched from a data store. - -Elide supports computed properties by way of the `@ComputedAttribute` and `@ComputedRelationship` annotations. These -are useful if our data store is also tied to your Elide view data model. For instance, if we mark a field `@Transient`, -a data store such as Hibernate will ignore it. In the absence of the `@Computed*` attributes, Elide will too. However, -when applying a computed property attribute, Elide will expose this field anyway. - -A computed attribute can perform arbitrary computation and is exposed through Elide as a typical attribute. In the case -below, this will create an attribute called `myComputedAttribute`. - -```java -@Entity -@Include -public class Book { - - ... - - @Transient - @ComputedAttribute - public String getMyComputedAttribute(RequestScope requestScope) { - return "My special string stored only in the JVM!"; - } - - ... -} -``` - -The same principles are analogous to `@ComputedRelationship`s. - -Lifecycle Hooks ---------------- - -Lifecycle hooks allow custom business logic (defined in functions) to be invoked during CRUD operations at four -distinct phases of the client request: - -1. *Pre Security* - Executed immediate prior to Elide security check evaluation. -2. *Pre Flush* - Executed immediate prior to Elide flushing the transaction. This is when database calls are first sent - to the database. -3. *Pre Commit* - Executed immediately prior to transaction commit but after all security checks have been evaluated. -4. *Post Commit* - Executed immediately after transaction commit. - -There are two mechanisms to enable lifecycle hooks on a particular model: - -1. The simplest mechanism is to [decorate](#annotation-based-hooks) the Elide model or model fields with the life cycle - hook function class and the conditions of when to invoke it. -2. Lifecycle hook functions can also be [registered](#registered-function-hooks) with the `EntityDictionary` when - initializing Elide. - -Life cycle hooks are simply functions that conform to the following interface: - -```java -/** - * Function which will be invoked for Elide lifecycle triggers - * - * @param The elide entity type associated with this callback. - */ -@FunctionalInterface -public interface LifeCycleHook { - - /** - * Run for a lifecycle event. - * - * @param operation CREATE, UPDATE, or DELETE - * @param phase PRESECURITY, PREFLUSH, PRECOMMIT or POSTCOMMIT - * @param elideEntity The entity that triggered the event - * @param requestScope The request scope - * @param changes Optionally, the changes that were made to the entity - */ - void execute( - LifeCycleHookBinding.Operation operation, - LifeCycleHookBinding.TransactionPhase phase, - T elideEntity, - RequestScope requestScope, - Optional changes - ); -} -``` - -### Annotation Based Hooks - -Model fields can be decorated with a `LifeCycleHookBinding` annotation. The annotation provides the following -information: - -1. The hook function to invoke. -2. The model operation (CREATE, UPDATE, or DELETE) that triggers the hook. -3. The transaction phase of when to trigger the hook (PRESECURITY, PREFLUSH, PRECOMMIT, or POSTCOMMIT). -4. For class level triggers, whether or not the hook should be called for each impacted field or exactly once for - the class. - -```java -class Publisher { - - @Id - private long id; - - @OneToMany(mappedBy = "publisher") - @LifeCycleHookBinding(operation = UPDATE, phase = PRECOMMIT, hook = PublisherUpdateHook.class) - private Set books; -} -``` - -### Registered Function Hooks - -Lifecycle hooks can be registered in Elide directly without an explicit annotation: - -```java -// Register a lifecycle hook for deletes on the model Book. Call exactly once. -dictionary.bindTrigger(Book.class, DELETE, PRESECURITY, hook, false); - -// Register a lifecycle hook for updates on the Book model's title attribute -dictionary.bindTrigger(Book.class, "title", UPDATE, POSTCOMMIT, hook); - -// Register a lifecycle hook for updates on _any_ of the Book model's attributes -dictionary.bindTrigger(Book.class, UPDATE, POSTCOMMIT, hook, true); -``` - -Dependency Injection --------------------- - -Elide does not depend on a specific dependency injection framework. However, Elide can inject entity models, security -checks, lifecycle hooks, and serdes during their construction. Elide provides a framework agnostic, functional -interface to inject entity models: - -```java -/** - * Abstraction around dependency injection. - */ -@FunctionalInterface -public interface Injector { - - /** - * Inject an Elide object. - * - * @param entity object to inject - */ - void inject(Object entity); - - /** - * Instantiates a new instance of a class using the DI framework. - * - * @param cls The class to instantiate. - * @return An instance of the class. - */ - default T instantiate(Class cls) { - try { - return cls.newInstance(); - } catch (InstantiationException | IllegalAccessException e) { - throw new IllegalStateException(e); - } - } -} -``` - -An implementation of this interface can be passed to the `EntityDictionary` during its construction: - -```java -EntityDictionary dictionary = new EntityDictionary( - PermissionExpressions.getExpressions(), - (obj) -> injector.inject(obj) -); -``` - -If we're using the `elide-spring-boot*` artifacts, dependency injection is already setup using Spring. If we're using -the [elide-standalone](elide-standalone) artifact, dependency injection is already setup using Jetty's `ServiceLocator` - -Validation ----------- - -Data models can be validated using [bean validation](http://beanvalidation.org/1.0/spec/). This requires *JSR303* -data model annotations and wiring in a bean validator in the `DataStore`. - -Type Coercion -------------- - -Type coercion between the API and underlying data model has common support across JSON-API and GraphQL and is -covered [here](clientapis#type-coercion). - -Inheritance ------------ - -Elide supports two kinds of inheritance: - -1. Non-entity inheritance via the JPA annotation `@MappedSuperclass`. -2. Entity inheritance via the JPA annotation `@Inheritance`. - -Entity inheritance has a few caveats: - -1. Only the `InheritanceType.JOINED` and `InheritanceType.SINGLE_TABLE` strategies are supported. -2. Entity relationships whose type is a superclass have different behavior in JSON-API and GraphQL: - - 1. JSON-API will return the type and attributes of the subclass (as well as the super class). - 2. GraphQL will return the type and attributes of the superclass only. - -API Versions ------------- - -Elide models can be bound to a specific API version. Once bound, the models will only be visible to API requests -that ask for the specific version. API versions are bound by creating a package-info.java file with the -`ApiVersion` annotation: - -```java -@ApiVersion(version = "1.0") -package example.models; - -import com.paiondata.elide.annotation.ApiVersion; -``` - -API versioning is optional configuration. By default, all models have no implicit version. The API client is also -not required to provide a version in its request.By adding a version to one or more packages however, the versioned -models will only be visible when the client provides the corresponding version in its request. - -There is an important caveat when using API versioning with JPA models.JPA does not allow two `Entity` classes to -share the same name - even if they belong to different packages. To work around this, we can either: - -- Rename the class (class BookV2) but preserve the Elide model (`@Include(type = "book")`) and database table - (`@Table(name = "book")`) names. -- Rename the entity name (`@Entity(name = "BookV2")`) but preserve the Elide model (`@Include(type = "book")`) and - class (class Book) names. - -Details of how to construct client queries for a specific version can be found [here](clientapis#api-versioning). - -Philosophy ----------- - - -Data models are intended to be a _view_ on top of the [data store](datastores) or the set of data stores which support -our Elide-based service. While other JPA-based workflows often encourage writing data models that exactly match the -underlying schema of the data store, we propose a strategy of isolation on per-service basis. Namely, we recommend -creating a data model that only supports precisely the bits of data we need from our underlying schema. Often times -there will be no distinction when first building our systems. However, as our systems scale and we develop multiple -services with overlapping data store requirements, isolation often serves as an effective tool to **reduce -interdependency** among services and **maximize the separation of concern**. Overall, while models can correspond to -our underlying data store schema as a one-to-one representation, it's not always strictly necessary and sometimes even -undesirable. - - -As an example, let's consider a situation where we have two Elide-based microservices: one for our application backend -and another for authentication (suppose account creation is performed out-of-band for this example). Assuming both of -these rely on a common data store, they'll both likely want to recognize the same underlying _User_ table. However, -it's quite likely that the authentication service will only ever require information about user **credentials** and the -application service will likely only ever need user **metadata**. More concretely, you could have a system that looks -like the following: - -Table schema: - -```sql -id -userName -password -firstName -lastName -``` - -Authentication schema: - -```sql -id -userName -password -``` - -Application schema: - -```sql -id -userName -firstName -lastName -``` - -While we could certainly just use the raw table schema directly (represented as a JPA-annotated data model) and -reuse it across services, the point is that we may be over-exposing information in areas where we may not want to. -In the case of the _User_ object, it's quite apparent that the application service should never be _capable_ of -accidentally exposing a user's private credentials. By creating isolated views per-service on top of common data -stores, we sacrifice a small bit of [DRY principles](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) for much -better isolation and a more targeted service. Likewise, if the underlying table schema is updated with a new field -that neither one of these services needs, neither service requires a rebuild and redeploy since the change is -irrelevant to their function. - -**A note about microservices:** Another common technique to building microservices is for each service to have its -own set of data stores entirely independent from other services (i.e. no shared overlap); these data stores are then -synced by other services as necessary through a messaging bus. If our system architecture calls for such a model, -it's quite likely we will follow the same pattern we have outlined here with _one key difference_: the underlying -table schema for our _individual service's data store_ will likely be exactly the same as our service's model -representing it. However, overall, the net effect is the same since only the relevant information delivered over the -bus is stored in our service's schema. In fact, this model is arguably more robust in the sense that if one data -store fails not all services necessarily fail. - -Miscellaneous -------------- - -### Supporting Cascading Delete on Data Models - -Cascading delete is handled through JPA annotation which offers possibility to cascade operations (merge, persist, -refresh, remove) to associated entities. Logic is in JPA and does not utilize database cascades. We can use - -```java -@OneToMany(cascade=CascadeType.REMOVE) -``` - -For example: - -```java -@Entity -@Table(name = "author") -@Include(rootLevel = true, name = "author", description = "author data", friendlyName = "author") -public class Author { - - ... - - @OneToMany(cascade = CascadeType.REMOVE) - public List books; -} -``` - -Deleting an author will also delet all of his/her published books in database. diff --git a/docs/docs/crud/elide/datastores/_category_.json b/docs/docs/crud/elide/datastores/_category_.json deleted file mode 100644 index 83ed503c..00000000 --- a/docs/docs/crud/elide/datastores/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Data Stores", - "position": 8, - "link": { - "type": "generated-index" - } -} diff --git a/docs/docs/crud/elide/datastores/index.mdx b/docs/docs/crud/elide/datastores/index.mdx deleted file mode 100644 index 8195a055..00000000 --- a/docs/docs/crud/elide/datastores/index.mdx +++ /dev/null @@ -1,454 +0,0 @@ ---- -sidebar_position: 1 -title: Data Stores -description: A boundary between Webservice and Database ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -A data store is responsible for: - -1. Reading and writing entity models to/from a persistence layer. -2. Providing transactions that make all persistence operations atomic in a single request. -3. Implementing filtering, sorting, and pagination. -4. Declaring the entities it manages persistence for. - -If a data store is unable to fully implement filtering, sorting, or pagination, it can instead rely on the Elide -framework to perform these functions in memory. By default, however, Elide pushes these responsibilities to the store. - -Included Stores ---------------- - -Elide comes bundled with a number of data stores: - -1. Hashmap Data Store - Data is persisted in a hash table on the JVM heap. -2. JPA Data Store - A data store that can map operations on a data model to an underlying relational database (ORM) or - nosql persistence layer (OGM). The JPA Data Store can work with any JPA provider. -3. Multiplex Data Store - A multiplex store that delegates persistence to different underlying stores depending on the - data model. -4. Noop Data Store - A store which does nothing, allowing business logic in computed attributes and life cycle hooks to - entirely implement CRUD operations on the model. -5. [Search Data Store](https://github.com/paion-data/elide/tree/master/elide-datastore/elide-datastore-search) - A store - which provides full text search on text fields while delegating other requests to another provided store. -6. [Aggregation Data Store](analytics) - A store which provides computation of groupable measures (similar to SQL group - by). The aggregation store has custom annotations that map an Elide model to native SQL queries against a JDBC - database. - -The Hashmap Data Store is included as part of `elide-core` while other data stores can be included through the -following artifact dependencies: - -### JPA Data Store - -```xml - - com.paiondata.elide - elide-datastore-jpa - ${elide.version} - -``` - -### Multiplex Data Store - -```xml - - com.paiondata.elide - elide-datastore-multiplex - ${elide.version} - -``` - -### Noop Data Store - -```xml - - com.paiondata.elide - elide-datastore-noop - ${elide.version} - -``` - -### Search Data Store - -```xml - - com.paiondata.elide - elide-datastore-search - ${elide.version} - -``` - -### Aggregation Data Store - -```xml - - com.paiondata.elide - elide-datastore-aggregation - ${elide.version} - -``` - -Overriding the Store --------------------- - -### Overriding in Spring Boot - -[Elide Spring Boot][elide-spring] by default will configure a JPA Data Store with the default transaction manager and -entity manager and manage all the entities associated with the entity manager. - -If not all entities should be managed then this can be customized by using the `@EnableJpaDataStore` annotation. - -```java -@Configuration -@EnableJpaDataStore(managedClasses = { Author.class, Book.class } ) -public class ElideConfiguration { ... } -``` - -To completely override the auto configured store, define a `DataStore` bean: - -```java -@Configuration -public class ElideConfiguration { - @Bean - public DataStore dataStore(EntityManagerFactory entityManagerFactory, PlatformTransactionManager transactionManager, - ElideConfigProperties settings) { - EntityManagerSupplier entityManagerSupplier = new EntityManagerProxySupplier(); - JpaTransactionSupplier jpaTransactionSupplier = new PlatformJpaTransactionSupplier( - new DefaultTransactionDefinition(), transactionManager, - entityManagerFactory, settings.getJpaStore().isDelegateToInMemoryStore()); - return new JpaDataStore(entityManagerSupplier, jpaTransactionSupplier, entityManagerFactory::getMetamodel); - } -} -``` - -### Overriding in Elide Standalone - -[Elide Standalone][elide-standalone] is configured by default with the JPA Data Store. - -To change the store, one of two possible functions should be overridden depending on whether the `AggregationDataStore` -is enabled: - - - - ```java - /** - * Initializes the Elide {@link DataStore} service with the specified {@link EntityManagerFactory}. - * - * @param entityManagerFactory An object used to initialize JPA - * - * @return a new instance - */ - @NotNull - private DataStore buildDataStore(@NotNull final EntityManagerFactory entityManagerFactory) { - return new JpaDataStore( - entityManagerFactory::createEntityManager, - em -> new NonJtaTransaction(em, TXCANCEL), - entityManagerFactory::getMetamodel - ); - } - ``` - - - ```java - /** - * Gets the DataStore for Elide. - * - * @param metaDataStore MetaDataStore object. - * @param aggregationDataStore AggregationDataStore object. - * @param entityManagerFactory EntityManagerFactory object. - * @return DataStore object initialized. - */ - @Override - public DataStore getDataStore(MetaDataStore metaDataStore, AggregationDataStore aggregationDataStore, - EntityManagerFactory entityManagerFactory) { - DataStore jpaDataStore = new JpaDataStore( - () -> { return entityManagerFactory.createEntityManager(); }, - (em) -> { return new NonJtaTransaction(em, ElideStandaloneSettings.TXCANCEL); }); - - DataStore dataStore = new MultiplexManager(jpaDataStore, metaDataStore, aggregationDataStore); - - return dataStore; - } - ``` - - - -Custom Stores -------------- - -Custom stores can be written by implementing the `DataStore` and `DataStoreTransaction` interfaces. - -### Enabling In-Memory Filtering, Sorting, or Pagination - -If a Data Store is unable to fully implement sorting, filtering, or pagination, the Elide framework can perform -these functions in-memory instead. - -The Data Store Transaction can inform Elide of its capabilities (or lack thereof) by returning a `DataStoreIterable` for -any collection loaded: - -```java -/** - * Returns data loaded from a DataStore. Wraps an iterable but also communicates to Elide - * if the framework needs to filter, sort, or paginate the iterable in memory before returning to the client. - * @param The type being iterated over. - */ -public interface DataStoreIterable extends Iterable { - - /** - * Returns the underlying iterable. - * @return The underlying iterable. - */ - Iterable getWrappedIterable(); - - - /** - * Whether the iterable should be filtered in memory. - * @return true if the iterable needs sorting in memory. false otherwise. - */ - default boolean needsInMemoryFilter() { - return false; - } - - /** - * Whether the iterable should be sorted in memory. - * @return true if the iterable needs sorting in memory. false otherwise. - */ - default boolean needsInMemorySort() { - return false; - } - - /** - * Whether the iterable should be paginated in memory. - * @return true if the iterable needs pagination in memory. false otherwise. - */ - default boolean needsInMemoryPagination() { - return false; - } -} - -``` - -Multiple Stores ---------------- - -A common pattern in Elide is the need to support multiple data stores. Typically, one data store manages most models, -but some models may require a different persistence backend or have other needs to specialize the behavior of the store. - -The Multiplex Data Store (`MultiplexManager`) in Elide manages multiple stores - delegating calls to the appropriate -store which is responsible for a particular model. By default it will apply compensating transactions to undo failures -if multiple stores are involved in the multiplex transaction and an error occurs after transactions to some of the -stores were already committed. - -### Spring Boot - -If there are multiple JPA Data Stores required the `@EnableJpaDataStore` annotation can be used to configure them. - -|Annotation Element |Description |Default | -|--------------------------|----------------------------------------------------------------------------------------------------------|----------------------| -|`entityManagerFactoryRef` |(Optional) The bean name of the `EntityManagerFactory` bean to be used. |`entityManagerFactory`| -|`transactionManagerRef` |(Optional) The bean name of the `PlatformTransactionManager` bean to be used. |`transactionManager` | -|`managedClasses` |(Optional) The entities to manage, otherwise all the entities associated with the `EntityManagerFactory`. | | - -Spring Boot will auto configure the default `JpaTransactionManager` or `JtaTransactionManager` with the -`transactionManager` bean name and the `EntityManagerFactory` with the `entityManagerFactory` bean name. - -The following shows sample configuration with 2 `EntityManagerFactory` and 2 `JpaTransactionManager` where each -`EntityManagerFactory` participates in separate transactions: - -```java -@Configuration -@EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactory1", transactionManagerRef = "transactionManager1") -@EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactory2", transactionManagerRef = "transactionManager2") -public class ElideConfiguration { - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory1(EntityManagerFactoryBuilder builder, - DefaultListableBeanFactory beanFactory, DataSource dataSource1) { - Map vendorProperties = new HashMap<>(); - vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); - vendorProperties.put(AvailableSettings.JTA_PLATFORM, new NoJtaPlatform()); - final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSource1) - .packages("example.models.jpa.v1").properties(vendorProperties).build(); - return emf; - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory2(EntityManagerFactoryBuilder builder, - DefaultListableBeanFactory beanFactory, DataSource dataSource2) { - Map vendorProperties = new HashMap<>(); - vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); - vendorProperties.put(AvailableSettings.JTA_PLATFORM, new NoJtaPlatform()); - final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSource2) - .packages("example.models.jpa.v2").properties(vendorProperties).build(); - return emf; - } - - @Bean - public PlatformTransactionManager transactionManager1(EntityManagerFactory entityManagerFactory1) { - return new JpaTransactionManager(entityManagerFactory1); - } - - @Bean - public PlatformTransactionManager transactionManager2(EntityManagerFactory entityManagerFactory2) { - return new JpaTransactionManager(entityManagerFactory2); - } - - @Bean - public DataSource dataSource1() { - return DataSourceBuilder.create().url("jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1").username("sa").password("").build(); - } - - @Bean - public DataSource dataSource2() { - return DataSourceBuilder.create().url("jdbc:h2:mem:db2;DB_CLOSE_DELAY=-1").username("sa").password("").build(); - } - - @Bean - public EntityManagerFactoryBuilder entityManagerFactoryBuilder( - ObjectProvider persistenceUnitManager, - ObjectProvider customizers) { - EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), - new HashMap<>(), persistenceUnitManager.getIfAvailable()); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return builder; - } -} -``` - -The following shows sample configuration with 2 `EntityManagerFactory` and a `JtaTransactionManager` where both -`EntityManagerFactory` participates in a single transaction: - -```java -@Configuration -@EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactory1") -@EnableJpaDataStore(entityManagerFactoryRef = "entityManagerFactory2") -public class ElideConfiguration { - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory1(EntityManagerFactoryBuilder builder, - DefaultListableBeanFactory beanFactory, DataSource dataSource1, JtaTransactionManager transactionManager) { - Map vendorProperties = new HashMap<>(); - vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); - vendorProperties.put(AvailableSettings.JTA_PLATFORM, new SpringJtaPlatform(transactionManager)); - final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSource1) - .packages("example.models.jpa.v1").properties(vendorProperties).jta(true).build(); - return emf; - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory2(EntityManagerFactoryBuilder builder, - DefaultListableBeanFactory beanFactory, DataSource dataSource2, JtaTransactionManager transactionManager) { - Map vendorProperties = new HashMap<>(); - vendorProperties.put(AvailableSettings.HBM2DDL_AUTO, "create-drop"); - vendorProperties.put(AvailableSettings.JTA_PLATFORM, new SpringJtaPlatform(transactionManager)); - final LocalContainerEntityManagerFactoryBean emf = builder.dataSource(dataSource2) - .packages("example.models.jpa.v2").properties(vendorProperties).jta(true).build(); - return emf; - } - - @Bean - public DataSource dataSource1() { - XADataSource xaDataSource = DataSourceBuilder.create().url("jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1") - .driverClassName("org.h2.Driver").type(org.h2.jdbcx.JdbcDataSource.class).username("sa") - .password("").build(); - AtomikosDataSourceBean atomikosDataSource = new AtomikosDataSourceBean(); - atomikosDataSource.setXaDataSource(xaDataSource); - return atomikosDataSource; - } - - @Bean - public DataSource dataSource2() { - XADataSource xaDataSource = DataSourceBuilder.create().url("jdbc:h2:mem:db2;DB_CLOSE_DELAY=-1") - .driverClassName("org.h2.Driver").type(org.h2.jdbcx.JdbcDataSource.class).username("sa") - .password("").build(); - AtomikosDataSourceBean atomikosDataSource = new AtomikosDataSourceBean(); - atomikosDataSource.setXaDataSource(xaDataSource); - return atomikosDataSource; - } - - @Bean - public EntityManagerFactoryBuilder entityManagerFactoryBuilder( - ObjectProvider persistenceUnitManager, - ObjectProvider customizers) { - EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), - new HashMap<>(), persistenceUnitManager.getIfAvailable()); - customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return builder; - } -} -``` - -If customizations are required to the `MultiplexManager` used or to add other data stores the -`DataStoreBuilderCustomizer` can be used: - -```java -@Configuration -public class ElideConfiguration { - @Bean - public DataStoreBuilderCustomizer dataStoreBuilderCustomizer() { - return builder -> { - builder - .dataStore(new MyCustomDataStore()) - .multiplexer(dataStores -> { - return new MultiplexManager(ObjectCloners::clone, - dataStore -> !(dataStore instanceof JpaDataStore), dataStores); - }); - }; - } -} -``` - -To completely override the auto configured store and setup the Multiplex Data Store, define a `DataStore` bean: - -```java -@Configuration -public class ElideConfiguration { - @Bean - public DataStore dataStore(EntityManagerFactory entityManagerFactory, PlatformTransactionManager transactionManager, - ElideConfigProperties settings) { - EntityManagerSupplier entityManagerSupplier = new EntityManagerProxySupplier(); - JpaTransactionSupplier jpaTransactionSupplier = new PlatformJpaTransactionSupplier( - new DefaultTransactionDefinition(), transactionManager, - entityManagerFactory, settings.getJpaStore().isDelegateToInMemoryStore()); - //Store 1 manages Book, Author, and Publisher - DataStore store1 = new JpaDataStore(entityManagerSupplier, jpaTransactionSupplier, - ClassType.of(Book.class), - ClassType.of(Author.class), - ClassType.of(Publisher.class)); - - //Store 2 is a custom store that manages Manufacturer - DataStore store2 = new MyCustomDataStore(...); - - //Return the new multiplex store... - return new MultiplexManager(store1, store2); - } -} -``` - -### Elide Standalone - -To set up the Multiplex Data Store, override the `getDataStore` function: - -```java -/** - * Gets the DataStore for elide when aggregation store is disabled. - * @param entityManagerFactory EntityManagerFactory object. - * @return DataStore object initialized. - */ -@Override -public DataStore getDataStore(EntityManagerFactory entityManagerFactory) { - //Store 1 manages Book, Author, and Publisher - DataStore store1 = new JpaDataStore( - () -> { return entityManagerFactory.createEntityManager(); }, - (em) -> { return new NonJtaTransaction(em, ElideStandaloneSettings.TXCANCEL); }, - Book.class, Author.class, Publisher.class - ); - - //Store 2 is a custom store that manages Manufacturer - DataStore store2 = new MyCustomDataStore(...); - - //Create the new multiplex store... - return new MultiplexManager(store1, store2); -} -``` - -[elide-spring]: https://github.com/paion-data/elide/tree/master/elide-spring/elide-spring-boot-autoconfigure -[elide-standalone]: https://github.com/paion-data/elide/tree/master/elide-standalone diff --git a/docs/docs/crud/elide/datastores/search-datastore.md b/docs/docs/crud/elide/datastores/search-datastore.md deleted file mode 100644 index 9f06bc09..00000000 --- a/docs/docs/crud/elide/datastores/search-datastore.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -sidebar_position: 2 -title: Search Data Store ---- - -Overview --------- - -Search Data Store provides full text search for Elide. - -### Requirements - -This store leverages [Hibernate Search](https://hibernate.org/search/) which requires Hibernate 6+. - -### Usage - -`SearchDataStore` wraps another fully featured store and supports full text search on fields that are indexed using -Hibernate Search. If the query cannot be answered by the `SearchDataStore`, it delegates the query to the underlying -(wrapped) data store. - -#### Annotating Entity - -Use Hibernate Search annotations to describe how your entities are indexed and stored in Lucene or Elasticsearch. Some -of the annotations (like `AnalyzerDef`) can be defined once at the package level if desired. - -```java -@Entity -@Include -@Indexed -@Data // Lombok -public class Item { - - @Id - private long id; - - @FullTextField( - name = "name", - searchable = Searchable.YES, - projectable = Projectable.NO, - analyzer = "case_insensitive" - ) - @KeywordField(name = "sortName", sortable = Sortable.YES, projectable = Projectable.NO, searchable = Searchable.YES) - private String name; - - @FullTextField(searchable = Searchable.YES, projectable = Projectable.NO, analyzer = "case_insensitive") - private String description; - - @GenericField(searchable = Searchable.YES, projectable = Projectable.NO, sortable = Sortable.YES) - private Date modifiedDate; - - private BigDecimal price; -} -``` - -#### (Optional) Defining a Custom Analyzer - -The `Item` entity above references a non-standard analyzer - `case_insensitive`. This analyzer needs to be -programmatically created: - -```java -public class MyLuceneAnalysisConfigurer implements LuceneAnalysisConfigurer { - - @Override - public void configure(LuceneAnalysisConfigurationContext ctx) { - ctx.analyzer("case_insensitive") - .custom() - .tokenizer(NGramTokenizerFactory.class) - .param("minGramSize", "3") - .param("maxGramSize", "50") - .tokenFilter(LowerCaseFilterFactory.class); - } -} -``` - -and then configured by setting the property `hibernate.search.backend.analysis.configurer` to the new analyzer. - -```xml - - - com.paiondata.elide.datastores.search.models.Item - - - - - ... - - - -``` - -#### Wrapping DataStore - -```java -/* Create your JPA data store */ -DataStore store = ... - -/* Wrap it with a SearchDataStore */ -EntityManagerFactory emf = ... - -boolean indexOnStartup = true; //Create a fresh index when the server starts -searchStore = new SearchDataStore(store, emf, indexOnStartup); - -/* Configure Elide with your store */ -ElideSettings = new ElideSettingsBuidler(searchStore).build(); -``` - -#### Indexing Data - -We can index data either by: - -1. When the `SearchDataStore` is initialized, indicate (by setting `indexOnStartup` to `true`) that the search store - should build a complete index. -2. Issuing created, updated, and delete requests against our Elide service. -3. Using an out of band process using Hibernate Search APIs. - -### Caveats - -#### Data Type Support - -Only text fields (String) are supported/tested. Other data types (dates, numbers, etc) have not been tested. Embedded -index support has not been implemented. - -#### Filter Operators - -Only INFIX, and PREFIX filter operators (and their case insensitive equivalents) are supported. Note that hibernate -search only indexes and analyzes fields as either case sensitive or not case-sensitive - so a given field will only -support the INFIX/PREFIX filter operator that matches how the field was indexed. - -All other filter operators are passed to the underlying wrapped JPA store. - -#### Analyzer Assumptions - -##### Index Analysis - -To implement correct behavior for Elide's INFIX and PREFIX operators, the search store assumes an ngram (non-edge) -tokenizer is used. This allows white spaces and punctuation to be included in the index. - -If the client provides a filter predicate with a term which is smaller or larger than the min/max ngram sizes -respectively, it will not be found in the index. - -The search store can be configured to return a 400 error to the client in those scenarios by passing the minimum and -maximum ngram size to the constructor of the `SearchDataStore`. The sizes are global and apply to all Elide entities -managed by the store instance: - -```java -new SearchDataStore(jpaStore, emf, true, 3, 50); -``` - -##### Search Term Analysis - -Elide creates a Hibernate Search `SimpleQueryString` for each predicate. It first escapes white space and punctuation -in any user provided input (to match Elide's default behavior when not using the `SearchDataStore`). The resulting -single token is used to construct a prefix query. - -##### Sorting and Pagination - -When using the INFIX operator, sorting and pagination are pushed to down Lucene/ElasticSearch. When using the PREFIX -operator, they are performed in-memory in the Elide service. - -Elide constructs a Prefix query, which together with an ngram index fully implements the INFIX operator. However, the -ngram analyzer adds ngrams to the index that do not start on word boundaries. For the prefix operator, the search -store first performs the lucene filter and then filters again in-memory to return the correct set of matching terms. - -In this instance, because filtering is performed partially in memory, Elide also sorts and paginates in memory as well. diff --git a/docs/docs/crud/elide/design.md b/docs/docs/crud/elide/design.md deleted file mode 100644 index fdf14046..00000000 --- a/docs/docs/crud/elide/design.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -sidebar_position: 15 -title: Design -description: Design Overview ---- - -Overview --------- - -The following guide is intended for developers who want to make changes to the Elide framework. It will cover the design -of various subsystems. - -Module Layout -------------- - -Elide is a mono-repo consisting of the following published modules: - -| Module Name | Description | -|---------------------------------|----------------------------------------------------------------------------------| -| elide-core | Contains modeling annotations, JSON-API parser, and core functions. | -| elide-graphql | Contains the GraphQL parser. | -| elide-async | Contains Elide's asynchronous and data export APIs. | -| elide-swagger | Contains OpenAPI document generation for JSON-API. | -| elide-standalone | Opinionated embedded Jetty application with JAX-RS endpoints for Elide | -| elide-spring | Parent module for spring boot support | -| elide-spring-boot-autoconfigure | Elide spring boot auto configuration module | -| elide-spring-boot-starter | Elide spring boot starter pom | -| elide-test | JSON-API and GraphQL test DSLs for [Rest Assured Testing Framework](https://rest-assured.io/) | -| elide-integration-tests | Integration tests that are run for JPA, Hibernate, and In-Memory data stores. | -| elide-model-config | HJSON Configuration language for the Aggregation store semantic layer. | -| elide-datastore | Parent module for all data stores. | -| elide-datastore-aggregation | Datastore and semantic layer for processing analytic API queries. | -| elide-datastore-hibernate | Parent package for all hibernate and JPA stores. | -| elide-datastore-hibernate3 | Legacy data store for Hibernate 3 support. | -| elide-datastore-hibernate5 | Legacy data store for Hibernate 5 support. | -| elide-datastore-jpa | Primary data store for elide CRUD API queries. Replaces legacy hibernate stores. | -| elide-datastore-mulitplex | Support for multiple data stores. | -| elide-datastore-noop | Zero persistence store. This is used for implementing simple POST functions. | -| elide-datastore-search | Indexed text search store. It must be used in conjunction with the JPA store. | -| elide-datastore-inmemorydb | Hashmap implementation of a datastore. | - -High Level Design ------------------ - -The following diagram represents a high level component breakout of Elide. Names in italics represent class names -whereas other names represent functional blocks (made up of many classes). Gray arrows represent client request and -response flow through the system. - -![High Level Design](img/high-level-design.png) - -Elide can be broken down into the following layers: - -### Parsing Layer - -The parsing layer consists of a JSON-API parser and GraphQL parser. This layer is responsible for mapping a client -request in JSON-API or GraphQL into [Elide's internal request model](#client-request-model). The parsers load, create, -and manipulate Elide models via the `PersistentResource`. - -### Business Logic Layer - -The business logic layer is responsible for performing: - -- Authorization checks -- Lifecycle hooks -- Audit & Logging - -All elide models (once loaded or created) are wrapped in a `PersistentResource`. All attribute and relationship access -(read & write) occur through this abstraction allowing a central place to enforce business rules. - -In addition to invoking security checks and lifecycle hooks, the `PersistentResource` is also responsible for reading -and writing the model and its fields to the persistence layer. - -### Persistence Layer - -The persistence layer consists of two abstractions and their concrete implementations: - -- A `DataStore` which is responsible for telling Elide which models it manages and creating `DataStoreTransaction` - objects. -- A `DataStoreTransaction` which is created per request and is responsible for saving, loading, and deleting Elide - models. Each request's interactions with the persistence layer should occur atomically. - -Elide comes bundled with a number of `DataStore` [implementations](datastores). The most notable are the JPA, Search, -and Aggregation stores. - -### Client Request Model - -The primary object in the client request model is the `EntityProjection`. It represents the entire model graph -requested by the client. The entity projection consists of `Attribute` objects (model fields), `Relationship` objects -(named entity projections), and also whether the projection should be filtered, sorted, or paginated. `Attribute` -objects can take `Argument` objects as parameters. - -### Metadata and Configuration - -Elide is configured either with Spring Boot or the Elide Standalone module. Application settings for Spring and -Standalone are mapped to an internal `ElideSettings` object that configures the Elide framework (denoted by the `Elide` -object). - -All static metadata about Elide models is extracted at service boot and stored in the `EntityDictionary`. This class is -used throughout Elide whenever a model must be read from or written to by the `PersistentResource`. - -While earlier versions of Elide represented models as JVM classes, Elide 5.x introduced its own `Type` system. This -allows Elide to register and use dynamic models that are not JVM classes. - -### Modeling - -CRUD models in Elide are created from JVM classes whereas analytic models are created either from JVM classes or HJSON -configuration files. In either case, Elide annotations are used to add the metadata Elide needs to perform persistence -and business rules. All Elide annotations are documented here - -## Security Subsystem - -Coming Soon. - -## Analytic Query Subsystem - -Coming Soon. diff --git a/docs/docs/crud/elide/development.md b/docs/docs/crud/elide/development.md deleted file mode 100644 index b0ceb4aa..00000000 --- a/docs/docs/crud/elide/development.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -sidebar_position: 14 -title: Development ---- - -Overview --------- - -The following guide is intended to help developers who maintain or want to make changes to the Elide framework. - -### Building - -Elide is built using maven. Because elide is a mono-repo with interdependencies between modules, it is recommended to -fully build and install the project at least once: - -```console -mvn clean install -``` - -Thereafter, individual modules can be built whenever making changes to them. For example, the following command would -rebuild only elide-core: - -```console -mvn clean install -f elide-core -``` - -Pull requests and release builds leverage [GitHub Actions](https://docs.github.com/en/actions). PR builds simply run the -complete build along with code coverage: - -```console -mvn -B install -DrepoToken=${{ secrets.COVERALLS_REPO_TOKEN }} -``` - -### Release Versions - -Elide follows [semantic versioning](https://semver.org/) for its releases. Minor and patch versions only have the -following version components: - -`MAJOR.MINOR.PATCH`. - -Integration Tests ------------------ - -The `elide-integration-tests` module runs API tests against an embedded Jetty application with an H2 database for -persistence. Integration tests are run for the JPA, hibernate, and inmemory stores. The module produce a 'test-jar' -artifact that is then referenced for each data store module (jpa, hibernate, etc) that runs the corresponding tests. - -Not every tests works for every store, and JUnit tags are leveraged to isolate the tests appropriate for each target. - -When run in an IDE, the inmemory store is leveraged. To tests against a different data store, the IDE must be configured -to: - -1. Set a property that selects the - [DataStoreTestHarness](https://github.com/paion-data/elide/blob/master/elide-core/src/main/java/com/paiondata/elide/core/datastore/test/DataStoreTestHarness.java) - which in turn initializes the data store to test. -2. Sets the classpath appropriately to the data store submodule that is being tested. - -The following screenshot demonstrates configuring these two settings for the 'FilterIT' tests in IntelliJ: - -![Configuring IT Tests In Intellij](img/intellij-config.png) diff --git a/docs/docs/crud/elide/elide-standalone.mdx b/docs/docs/crud/elide/elide-standalone.mdx deleted file mode 100644 index cef49079..00000000 --- a/docs/docs/crud/elide/elide-standalone.mdx +++ /dev/null @@ -1,423 +0,0 @@ ---- -sidebar_position: 2 -title: Elide Standalone ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Overview --------- - -The Elide standalone application is a configurable web server using Elide. While Elide is typically a pluggable -**middleware** framework, we have constructed a flexible and complete service to allow our users to get started quickly. - -The Elide standalone application takes an opinionated stance on its technology stack (i.e. jersey/jetty), but provides -many opportunities for users to configure the behavior of their application. To use the Elide standalone application, -there are only a few steps: - -1. Configure ElideStandalone by implementing a BinderFactory & ResourceConfig. -2. Build an uber jar containing `elide-standalone`, our models, security checks, and additional application - configurations. -3. Start your web service as WAR - -Getting Started ---------------- - -Elide models are some of the most important code in any Elide project. Our models are the view of our data that we -wish to expose. In this example we will be modeling a software artifact repository since most developers have a -high-level familiarity with artifact repositories such as Maven, Artifactory, npm, and the like. - -There will 2 kinds of models: - -1. **Models that we intend to both read & write**. These models are created by defining Java classes. For this - example, that includes `ArtifactGroup`, `ArtifactProduct`, and `ArtifactVersion`. For brevity we will omit package - names and import statements. -2. **Read-only models** that we intend to run analytic queries against. These models can be created with Java classes - or with a HJSON configuration language. For this example, we will use the latter to create a `Downloads` model. - - - - - ```java - @Include(rootLevel = true, name = "group") - @Entity - public class ArtifactGroup { - @Id - private String name = ""; - - private String commonName = ""; - - private String description = ""; - - @OneToMany(mappedBy = "group") - private List products = new ArrayList<>(); - } - ``` - - - - - ```java - @Include(name = "product") - @Entity - public class ArtifactProduct { - @Id - private String name = ""; - - private String commonName = ""; - - private String description = ""; - - @ManyToOne - private ArtifactGroup group = null; - - @OneToMany(mappedBy = "artifact") - private List versions = new ArrayList<>(); - } - ``` - - - - - ```java - @Include(name = "version") - @Entity - public class ArtifactVersion { - @Id - private String name = ""; - - private Date createdAt = new Date(); - - @ManyToOne - private ArtifactProduct artifact; - } - ``` - - - - - ```hjson - { - tables: [ - { - name: Downloads - table: downloads - description: - ''' - Analytics for artifact downloads. - ''' - joins: [ - { - name: artifactGroup - to: group - kind: toOne - type: left - definition: '{{group_id}} = {{artifactGroup.name}}' - }, - { - name: artifactProduct - to: product - kind: toOne - definition: '{{product_id}} = {{artifactProduct.name}}' - } - ] - dimensions: [ - { - name: group - type: TEXT - definition: '{{artifactGroup.name}}' - } - { - name: product - type: TEXT - definition: '{{artifactProduct.name}}' - } - { - name: date - type: TIME - definition: '{{date}}' - grains: [{ - type: DAY - }] - } - ] - measures: [ - { - name: downloads - type: INTEGER - definition: 'SUM({{downloads}})' - } - ] - } - ] - } - ``` - - - - -Spinning up the API -------------------- - -Running Elide standalone is pretty straight forward by essentially implementing a **Binder** and a **ResourceConfig**: - -:::danger - -The `hibernate.hbm2ddl.auto` in this example is set to `create`, which means each run of the standalone will erase the -database and re-create tables. Please do change this value if used in production - -::: - -[In order for dependency injection to work properly in Elide standalone](https://github.com/QubitPi/jersey-webservice-template/pull/29/files), -we must use [Jersey binder](https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/ioc.html#d0e17933), -not [HK binder](https://javaee.github.io/hk2/) - -```java - -import org.glassfish.jersey.internal.inject.AbstractBinder; -import org.glassfish.jersey.internal.inject.Binder; - -/** - * A binder factory builds a custom binder for the Jersey application. - *

- * The factory makes the component object instance that will eventually be passed to - * {@link org.glassfish.jersey.server.ResourceConfig#register(Object)}. - */ -@Immutable -@ThreadSafe -public class BinderFactory { - - /** - * Builds a hk2 Binder instance. - *

- * This binder should bind all relevant resources for runtime dependency injection. - * - * @param injector A standard HK2 service locator - * - * @return a binder instance that will be registered by putting as a parameter to - * {@link org.glassfish.jersey.server.ResourceConfig#register(Object)} - */ - @NotNull - public Binder buildBinder(final ServiceLocator injector) { - return new AbstractBinder() { - - private static final Consumer TXCANCEL = em -> em.unwrap(Session.class).cancelQuery(); - - private final ClassScanner classScanner = new DefaultClassScanner(); - - @Override - protected void configure() { - final ElideSettings elideSettings = buildElideSettings(); - - bind(buildElide(elideSettings)).to(Elide.class).named("elide"); - bind(elideSettings).to(ElideSettings.class); - bind(elideSettings.getDictionary()).to(EntityDictionary.class); - bind(elideSettings.getDataStore()).to(DataStore.class).named("elideDataStore"); - } - - /** - * Initializes Elide middleware service. - * - * @param elideSettings An object for configuring various aspect of the Elide middleware - * - * @return a new instance - */ - @NotNull - private Elide buildElide(@NotNull final ElideSettings elideSettings) { - return new Elide( - elideSettings, - new TransactionRegistry(), - elideSettings.getDictionary().getScanner(), - false - ); - } - - /** - * Initializes Elide config object. - * - * @return a new instance - */ - @NotNull - private ElideSettings buildElideSettings() { - return new ElideSettingsBuilder(buildDataStore(buildEntityManagerFactory())) - .withEntityDictionary(buildEntityDictionary(injector)) - .build(); - } - - /** - * Initializes the Elide {@link DataStore} service with the specified {@link EntityManagerFactory}. - * - * @param entityManagerFactory An object used to initialize JPA - * - * @return a new instance - */ - @NotNull - private DataStore buildDataStore(@NotNull final EntityManagerFactory entityManagerFactory) { - return new JpaDataStore( - entityManagerFactory::createEntityManager, - em -> new NonJtaTransaction(em, TXCANCEL), - entityManagerFactory::getMetamodel - ); - } - - /** - * Initializes the {@link EntityManagerFactory} service used by Elide JPA. - * - * @return a new instance - */ - @NotNull - private EntityManagerFactory buildEntityManagerFactory() { - final String modelPackageName = "com.mycompany.mymodel"; - - final ClassLoader classLoader = null; - - final PersistenceUnitInfo persistenceUnitInfo = new PersistenceUnitInfoImpl( - "my-webservice", - getAllEntities(classScanner, modelPackageName), - getDefaultDbConfigs(), - classLoader - ); - - return new EntityManagerFactoryBuilderImpl( - new PersistenceUnitInfoDescriptor(persistenceUnitInfo), - new HashMap<>(), - classLoader - ).build(); - } - - /** - * Get all the entities in a package. - * - * @param scanner An object that picks up entities by Elide annotation - * @param packageName A fully qualified package name under which contains all entities - * - * @return all entities found in the provided package. - */ - @NotNull - public static List getAllEntities( - @NotNull final ClassScanner scanner, - @NotNull final String packageName - ) { - return scanner.getAnnotatedClasses(packageName, Entity.class).stream() - .map(Class::getName) - .collect(Collectors.toList()); - } - - /** - * Returns a collection of DB configurations, including connecting credentials. - *

- * In addition, the configurations consumes all configs defined in {@link JpaDatastoreConfig} - * - * @return a new instance - */ - @NotNull - @SuppressWarnings("MultipleStringLiterals") - private static Properties getDefaultDbConfigs() { - final Properties dbProperties = new Properties(); - - dbProperties.put("hibernate.show_sql", "true"); - dbProperties.put("hibernate.hbm2ddl.auto", "create"); - dbProperties.put("hibernate.dialect", "org.hibernate.dialect.MySQLDialect"); - dbProperties.put("hibernate.current_session_context_class", "thread"); - dbProperties.put("hibernate.jdbc.use_scrollable_resultset", "true"); - - // Collection Proxy & JDBC Batching - dbProperties.put("hibernate.jdbc.batch_size", "50"); - dbProperties.put("hibernate.jdbc.fetch_size", "50"); - dbProperties.put("hibernate.default_batch_fetch_size", "100"); - - // Hikari Connection Pool Settings - dbProperties.putIfAbsent("hibernate.connection.provider_class", - "com.zaxxer.hikari.hibernate.HikariConnectionProvider"); - dbProperties.putIfAbsent("hibernate.hikari.connectionTimeout", "20000"); - dbProperties.putIfAbsent("hibernate.hikari.maximumPoolSize", "30"); - dbProperties.putIfAbsent("hibernate.hikari.idleTimeout", "30000"); - - dbProperties.put("jakarta.persistence.jdbc.driver", "com.mysql.cj.jdbc.Driver"); - dbProperties.put("jakarta.persistence.jdbc.url", "jdbc:mysql://db/minerva?serverTimezone=UTC"); - dbProperties.put("jakarta.persistence.jdbc.user", "root"); - dbProperties.put("jakarta.persistence.jdbc.password", "root"); - - return dbProperties; - } - - /** - * Initializes the Elide {@link EntityDictionary} service with a given dependency injector. - * - * @param injector A standard HK2 service locator used by Elide - * - * @return a new instance - */ - @NotNull - private EntityDictionary buildEntityDictionary(@NotNull final ServiceLocator injector) { - return new EntityDictionary( - new HashMap<>(), - new HashMap<>(), - new Injector() { - @Override - public void inject(final Object entity) { - injector.inject(entity); - } - - @Override - public T instantiate(final Class cls) { - return injector.create(cls); - } - }, - CoerceUtil::lookup, - new HashSet<>(), - classScanner - ); - } - }; - } -} -``` - -```java -/** - * The resource configuration for the web applications. - */ -@Immutable -@ThreadSafe -@ApplicationPath("/v1/data/") -public class ResourceConfig extends org.glassfish.jersey.server.ResourceConfig { - - private static final String GRAPHQL_ENDPOINT_PACKAGE = "com.paiondata.elide.graphql"; - private static final String JAON_API_ENDPOINT_PACKAGE = "com.paiondata.elide.jsonapi.resources"; - - /** - * DI Constructor. - * - * @param injector A standard HK2 service locator - */ - @Inject - public ResourceConfig(@NotNull final ServiceLocator injector) { - this(injector, new BinderFactory()); - } - - /** - * Constructor that allows for finer dependency injection control. - * - * @param injector A standard HK2 service locator - * @param binderFactory An object that produces resource binder - */ - private ResourceConfig(@NotNull final ServiceLocator injector, @NotNull final BinderFactory binderFactory) { - packages(JAON_API_ENDPOINT_PACKAGE, GRAPHQL_ENDPOINT_PACKAGE); - - register(binderFactory.buildBinder(injector)); - - // Bind api docs to given endpoint - // This looks strange, but Jersey binds its Abstract binders first, and then later it binds 'external' - // binders (like this HK2 version). This allows breaking dependency injection into two phases. - // Everything bound in the first phase can be accessed in the second phase. - register(new org.glassfish.hk2.utilities.binding.AbstractBinder() { - @Override - protected void configure() { - injector.getService(Elide.class, "elide").doScans(); - } - }); - } -} -``` diff --git a/docs/docs/crud/elide/features.md b/docs/docs/crud/elide/features.md deleted file mode 100644 index 6ac8660e..00000000 --- a/docs/docs/crud/elide/features.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -sidebar_position: 3 -title: Features -description: An overview of Elide features and links to their respective documentations. ---- - -The following guide provides an overview of Elide features and links to their respective documentation. - - - -Common API Features -------------------- - -- **Rich Filter Support** - Support for complex filter predicates including conjunction (logical and), disjunction - (logical OR) and parenthetic expressions for both [GraphQL](graphql#filtering) and [JSON-API](jsonapi#filtering). - Support for filtering models on fields nested in other models (relationship traversal) or attribute object - hierarchies (complex attribute types). -- **Collection Sorting** - Sort collections by one or more fields in the current or related models in - [GraphQL](graphql#sorting) and [JSON-API](jsonapi#sorting). -- **Pagination** - Support to paginate collections and request the total number of pages or records in - [GraphQL](graphql#pagination) and [JSON-API](jsonapi#pagination). -- **Type Coercion** - Support to [type coerce](clientapis#type-coercion) fields between the API representation and - the model representation by registering one's own custom data type serializers/deserializers. -- **Synchronous or Asynchronous API** - Elide supports both synchronous and asynchronous APIs for short and long-running - queries. - -Data Modeling Features ----------------------- - -- **Lifecycle Hooks** - Register [custom functions & business logic](data-model#lifecycle-hooks) that get invoked - whenever our data model is read or manipulated. -- **Security** - Assign [permission rules](security) to fields and entities in our data model using a custom security - DSL. Bind rules to in-memory functions or security filters that are pushed to the persistence layer. -- **Computed Attributes & Relationships** - Define [custom fields and relationships](data-model#computed-attributes) - that are computed at query time. -- **API Versioning** - [Version our models](data-model#api-versions) to support schema evolution without breaking our - client contracts. -- **Composite Identifiers** - Support both simple and complex [model identifiers](data-model#model-identifiers) - including compound types. - -JSON-API Features ------------------ - -- **OpenAPI** - Elide can automatically generate [OpenAPI documentation](openapi) for Elide APIS for schema - introspection. -- **Test DSL** - Elide includes a [test DSL](test) that works with [Rest Assured](https://rest-assured.io/) for writing - readable integration tests. - -GraphQL Features ----------------- - -- **GraphQL Schemas** - The GraphQL specification includes type introspection that integrates with tools like [Graphiql](https://github.com/graphql/graphiql). -- **Subscription Suport** - Elide supports [model driven subscriptions](subscriptions) backed by any JMS message broker - that can be consumed over websockets. -- **Test DSL** - Elide includes a [test DSL](test) that works with [Rest Assured](https://rest-assured.io/) for - writing readable integration tests. - -Persistence Features --------------------- - -- **JPA Store** - The [JPA store](datastores#jpa-store) persists Elide models decorated with JPA annotations. -- **In-Memory Store** - The [in-memory store](datastores#in-memory-store) persists Elide models locally in the server's - memory. -- **Search Store** - The [search store](datastores#search-store) provides full text search on annotated fields in Elide - models. It works in conjunction with the JPA store. -- **Multiple Stores** - Elide services can be configured with [multiple data stores](datastores#multiple-stores) - each - managing a different set of models. -- **Custom Stores** - Elide can be extended to talk to web services or other persistence layers by writing - [custom stores](datastores#custom-stores). -- **Server Side Filtering, Sorting, & Pagination** - For custom stores that cannot filter, sort, or paginate natively, - Elide can optionally perform these functions on the server. - -Analytic Features ------------------ - -- **Analytic Query Support** - Elide's [aggregation store](analytics) exposes read-only models that support data - analytic queries. Model attributes represent either metrics (for aggregating, filtering, and sorting) and - dimensions (for grouping, filtering, and sorting). -- **Virtual Semantic Layer** - Analytic models are configured with a - [semantic modeling language](analytics.html#model-configuration) that allows non-developers the ability to define - metrics and dimensions by writing templated native SQL fragments. The fragments are assembled into complete SQL - statements at query time. -- **Caching** - The aggregation store includes a [customizable cache](performance#aggregationdatastore-cache). The - cache supports time and version based strategies for expunging stale data. -- **Async API** - Elide includes an asynchronous API for long-running queries on analytic models. -- **Data Export** - Elide includes a data export API for streaming large query results in JSON or CSV formats. - -Operability Features --------------------- - -- **Logging** - Elide supports rich [native logging](audit) for query generation and security rules. -- **Spring Integration** - Elide integrates with [Spring Boot](https://spring.io/projects/spring-boot) including an - [example project](https://github.com/yahoo/elide-spring-boot-example) and - [starter package](https://github.com/paion-data/elide/tree/master/elide-spring). -- **Elide Standalone Integration** - Elide includes - [JAX-RS](https://download.oracle.com/otndocs/jcp/jaxrs-2_0-fr-eval-spec/index.html) endpoints and can run as a - [standalone](https://github.com/yahoo/elide-standalone-example) embedded Jetty service. -- **Java 17** - Elide compiles with Java 17. diff --git a/docs/docs/crud/elide/img/check-tree.png b/docs/docs/crud/elide/img/check-tree.png deleted file mode 100644 index 6cdce621..00000000 Binary files a/docs/docs/crud/elide/img/check-tree.png and /dev/null differ diff --git a/docs/docs/crud/elide/img/high-level-design.png b/docs/docs/crud/elide/img/high-level-design.png deleted file mode 100644 index 40ab18ad..00000000 Binary files a/docs/docs/crud/elide/img/high-level-design.png and /dev/null differ diff --git a/docs/docs/crud/elide/img/intellij-config.png b/docs/docs/crud/elide/img/intellij-config.png deleted file mode 100644 index 91bbde4d..00000000 Binary files a/docs/docs/crud/elide/img/intellij-config.png and /dev/null differ diff --git a/docs/docs/crud/elide/img/resource-binding.drawio b/docs/docs/crud/elide/img/resource-binding.drawio deleted file mode 100644 index a2874757..00000000 --- a/docs/docs/crud/elide/img/resource-binding.drawio +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/docs/crud/elide/img/resource-binding.png b/docs/docs/crud/elide/img/resource-binding.png deleted file mode 100644 index 3273077a..00000000 Binary files a/docs/docs/crud/elide/img/resource-binding.png and /dev/null differ diff --git a/docs/docs/crud/elide/img/security-article-comment-uml.png b/docs/docs/crud/elide/img/security-article-comment-uml.png deleted file mode 100644 index de1bd707..00000000 Binary files a/docs/docs/crud/elide/img/security-article-comment-uml.png and /dev/null differ diff --git a/docs/docs/crud/elide/intro.mdx b/docs/docs/crud/elide/intro.mdx deleted file mode 100644 index 91433b35..00000000 --- a/docs/docs/crud/elide/intro.mdx +++ /dev/null @@ -1,613 +0,0 @@ ---- -sidebar_position: 1 -title: Getting Started ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -So You Want An API? -------------------- - -The easiest way to get started with Elide is to use the [Spring Boot starter dependency]. The starter bundles all of -the dependencies we will need to stand up a web service. This tutorial uses the starter, and all of the code is -[available here][elide-demo]. - -Don't like Spring/Spring Boot? - check out the same getting starting guide using Jetty/Jersey and -[Elide standalone](elide-standalone). - -Adding Elide as a Dependency ----------------------------- - -To include elide into our spring project, add the single starter dependency: - -```xml - - com.paiondata.elide - elide-spring-boot-starter - ${elide.version} - -``` - -Creating Models ---------------- - -**Elide models** are some of the most important code in any Elide project. Our models are the view of our data that we -wish to expose. In this example we will be modeling a software artifact repository since most developers have a -high-level familiarity with artifact repositories such as Maven, Artifactory, npm, and the like. - -There will 2 kinds of models: - -1. **Models that we intend to both read & write**. These models are created by defining Java classes. For this - example, that includes `ArtifactGroup`, `ArtifactProduct`, and `ArtifactVersion`. For brevity we will omit package - names and import statements. -2. **Read-only models** that we intend to run analytic queries against. These models can be created with Java classes - or with a HJSON configuration language. For this example, we will use the latter to create a `Downloads` model. - - - - - ```java - @Include(rootLevel = true, name = "group") - @Entity - public class ArtifactGroup { - @Id - private String name = ""; - - private String commonName = ""; - - private String description = ""; - - @OneToMany(mappedBy = "group") - private List products = new ArrayList<>(); - } - ``` - - - - - ```java - @Include(name = "product") - @Entity - public class ArtifactProduct { - @Id - private String name = ""; - - private String commonName = ""; - - private String description = ""; - - @ManyToOne - private ArtifactGroup group = null; - - @OneToMany(mappedBy = "artifact") - private List versions = new ArrayList<>(); - } - ``` - - - - - ```java - @Include(name = "version") - @Entity - public class ArtifactVersion { - @Id - private String name = ""; - - private Date createdAt = new Date(); - - @ManyToOne - private ArtifactProduct artifact; - } - ``` - - - - - ```hjson - { - tables: [ - { - name: Downloads - table: downloads - description: - ''' - Analytics for artifact downloads. - ''' - joins: [ - { - name: artifactGroup - to: group - kind: toOne - type: left - definition: '{{group_id}} = {{artifactGroup.name}}' - }, - { - name: artifactProduct - to: product - kind: toOne - definition: '{{product_id}} = {{artifactProduct.name}}' - } - ] - dimensions: [ - { - name: group - type: TEXT - definition: '{{artifactGroup.name}}' - } - { - name: product - type: TEXT - definition: '{{artifactProduct.name}}' - } - { - name: date - type: TIME - definition: '{{date}}' - grains: [{ - type: DAY - }] - } - ] - measures: [ - { - name: downloads - type: INTEGER - definition: 'SUM({{downloads}})' - } - ] - } - ] - } - ``` - - - - -Spinning up the API -------------------- - -So now we have some models, but without an API it is not very useful. Before we add the API component, we need to create -the schema in the database that our models will use. Our example uses liquibase to manage the schema. Our example will -execute the [database migrations][demo-schema] to configure the database with some test data automatically. This demo -uses *Postgres*. Feel free to modify the migration script if a different database provider is used. - -We may notice the example liquibase migration script adds an extra table, `AsyncQuery`. This is only required if -leveraging Elide's [asynchronous API](clientapis/asyncapi) to manage long-running analytic queries. - -There may be more tables in our database than models in our project or vice versa. Similarly, there may be more columns -in a table than in a particular model or vice versa. Not only will our models work just fine, but we expect that models -will normally expose only a subset of the fields present in the database. Elide is an ideal tool for building -micro-services - **each service in your system can expose only the slice of the database that it requires**. - -### Classes - -Bringing life to our API is trivially easy. We need a single Application class: - -```java -/** - * Example app using elide-spring. - */ -@SpringBootApplication -public class App { - public static void main(String[] args) throws Exception { - SpringApplication.run(App.class, args); - } -} -``` - -### Supporting Files - -The application is configured with a Spring application YAML file (broken into sections below). - -The Elide Spring starter uses a JPA data store (the thing that talks to the database). This can be configured like any -other Spring data source and JPA provider. The one below uses an H2 in-memory database: - -```yaml -spring: - jpa: - show-sql: true - properties: - hibernate: - dialect: 'org.hibernate.dialect.H2Dialect' - '[default_batch_fetch_size]': 100 - '[use_scrollable_resultset]': true - hibernate: - naming: - physical-strategy: 'org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl' - ddl-auto: 'validate' - datasource: - url: 'jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1' - username: 'sa' - password: '' - driver-class-name: 'org.h2.Driver' -``` - -Elide has its own configuration to turn on APIs and setup their URL paths: - -```yaml -elide: - json-api: - path: /api/v1 - enabled: true - graphql: - path: /graphql/api/v1 - enabled: true - api-docs: - path: /doc - enabled: true - version: openapi_3_0 -``` - -The following configuration enables Elide's asynchronous API for analytic queries: - -```yaml - async: - enabled: true - thread-pool-size: 7 - cleanup: - enabled: true - query-max-run-time: 65s - query-retention-duration: 7d -``` - -To enable analytic queries, we have to turn on the the aggregation data store. This example also enables HJSON configuration for analytic models: - -```yaml - aggregation-store: - enabled: true - default-dialect: h2 - dynamic-config: - path: src/main/resources/analytics - enabled: true -``` - -### Running - -With these new classes, we have two options for running our project. We can either run the `App` class using our -favorite IDE, or we can run the service from the command line: - -```console -java -jar target/elide-spring-boot-1.0.jar -``` - -Our example requires the following environment variables to be set to work correctly with Postgres. - -1. JDBC_DATABASE_URL -2. JDBC_DATABASE_USERNAME -3. JDBC_DATABASE_PASSWORD - -If we don't set them, the example will use the H2 in memory database. - -With the `App` class and application YAML file, we can now run our API. - -We can now run the following curl commands to see some of the sample data that the liquibase migrations added for us. - - - - - ```console - curl http://localhost:8080/api/v1/group - ``` - - - - - ```console - curl -g -X POST -H"Content-Type: application/json" -H"Accept: application/json" \ - "http://localhost:8080/graphql/api/v1" \ - -d'{ - "query" : "{ group { edges { node { name commonName description } } } }" - }' - ``` - - - - -Here are the respective responses: - - - - - ```json - { - "data": [ - { - "attributes": { - "commonName": "Example Repository", - "description": "The code for this project" - }, - "id": "com.example.repository", - "relationships": { - "products": { - "data": [ - { - "id": "elide-demo", - "type": "product" - } - ] - } - }, - "type": "group" - }, - { - "attributes": { - "commonName": "Elide", - "description": "The magical library powering this project" - }, - "id": "com.paiondata.elide", - "relationships": { - "products": { - "data": [ - { - "id": "elide-core", - "type": "product" - }, - { - "id": "elide-standalone", - "type": "product" - }, - { - "id": "elide-datastore-hibernate5", - "type": "product" - } - ] - } - }, - "type": "group" - } - ] - } - ``` - - - - - ```json - { - "data": { - "group": { - "edges": [ - { - "node": { - "commonName": "Example Repository", - "description": "The code for this project", - "name": "com.example.repository" - } - }, - { - "node": { - "commonName": "Elide", - "description": "The magical library powering this project", - "name": "com.paiondata.elide" - } - } - ] - } - } - } - ``` - - - - -Looking at More Data --------------------- - -We can navigate through the entity relationship graph defined in the models and explore relationships: - -``` -List groups: group/ -Show a group: group/ -List a group's products: group//products/ -Show a product: group//products/ -List a product's versions: group//products//versions/ -Show a version: group//products//versions/ -``` - -Writing Data ------------- - -So far we have defined our views on the database and exposed those views over HTTP. This is great progress, but so far -we have only read data from the database. - -### Inserting Data - -Fortunately for us adding data is just as easy as reading data. For now let’s use cURL to put data in the database. - - - - - ```console - curl -X POST http://localhost:8080/api/v1/group/com.example.repository/products -H"Content-Type: application/vnd.api+json" -H"Accept: application/vnd.api+json" -d '{"data": {"type": "product", "id": "elide-demo"}}' - ``` - - - - - ```console - curl -g -X POST -H"Content-Type: application/json" -H"Accept: application/json" \ - "http://localhost:8080/graphql/api/v1" \ - -d'{ "query" : "mutation { group(ids: [\"com.example.repository\"]) { edges { node { products(op: UPSERT, data: {name: \"elide-demo\"}) { edges { node { name } } } } } } }" }' - ``` - - - - -When we run that cURL call we should see a bunch of json returned, that is our newly inserted object! - - - - - ```json - { - "data": [ - { - "attributes": { - "commonName": "", - "description": "" - }, - "id": "elide-demo", - "relationships": { - "group": { - "data": { - "id": "com.example.repository", - "type": "group" - } - }, - "versions": { - "data": [] - } - }, - "type": "product" - } - ] - } - ``` - - - - - ```json - { - "data":{ - "group":{ - "edges":[ - { - "node":{ - "products":{ - "edges":[ - { - "node":{ - "name":"elide-demo" - } - } - ] - } - } - } - ] - } - } - } - ``` - - - - -### Modifying Data - -Notice that, when we created it, we did not set any of the attributes of our new product record. Updating our -data to help our users is just as easy as it is to add new data. Let's update our model with the following cURL call. - - - - - ```console - curl -X PATCH http://localhost:8080/api/v1/group/com.example.repository/products/elide-demo \ - -H"Content-Type: application/vnd.api+json" -H"Accept: application/vnd.api+json" \ - -d '{ - "data": { - "type": "product", - "id": "elide-demo", - "attributes": { - "commonName": "demo application", - "description": "An example implementation of an Elide web service that showcases many Elide features" - } - } - }' - ``` - - - - - ```console - curl -g -X POST -H"Content-Type: application/json" -H"Accept: application/json" \ - "http://localhost:8080/graphql/api/v1" \ - -d'{ - "query" : "mutation { group(ids: [\"com.example.repository\"]) { edges { node { products(op: UPDATE, data: { name: \"elide-demo\", commonName: \"demo application\", description: \"An example implementation of an Elide web service that showcases many Elide features\" }) { edges { node { name } } } } } } }" - }' - ``` - - - - -Running Analytic Queries ------------------------- - -Analytic queries leverage the same API as reading any other Elide model. Note that Elide will aggregate the measures -selected by the dimensions requested. Learn more about analytic queries [here](analytics). - - - - - ```console - curl -g "http://localhost:8080/api/v1/downloads?fields[downloads]=downloads,group,product" - ``` - - - - - ```console - curl -g -X POST -H"Content-Type: application/json" -H"Accept: application/json" \ - "http://localhost:8080/graphql/api/v1" \ - -d'{ - "query" : "{ downloads { edges { node { downloads group product } } } }" - }' - - ``` - - - - -Here are the respective responses: - - - - - ```json - { - "data": [ - { - "attributes": { - "downloads": 35, - "group": "com.example.repository", - "product": "elide-core" - }, - "id": "0", - "type": "downloads" - } - ] - } - ``` - - - - - ```json - { - "data": { - "downloads": { - "edges": [ - { - "node": { - "downloads": 35, - "group": "com.example.repository", - "product": "elide-core" - } - } - ] - } - } - } - ``` - - - - -[demo-schema]: https://github.com/yahoo/elide-spring-boot-example/blob/master/src/main/resources/db/changelog/changelog.xml - -[elide-demo]: https://github.com/yahoo/elide-spring-boot-example - -[Spring Boot starter dependency]: https://github.com/paion-data/elide/tree/master/elide-spring/elide-spring-boot-starter diff --git a/docs/docs/crud/elide/openapi.md b/docs/docs/crud/elide/openapi.md deleted file mode 100644 index a8389d37..00000000 --- a/docs/docs/crud/elide/openapi.md +++ /dev/null @@ -1,413 +0,0 @@ ---- -sidebar_position: 10 -title: OpenAPI -description: JSON API documentations ---- - -Overview --------- - -Elide supports the generation of [OpenAPI](https://www.openapis.org/) documentation from Elide annotated beans. -Specifically, it generates a JSON or YAML document conforming to the OpenAPI specification that can be used by tools -like [Swagger UI](http://swagger.io/) (among others) to explore, understand, and compose queries against our Elide API. - -Only JSON-API endpoints are documented. The GraphQL API schema can be explored directly with tools like -[Graphiql](https://github.com/graphql/graphiql). - -Features Supported ------------------- - -- **JaxRS & Spring Endpoint** - Elide ships with a customizable JaxRS endpoints that can publish one or more OpenAPI - documents in both JSON or YAML. -- **Path Discovery** - Given a set of entities to explore, Elide will generate the minimum, cycle-free, de-duplicated - set of URL paths in the OpenAPI document. -- **Filter by Primitive Attributes** - All _GET_ requests on entity collections include filter parameters for each - primitive attribute. -- **Prune Fields** - All _GET_ requests support JSON-API sparse fields query parameter. -- **Include Top Level Relationships** - All _GET_ requests support the ability to include direct relationships. -- **Sort by Attribute** - All _GET_ requests support sort query parameters. -- **Pagination** - All _GET_ requests support pagination query parameters. -- **Permission Exposition** - Elide permissions are exported as documentation for entity schemas. -- **Model & Attribute Properties** - The _required_, _readOnly_, _example_, _value_, _description_, _title_, - _accessMode_, _requiredMode_ fields are extracted from `Schema` annotations. -- **API Version Support** - Elide can create separate documents for different API versions. - -Getting Started ---------------- - -### Maven - -If we are not using [Elide Spring Starter][elide-spring] or [Elide Standalone][elide-standalone] (which package -swagger as a dependency), we will need to pull in the following elide dependencies : - -```xml - - com.paiondata.elide - elide-swagger - - - - com.paiondata.elide - elide-core - -``` - -Pull in swagger core: - -```xml - - io.swagger - swagger-core-jakarta - -``` - -#### Spring Boot Configuration - -If we are using -[Elide Spring Autoconfigure](https://github.com/paion-data/elide/tree/master/elide-spring/elide-spring-boot-autoconfigure), -we can customize the `OpenAPI` document by using a `OpenApiDocumentCustomizer` bean: - -```java -@Configuration -public class ElideConfiguration { - - @Bean - public OpenApiDocumentCustomizer openApiDocumentCustomizer() { - return openApi -> { - Info info = new Info().title("My Title"); - openApi.setInfo(info); - }; - } -} -``` - -The application YAML file has settings: - -- to enable the OpenAPI document endpoint -- to set the URL path for the OpenAPI document endpoint -- to set the OpenAPI specification version to generate either 3.0 or 3.1 - -For example: - -```yaml -elide: - api-docs: - enabled: true - path: /doc - version: openapi_3_0 -``` - -#### Supporting OAuth - -If we want Swagger UI to acquire & use a bearer token from an OAuth identity provider, we can configure the OpenAPI -document by using annotations: - -```java -@SpringBootApplication -@OpenAPIDefinition(info = @Info(title = "My Title"), security = @SecurityRequirement(name = "bearerAuth")) -@SecurityScheme( - name = "bearerAuth", - type = SecuritySchemeType.HTTP, - bearerFormat = "JWT", - scheme = "bearer" - ) -public class App { - - public static void main(String[] args) throws Exception { - SpringApplication.run(App.class, args); - } -} -``` - -#### SpringDoc Integration - -Elide contributes to [Springdoc](https://springdoc.org)'s OpenAPI document by exposing a Springdoc `OpenApiCustomizer` -bean. - -If API Versioning is used, only the Path strategy is supported when integrating with Springdoc as the other strategies -are difficult to document with OpenAPI. - -The default implementation is implemented in `DefaultElideOpenApiCustomizer`. To override the behavior a -`ElideOpenApiCustomizer` bean can be created which will cause the `DefaultElideOpenApiCustomizer` not to be configured. - -```java -@Configuration -public class ElideConfiguration { - @Bean - public ElideOpenApiCustomizer elideOpenApiCustomizer() { - return new MyCustomElideOpenApiCustomizer(); - } -} -``` - -When `GroupedOpenApi` is used, the `ElideOpenApiCustomizer` is not applied to the groups. Instead Elide has a -`DefaultElideGroupedOpenApiCustomizer` that will customize the `GroupedOpenApi` to set the appropriate -`OpenApiCustomizers` on the `GroupedOpenApi` that matches the paths to match and exclude. To override the behavior a -`ElideGroupedOpenApiCustomizer` can be defined that will need to process the `OpenApiCustomizers` and remove the ones -automatically added by Elide. - -```java -@Configuration -public class ElideConfiguration { - @Bean - public ElideGroupedOpenApiCustomizer elideGroupedOpenApiCustomizer() { - return new MyCustomElideGroupedOpenApiCustomizer(); - } -} -``` - -#### Elide Standalone Configuration - -If we are using [Elide Standalone](https://github.com/paion-data/elide/tree/master/elide-standalone), we can extend -`ElideStandaloneSettings` to: - -- Enable the OpenAPI document endpoint. -- Configure the URL Path for the OpenAPI document. -- Configure the OpenAPI document version. -- Configure the service name. -- Construct OpenAPI documents for your service. - -```java -public abstract class Settings implements ElideStandaloneSettings { - /** - * Enable OpenAPI documentation. - * @return whether OpenAPI is enabled; - */ - @Override - public boolean enableApiDocs() { - return true; - } - - /** - * API root path specification for the OpenAPI endpoint. Namely, this is the root uri for OpenAPI docs. - * - * @return Default: /api-docs/* - */ - @Override - public String getApiDocsPathSpec() { - return "/api-docs/*"; - } - - /** - * OpenAPI documentation requires an API name. - * @return open api service name; - */ - @Override - public String getApiTitle() { - return "Elide Service"; - } - - /** - * The OpenAPI Specification Version to generate. - * @return the OpenAPI Specification Version to generate - */ - @Override - public OpenApiVersion getOpenApiVersion() { - return OpenApiVersion.OPENAPI_3_0; - } - - /** - * Creates a singular OpenAPI document for JSON-API. - * @param dictionary Contains the static metadata about Elide models. . - * @return list of OpenAPI registration objects. - */ - @Override - public List buildApiDocs(EntityDictionary dictionary) { - List docs = new ArrayList<>(); - - dictionary.getApiVersions().stream().forEach(apiVersion -> { - Info info = new Info() - .title(getApiTitle()) - .version(apiVersion); - OpenApiBuilder builder = new OpenApiBuilder(dictionary).apiVersion(apiVersion); - String moduleBasePath = getJsonApiPathSpec().replace("/*", ""); - OpenAPI openApi = builder.build().info(info).addServersItem(new Server().url(moduleBasePath)); - docs.add(new ApiDocsEndpoint.ApiDocsRegistration("test", () -> openApi, getOpenApiVersion().getValue(), - apiVersion)); - }); - - return docs; - } -} -``` - -### Elide Library Configuration - -If we are using Elide directly as a library (and not using Elide Standalone), follow these instructions: - -Create and initialize an entity dictionary. - -```java -EntityDictionary dictionary = EntityDictionary.builder().build(); - -dictionary.bindEntity(Book.class); -dictionary.bindEntity(Author.class); -dictionary.bindEntity(Publisher.class); -``` - -Create a Info object. - -```java -Info info = new Info().title("My Service").version("1"); -``` - -Initialize a OpenAPI builder. - -```java -OpenApiBuilder builder = new OpenApiBuilder(dictionary); -``` - -Build the OpenAPI document - -```java -OpenAPI document = builder.build().info(info); -``` - -#### Converting OpenAPI to JSON or YAML - -We can directly convert to JSON: - -```java -OpenApiDocument openApiDocument = new OpenApiDocument(document, OpenApiDocument.Version.from("3.0")); -String jsonOutput = openApiDocument.of(OpenApiDocument.MediaType.APPLICATION_JSON); -``` - -We can directly convert to YAML as well: - -```java -OpenApiDocument openApiDocument = new OpenApiDocument(document, OpenApiDocument.Version.from("3.0")); -String jsonOutput = openApiDocument.of(OpenApiDocument.MediaType.APPLICATION_YAML); -``` - -#### Configure JAX-RS Endpoint - -Or we can use the OpenAPI document directly to configure the [provided JAX-RS Endpoint](https://github.com/paion-data/elide/blob/master/elide-swagger/src/main/java/com/paiondata/elide/swagger/resources/ApiDocsEndpoint.java): - -```java -List apiDocs = new ArrayList<>(); -OpenAPI openApi = new OpenAPI(); -apiDocs.add(new ApiDocsEndpoint.ApiDocsRegistration("test", () -> openApi, "3.0", "")); - -//Dependency Inject the ApiDocsEndpoint JAX-RS resource -bind(apiDocs).named("apiDocs").to(new TypeLiteral>() { }); -``` - -### Supporting OAuth - -If we want Swagger UI to acquire & use a bearer token from an OAuth identity provider, we can configure -the OpenAPI document similar to: - -```java -SecurityScheme oauthDef = new SecurityScheme() - .name("bearerAuth") - .type(SecurityScheme.Type.HTTP) - .bearerFormat("JWT") - .scheme("bearer"); -SecurityRequirement oauthReq = new SecurityRequirement().addList("bearerAuth"); - -OpenApiBuilder builder = new OpenApiBuilder(entityDictionary); -OpenAPI document = builder.build(); -document.addSecurityItem(oauthReq); -document.getComponents().addSecuritySchemes("bearerAuth", oauthDef); -``` - -### Adding a global parameter - -A query or header parameter can be added globally to all Elide API endpoints: - -```java -Parameter oauthParam = new Parameter() - .in("Header") - .name("Authorization") - .schema(new StringSchema()) - .description("OAuth bearer token") - .required(false); - -OpenApiBuilder builder = new OpenApiBuilder(dictionary).globalParameter(oauthParam); -``` - -### Adding a global response code - -An HTTP response can be added globally to all Elide API endpoints: - -```java -ApiResponse rateLimitedResponse = new ApiResponse().description("Too Many Requests"); - -OpenApiBuilder builder = new OpenApiBuilder(dictionary).globalResponse(429, rateLimitedResponse); -``` - -Performance ------------ - -### Path Generation - -The Swagger UI is very slow when the number of generated URL paths exceeds a few dozen. For large, complex data models, -it is recommended to generate separate OpenAPI documents for subgraphs of the model. - -```java -Set> entities = Set.of( - ClassType.of(Book.class), - ClassType.of(Author.class), - ClassType.of(Publisher.class) -); - -OpenApiBuilder builder = new OpenApiBuilder(dictionary).managedClasses(entities); -``` - -In the example above, the `OpenApiBuilder` will only generate paths that exclusively traverse the provided set of -entities. - -### Document Size - -The size of the OpenAPI document can be reduced significantly by limiting the number of filter operators that are used -to generate query parameter documentation. - -In this example, filter query parameters are only generated for the _IN_ operator. - -```java -OpenApiBuilder builder = new OpenApiBuilder(dictionary).filterOperators(Set.of(Operator.IN)); -``` - -### Model Properties - -Elide extracts the model description and title from the `Schema` and `Include` annotations and adds them to the OpenAPI -documentation. `Schema` has precedence over `Include` if both are present. - -```java -@Schema(description = "A book model description", title = "Book") -class Book { - -} -``` - -For `Schema` only the _description_ and _title_ property is extracted. For the `Include` annotation, the _friendlyName_ -is used as the _title_. - -### Attribute Properties - -Elide extracts properties from the `Schema` annotation and adds them to the OpenAPI documentation. - -```java -class Book { - - @Schema(requiredMode = RequiredMode.REQUIRED) - public String title; -} -``` - -Only the _required_, _value_, _example_, _readOnly_, _writeOnly_, _requiredProperties_, _requiredMode_ and _accessMode_ -properties are extracted. This is currently only supported for attributes on Elide models. - -API Versions ------------- - -OpenAPI documents are tied to an explicit API version. When constructing a OpenAPI document, the API version must be set -to match the API version of the models it will describe: - -```java -OpenApiBuilder builder = new OpenApiBuilder(dictionary).apiVersion("1"); -OpenAPI openApi = builder.build(); -``` - -[elide-spring]: https://github.com/paion-data/elide/tree/master/elide-spring/elide-spring-boot-autoconfigure -[elide-standalone]: https://github.com/paion-data/elide/tree/master/elide-standalone diff --git a/docs/docs/crud/elide/performance.md b/docs/docs/crud/elide/performance.md deleted file mode 100644 index 4749c5e8..00000000 --- a/docs/docs/crud/elide/performance.md +++ /dev/null @@ -1,231 +0,0 @@ ---- -sidebar_position: 12 -title: Performance -description: Tips on improving webservice performance ---- - -N+1 Problem ------------ - -### Overview - -The N+1 problem is a performance issue where an ORM issues a large number of database queries to fetch a parent/child -relationship. The ORM issues a single query to hydrate the parent and then _N_ queries to hydrate the children. - -Most ORMs solve this problem by providing a number of different fetching strategies that are enabled when a proxy object -or collection is hydrated. These strategies fall into one of two categories: - -1. A join is used to fetch both the parent and the children in a single query. The ORM populates its session cache with - all entities fetched in the join. Joining works well for fetching singular relationships. It is important to note - that a singular join that fetches an entire subgraph (spanning multiple relationships) is impractical and would break - row based pagination (offset & limit). Furthermore, large joins put considerable memory stress on the ORM server - because of the width of the records. -2. Instead of a single query per element of a collection, the number of queries is reduced by fetching multiple children - in fewer queries. - -These strategies may or not be available to the developer depending on how the ORM is leveraged. If the developer -interacts with a proxy object directly, all fetch strategies are available. However, the SQL queries generated from -proxy objects cannot be customized with additional filters, sorting, or pagination. - -Alternatively, the developer can have complete control over the query by writing JPQL or Criteria queries. However, only -join fetching is available through these APIs. - -### Solution - -Because Elide has to work well under a wide variety of circumstances, it has adopted a hybrid solution for ORM based -data stores. - -Whenever Elide traverses a to-one relationship, it returns the ORM proxy object directly. In most cases, these -relationships should already exist inside the session and result in no extra database queries. - -Whenever Elide traverses a to-many relationship, Elide determines if it is loading a single collection (1 query) or a -collection of collections (N+1 queries). In the latter case, it returns the ORM proxy directly and performs all -filtering, sorting, and pagination in memory. Otherwise, it constructs a custom JPQL query that will fetch the -collection - joining with all the client requested to-one relationships to prefetch them. - -In general, it is recommended to configure the ORM with batch fetching so the ORM will efficiently hydrate proxy -collections (batch fetching turns N queries into (N / batch size) queries). - -Security Checks ---------------- - -Elide provides different flavors of security checks for performance reasons. In general, it is expensive to execute -servers side functions for every entity row hydrated from the database. Because Elide is handling the results of each -query in a single thread, the CPU cost of these checks can add extra latency to our queries. - -To work around this, Elide provides two different kinds of security checks: - -1. *User Checks* - User checks are evaluated once per request rather than per entity row. -2. *Filter Expression Checks* - Filter Expression Checks generate filter expressions that are pushed to the persistence - layer and executed in the database. - -For data reads from the database, it is recommended to use User Checks and Filter Expression Checks wherever possible. - -ORM-isms --------- - -Beware to-one relationships where the entity doesn't own the relationship (`mappedBy` is specified) _and_ `optional` is -set to true. The ORM must **ALWAYS** fetch these relationships when hydrating a proxy (leading to N+1 queries depending -on how the ORM is configured). The ORM has no way of knowing if the relationship is null or non-null without issuing -another database query. - -Database-isms -------------- - -It is highly recommended to collocate Elide servers and the database in the same data center or region to reduce the -latency of database queries from Elide. - -It is also recommended to segregate Elide read only transactions (data fetches) to run against a read-only replica of -the database for optimal read performance. - -Text Search ------------ - -By default, text search (INFIX operator) is accomplished in Elide through a JPQL query similar to: - -```sql -SELECT id, field1, field2, ... FROM table WHERE field1 like CONCAT('%', searchTerm, '%') -``` - -For case insensitive searches, Elide will add a lower case function to both the search field and the search value. - -There are a number of limitations to this approach: - -1. If the database is using a b-tree index to index the search column, a preceding wildcard ('%') will disable the use - of the index. The query will result in a full table scan. -2. Databases have limited support for functional indices. Use of lower or upper case functions may also disable the use - of an index. - -Elide provides two capabilities to work around these issues for large tables that require text search: - -1. Elide supports the [ability to override the JPQL fragment](#jpql-fragment-override) that is generated for any - operator on any field in any entity model. This makes it possible to disable the use of lower/upper case functions on - a database column if the database column is already case insensitive. It is also possible to use custom SQL dialects - to leverage full text index support (where available). -2. Elide supports a - [Text Search Data Store](https://github.com/paion-data/elide/tree/master/elide-datastore/elide-datastore-search) that can - wrap another ORM data store. Whenever possible, the text search data store can delegate queries to a local Lucene - index or a Elasticsearch cluster rather than the default data store. - -### JPQL Fragment Override - -To override the JPQL fragment Elide generates for a filter operator, we must define a JPQL Predicate Generator: - -```java -@FunctionalInterface -public interface JPQLPredicateGenerator { - - /** - * Generate a JPQL fragment for a particular filter operator. - * - * @param predicate The filter predicate - * @param aliasGenerator Given a path, returns a JPQL alias for that path. - * - * @return A JPQL fragment. - */ - String generate(FilterPredicate predicate, Function aliasGenerator); -} -``` - -And then register it with Elide for the filter operator we want to modify. This can either be done globally: - -```java -FilterTranslator.registerJPQLGenerator( - Operator.NOTNULL, - (predicate, aliasGenerator) -> { - return String.format("%s IS NOT NULL", aliasGenerator.apply(predicate.getPath())); - } -); -``` - -Or the override can be registered for a specific model attribute: - -```java -FilterTranslator.registerJPQLGenerator( - Operator.NOTNULL, - Book.class, - "title", - (predicate, aliasGenerator) -> { - return String.format("%s IS NOT NULL", aliasGenerator.apply(predicate.getPath())); - } -); -``` - -Bespoke Field Sets ------------------- - -By default JSON-API fetches every relationship in an entity unless a client restricts what it asks for through sparse -fields. These relationship fetches result in extra database queries. It is recommended to either use GraphQL or educate -clients to use sparse fields in JSON-API whenever possible. - -AggregationDataStore Cache --------------------------- - -AggregationDataStore supports caching QueryEngine results. By default, a simple in-memory Caffeine-based cache is -configured, with a size limit of 1024 entries, but we can provide our own implementation. - -For the cache to apply to a query, there are two requirements: - -1. The `AggregationDataStore` must be supplied with a cache implementation. -2. The query being executed doesn't have `bypassingCache` set. - -### With Spring Configuration - -The configuration property `elide.aggregation-store.query-cache.max-size` controls the size of the default cache -implementation. Setting the value to be zero or negative disables the cache. - -The configuration property `elide-aggregation-store.query-cache.expiration` sets the default item expiration. - -```yaml -elide: - aggregation-store: - enabled: true - query-cache: - enabled: true - max-size: 1000 - expiration: 10m -``` - -To provide our own cache implementation, inject it as a `com.paiondata.elide.datastores.aggregation.cache.Cache` bean. - -### With Standalone Configuration - -To control the default size of the cache or the item expiration, override the following `ElideStandaloneSettings` methods: - -```java -public abstract class Settings implements ElideStandaloneSettings { - @Override - public ElideStandaloneAnalyticSettings getAnalyticProperties() { - return new ElideStandaloneAnalyticSettings() { - @Override - public Integer getQueryCacheMaximumEntries() { - return 1000; - } - @Override - public Long getDefaultCacheExpirationMinutes() { - return 10L; - } - }; - } -} -``` - -To provide our own cache implementation, override `ElideStandaloneSettings.getQueryCache`. - -### Query Versions - -The `AggregationDataStore` can prepend a table/data version to each cache entry key. This will prevent the cache from -returning stale data. Elide supports the `VersionQuery` annotation which specifies a SQL query to run that returns the -version for a particular table: - -```java -@Include -@FromTable(name = "stats") -@VersionQuery(sql = "SELECT COUNT(*) FROM stats") -public class Stats { - // fields -} -``` - -Returning a row count should work for tables that are insert-only. In most cases, a more sophisticated query will be -needed, such as one that returns a table modification timestamp. diff --git a/docs/docs/crud/elide/security.mdx b/docs/docs/crud/elide/security.mdx deleted file mode 100644 index c0e1ed68..00000000 --- a/docs/docs/crud/elide/security.mdx +++ /dev/null @@ -1,438 +0,0 @@ ---- -sidebar_position: 5 -title: Security -description: Authorization support ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Core Concepts -------------- - -API authentication is largely a solved problem and generally outside the scope of Elide. Authorization - the act of -verifying data and operation access for an _already authenticated user_ in the Elide framework involves a few core -concepts: - -- **User** - Each API request is associated with a user principal. The user is opaque to the Elide framework but is - passed to developer-defined _check_ functions that evaluate arbitrary logic or build filter expressions. More details - can be found [here](#user). -- **Checks** - a function _or_ filter expression that grants or denies a user **permission** to perform a particular - action. -- **Permissions** - a set of annotations (read, update, delete, create, and transfer) that correspond to actions on - the data model's entities and fields. Each **permission** is decorated with one or more checks that are evaluated - when a user attempts to perform that action. - -Security Evaluation -------------------- - -Security is applied hierarchically with three goals: - -1. **Granting or denying access.** When a model or field is accessed, a set of checks are evaluated to determine if - the access will be denied (i.e. 403 HTTP status code (JSON-API) or GraphQL error object) or permitted. If a user - has explicitly requested access to part of the data model they should not see, the request will be rejected. -2. **Filtering Collections.** If a model has read permissions defined, these checks are evaluated against each model - that is a member of the collection. Only the models the user has access to (by virtue of being able to read at - least one of the model's fields) are returned in the response. -3. **Filtering a model.** If a user has read access to a model, but only for a subset of a model's fields, the - disallowed fields are excluded from the output (rather than denying the request). However, when the user - explicitly requests a field-set that contains a restricted field, the request is rejected rather than filtered. - -### Hierarchical Security - -Both JSON-API and GraphQL define mechanisms to fetch and manipulate entities defined by the data model schema. Some -(rootable) entities can be reached directly by providing their data type and unique identifier in the query. Other -entities can only be reached through relationships to other entities by traversing the entity relationship graph. The -Elide framework supports both methods of access. This is beneficial because it alleviates the need for all models to be -read-accessible at the root of the graph. When everything is exposed at the root, the developer needs to enumerate all -of the valid access patterns for all of the data models which can be unwieldy. In addition to eliminating redundancy in -security declaration, this form of security can have significant performance benefits for enforcing security on large -collections stored in key-value stores that have limited ability for the underlying persistence layer to directly apply -security filters. It is often possible to deny read access to an entire collection (i.e. hierarchical relationship) -before attempting to verify access to each individual member within that collection. Typically, security rules only need -to be defined for a subset of models and relationships - often near the roots of the graph. Applying security rules to -the relationships to prune the graph can eliminate invalid access patterns. To better understand the sequence of how -security is applied, consider the data model depicted in Figure below consisting of articles where each contains zero or -more comments. - -

![Security Article Comment UML](img/security-article-comment-uml.png)
- -The request to update a specific comment of a particular article involves the following permission checks: - -1. Read permission check on the Article's comments field. -2. Update permission check on the Comment's title field. - -When a client modifies one side of a bidirectional relationship, Elide will automatically update the opposite side of -the relationship. This implies that the client must have permission to write both sides of the relationship. - -Checks ------- - -Checks are simply functions that either return: - -- whether or not access should be granted to the requesting user. -- a filter expression that can be used to filter a collection to what is visible to a given user. - -Checks can either be invoked: - -- immediately prior to the (create, read, update, and delete) action being performed. This is the default case. -- immediately before committing the transaction that wraps the entire API request. This is limited to checks on - fields of newly created objects. - -Elide supports three different concrete `Check` classes depending on what is being checked: - -![Error loading check-tree.png](img/check-tree.png) - -`Check` is the root interface of all three variants which are discussed next - -### Operation Checks - -Operation checks are inline checks whose evaluation requires the entity instance being read from or written to. They -operate in memory of the process running Elide. - -Operation checks are expected to implement the following `Check` interface: - -```java -/** - * Determines whether the user can access the resource. - * - * @param object The object that is being read/written. - * @param requestScope Request scope object - * @param changeSpec Summary of modifications - * - * @return true if security check passed - */ -public abstract boolean ok(T object, RequestScope requestScope, Optional changeSpec); -``` - -### User Checks - -User checks depend strictly on the user principal. They only take a User object as input. Because these checks only -depend on who is performing the operation and not on what has changed, these checks are only evaluated once per -request - an optimization that accelerates the filtering of large collections. - -User checks are expected to implement the following `Check` interface: - -```java -/** - * Method reserved for user checks. - * - * @param user User to check - * - * @return True if user check passes, false otherwise - */ -public abstract boolean ok(User user); -``` - -### Filter Expression Checks - -In some cases, the check logic can be pushed down to the data store itself. For example, a filter can be added to a -database query to remove elements from a collection where access is disallowed. These checks return a -`FilterExpression` predicate that your data store can use to limit the queries that it uses to marshal the data. -Checks which extend the `FilterExpessionCheck` must conform to the interface: - -```java -/** - * Returns a FilterExpression from FilterExpressionCheck. - * - * @param entityClass entity type - * @param requestScope Request scope object - * - * @return FilterExpression for FilterExpressionCheck. - */ -public abstract FilterExpression getFilterExpression(Type entityClass, RequestScope requestScope); -``` - -`FilterExpressionCheck` _is_ an `OperationCheck`. If a security rule combines both an `OperationCheck` and -`FilterExpression` in a disjunction (logical OR), Elide will evaluate both in memory as operation checks. - -Most `FilterExpressionCheck` functions construct a `FilterPredicate` which is a concrete implementation of the -`FilterExpression` interface: - -```java -/** - * Constructs a filter predicate. - * - * @param path The path through the entity relationship graph to a particular attribute to filter on. - * @param op The filter comparison operator to evaluate. - * @param values The list of values to compare the attribute against. - */ -public FilterPredicate(Path path, Operator op, List values) { - - ... -} -``` - -Here is an example to filter the Author model by book titles: - -```java -// Construct a filter for the Author model for books.title == 'Harry Potter' -Path.PathElement authorPath = new Path.PathElement(Author.class, Book.class, "books"); -Path.PathElement bookPath = new Path.PathElement(Book.class, String.class, "title"); -List pathList = Arrays.asList(authorPath, bookPath); -Path path = new Path(pathList); - -return new FilterPredicate(path, Operator.IN, Collections.singletonList("Harry Potter")); -``` - -Filter expression checks are most important when a security rule is tied in some way to the data itself. By pushing -the security rule down to the datastore, the data can be more efficiently queried which vastly improves performance. -Moreover, this feature is critical for implementing a service that requires complex security rules (i.e. anything -more than role-based access) on large collections. - -User ----- - -Each request is associated with a `User` object. The User is simply an object that wraps a -`java.security.Principal` object. It includes methods to: - -1. Extract the user name. -2. Extract the underlying principal. -3. Check if the user belongs to a particular role. - -### Spring Boot User Object - -When using Spring Boot, the `User` object always wraps the `org.springframework.security.core.Authentication` extracted -by Spring Security. - -### Elide Standalone User Object - -When using elide standalone, the `User` objects wraps the Jakarta -[SecurityContext](https://qubitpi.github.io/jakartaee-rest/jaxrs-api/target/apidocs/jakarta.ws.rs/jakarta/ws/rs/core/SecurityContext.html) --object. - -The `SecurityContext` is created outside the Elide framework in a [JAX-RS](https://jcp.org/en/jsr/detail?id=311) -[ContainerRequestFilter](https://qubitpi.github.io/jakartaee-rest/jaxrs-api/target/apidocs/jakarta.ws.rs/jakarta/ws/rs/container/ContainerRequestFilter.html): - -```java -@Override -public void filter(ContainerRequestContext containerRequestContext) throws IOException { - containerRequestContext.setSecurityContext(new SecurityContext(){ - - ... - } -} -``` - -This filter will typically authenticate the request and store an identifier about the user inside the new -`SecurityContext`. - -Permission Annotations ----------------------- - -The permission annotations include `ReadPermission`, `UpdatePermission`, `CreatePermission`, and `DeletePermission` -Permissions are annotations which can be applied to a model at the `package`, `entity`, or `field`-level. The most -specific annotation always take precedence (`package < entity < field`). More specifically, a field annotation -overrides the behavior of an entity annotation. An entity annotation overrides the behavior of a package annotation. -Entity annotations can be inherited from superclasses. When no annotation is provided at any level, access is -implicitly granted for `ReadPermission`, `UpdatePermission`, `CreatePermission`, and `DeletePermission`. - -The permission annotations wrap a boolean expression composed of the check(s) to be evaluated combined with `AND`, -`OR`, and `NOT` operators and grouped using parenthesis. The checks are uniquely identified within the expression by -a string - typically a human readable phrase that describes the intent of the check (_"principal is admin at company -OR principal is super user with write permissions"_). These strings are mapped to the explicit `Check` classes at -runtime by registering them with Elide. When no registration is made, the checks can be identified by their fully -qualified class names. The complete expression grammar can be found [here][source-grammar]. - -To better understand how permissions work consider the following sample code. (Only the relevant portions are -included.) - - - - ```java - @ReadPermission(expression = "Prefab.Role.All") - @UpdatePermission(expression = "user is a superuser OR user is this user") - @DeletePermission(expression = "user is a superuser OR user is this user") - public class User { - - String name; - - @OneToMany(mappedBy = "author") - Set posts; - } - ``` - - - ```java - @ReadPermission(expression = "Post.isVisible OR user owns this post OR user is a superuser") - @UpdatePermission(expression = "user owns this post") - @CreatePermission(expression = "user owns this post") - @DeletePermission(expression = "user owns this post") - public class Post { - - @ManyToOne - User author; - - @UpdatePermission(expression = "user owns this post OR user is a superuser") - boolean published; - - @OneToMany(mappedBy = "post") - Set comments; - } - ``` - - - ```java - // user has to be able to see the post and to see the comment, or else be a super user - @ReadPermission(expression = "((Post.isVisible OR user owns this post) AND (comment is visible OR user made this comment)) OR user is a superuser") - @UpdatePermission(expression = "user made this comment") - @CreatePermission(expression = "post is visible") - @DeletePermission(expression = "user made this comment") - public class Comment { - - @ManyToOne - User author; - @ManyToOne - Post post; - @UpdatePermission(expression = "user owns this post OR user is a superuser") - boolean suppressed; - } - ``` - - - ```java - @SecurityCheck(IsOwner.PRINCIPAL_IS_OWNER) - public static class IsOwner extends OperationCheck { - - public static final String PRINCIPAL_IS_OWNER = "user owns this post"; - - @Override - boolean ok(Post post, RequestScope requestScope, Optional changeSpec) { - return post.author.equals(requestScope.getUser()); - } - } - ``` - - - ```java - @SecurityCheck(IsSuperUser.PRINCIPAL_IS_SUPERUSER) - public static class IsSuperuser extends UserCheck { - - public static final String PRINCIPAL_IS_SUPERUSER = "user is a superuser"; - - @Override - boolean ok(User user) { - return user.isInRole("SUPER_USER"); - } - } - ``` - - - -### Read - -`ReadPermission` governs whether a model or field can be read by a particular user. If the expression evaluates to -`true` then access is granted. Notably, `ReadPermission` is evaluated as the user navigates through the entity -relationship graph. Elide's security model is focused on field-level access, with permission annotations applied on an -entity or package being shorthand for applying that same security to every field in that scope. For example, if a -request is made to `GET /users/1/posts/3/comments/99` the permission execution will be as follows: - -1. `ReadPermission` on `User<1>#posts` -2. `ReadPermission` on `Post<3>#comments` -3. `ReadPermission` on any field on `Comment<99>` - -If all of these checks succeed, then the response will succeed. The contents of the response are determined by -evaluating the `ReadPermission` on each field. The response will contain the subset of fields where `ReadPermission` is -granted. If a field does not have an annotation, then access defaults to whatever is specified at the entity level. If -the entity does not have an annotation, access defaults to whatever is specified at the package. If the package does not -have an annotation, access defaults to granted. - -### Update - -`UpdatePermission` governs whether a model can be updated by a particular user. Update is invoked when an attribute's -value is changed or values are added to or removed from a relationship. Examples of operations that will evaluate -`UpdatePermission` given objects `Post` and `User` from the code snippets above: - -- Changing the value of `Post.published` will evaluate `UpdatePermission` on `published`. Because more specific checks - override less specific checks, the `UpdatePermission` on the entity `Post` will not be evaluated. -- Setting `Post.author = User` will evaluate `UpdatePermission` on `Post` since `author` does not have a more specific - annotation. Because `author` is a bidirectional relationship, `UpdatePermission` will also be evaluated on the - `User.posts` field. -- Removing `Post` from `User.posts` will trigger `UpdatePermission` on both the `Post` and `User` entities. -- Creating `Post` will _not_ trigger `UpdatePermission` checks on any fields that are initialized in the request. - However, it will trigger `UpdatePermission` on any bidirectional relationship fields on preexisting objects. - -### Create - -`CreatePermission` governs whether a model can be created or a field can be initialized in a newly created model -instance. Whenever a model instance is newly created, initialized fields are evaluated against `CreatePermission` rather -than `UpdatePermission`. - -### Delete - -`DeletePermission` governs whether a model can be deleted. - -### NonTransferable - -`NonTransferable` governs whether an existing model instance (one created in a prior transaction) can be assigned to -another collection other than the one in which it was initially created. Basically, does a collection 'own' the model -instance in a private sense (composition) or can it be moved or referenced by other collections (aggregation). - -Marking an object `NonTransferable` means that it is owned by its containing collections at object creation. It cannot -be moved or copied to another collection after creation. Excluding this annotation means that instances of the class can -be moved or copied to other collections _provided the user agent can read the object_ (`ReadPermission` is satisfied on -at least some of its fields). - -Registering Checks in Elide ---------------------------- - -Once an Elide data model has been annotated with Permission annotations, the textual descriptions of the checks must be -tied to actual check classes and registered in Elide. This can be done in one of two ways: - -1. Automatically by marking the `Check` classes with a `SecurityCheck` annotation. Elide Spring Boot and Standalone will - automatically scan for `SecurityCheck` classes and bind them. -2. Manually by creating a `Map>` where they key is the description and the value is the - Check class. This is then passed to the constructor of the `EntityDictionary`. - -### Automatic Scanning - -Elide will find our security checks in the classpath if they are tagged with the `SecurityCheck` annotation: - -```java -@SecurityCheck(AdminCheck.USER_IS_ADMIN) -public class AdminCheck extends UserCheck { - - public static final String USER_IS_ADMIN = "User is Admin"; - - @Override - boolean ok(User user) { - ... - } -} -``` - -The `EntityDictionary` must be told to scan for checks (by calling `dictionary.scanForSecurityChecks`). This is done -automatically in elide spring boot and elide standalone at startup. - -### Manual Registration - -If not using Elide spring boot or standalone, we can register checks when creating the `Elide` instance: - -```java -//Create the check mappings -Map> checkMappings = new HashMap<>(); -checkMappings.put("User is an admin", AdminCheck.class); - -//Bind them in the dictionary -EntityDictionary dictionary = new EntityDictionary(checkMappings, beanFactory::autowireBean); - -//Create a data store -DataStore dataStore = ... - -//Configure Elide settings -ElideSettingsBuilder builder = ElideSettings.builder() - .dataStore(dataStore) - .entityDictionary(dictionary) - .settings(JsonApiSettings.builder() - .joinFilterDialect(new RSQLFilterDialect(dictionary)) - .subqueryFilterDialect(new RSQLFilterDialect(dictionary))) - .settings(GraphQLSettings.builder() - .filterDialect(new RSQLFilterDialect(dictionary))) - .serdes(serdes -> serdes.withISO8601Dates("yyyy-MM-dd'T'HH:mm'Z'", TimeZone.getTimeZone("UTC"))) - .auditLogger(new Slf4jLogger()); - -//Create the Elide instance -Elide elide = new Elide(builder.build()); -``` - -[source-grammar]: https://github.com/paion-data/elide/blob/master/elide-core/src/main/antlr4/com/paiondata/elide/generated/parsers/Expression.g4 diff --git a/docs/docs/crud/elide/test.md b/docs/docs/crud/elide/test.md deleted file mode 100644 index 3d3725fb..00000000 --- a/docs/docs/crud/elide/test.md +++ /dev/null @@ -1,202 +0,0 @@ ---- -sidebar_position: 11 -title: Test ---- - -The [elide-test-helpers](https://github.com/paion-data/elide/tree/master/elide-test) package provides a JSON-API and -GraphQL type safe DSL that simplifies adding integration tests to your service. The DSLs are designed to work with -[Rest Assured](http://rest-assured.io/). - -Dependencies ------------- - -The tests described here are based on a [the getting started example repo][elide-demo]. - -The example leverages: - -1. [Elide Spring Boot Starter][elide-spring] for running the test service and setting up Elide. -2. [JUnit 5](https://junit.org/junit5/) for adding tests. -3. [elide-test-helpers](https://github.com/paion-data/elide/tree/master/elide-test) for the JSON-API and GraphQL DSLs. -4. [Rest Assured](http://rest-assured.io/) for issuing HTTP requests against the test service. -5. [Spring Boot Test Starter](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test) for - adding test data for each test. -6. [H2 In Memory Database](https://www.h2database.com/html/main.html) for an in memory test database. - -### Maven - -```xml - - com.paiondata.elide - elide-spring-boot-starter - ${elide.version} - - - - com.paiondata.elide - elide-test-helpers - ${elide.version} - test - - - - com.jayway.restassured - rest-assured - 2.9.0 - test - - - - org.springframework.boot - spring-boot-starter-test - ${spring.version} - test - - - - org.junit.jupiter - junit-jupiter-api - 5.5.2 - test - - - - org.junit.jupiter - junit-jupiter-engine - 5.5.2 - test - - - - com.h2database - h2 - 1.4.197 - -``` - -Setup ------ - -Using elide with Spring Boot, we can set up a test service for integration tests by having our test classes extend a -common test base class like this one: - -```java -/** - * Base class for running a set of functional Elide tests. This class - * sets up an Elide instance with an in-memory H2 database. - */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class IntegrationTest { - - @LocalServerPort - int port; - - @BeforeAll - public void setUp() { - RestAssured.port = port; - } -} -``` - -JSON-API DSL ------------- - -Using Rest Assured and the JSON-API DSL, we can issue JSON-API requests and verify responses against our test service. -This example uses Spring Boot to initialize the H2 database with a clean set of test records. - -```java - @Test - @Sql(statements = { - "DELETE FROM ArtifactVersion; DELETE FROM ArtifactProduct; DELETE FROM ArtifactGroup;", - "INSERT INTO ArtifactGroup (name, commonName, description) VALUES\n" + - "\t\t('com.example.repository','Example Repository','The code for this project');" - }) - void jsonApiGetTest() { - when() - .get("/api/v1/group") - .then() - .log().all() - .body(equalTo( - data( - resource( - type( "group"), - id("com.example.repository"), - attributes( - attr("commonName", "Example Repository"), - attr("description", "The code for this project") - ), - relationships( - relation("products") - ) - ) - ).toJSON()) - ) - .log().all() - .statusCode(HttpStatus.SC_OK); - } -``` - -The complete set of static DSL operators for JSON-API can be found -[here](https://github.com/paion-data/elide/blob/master/elide-test/src/main/java/com/paiondata/elide/test/jsonapi/JsonApiDSL.java). - -GraphQL DSL ------------ - -Using Rest Assured and the GraphQL DSL, we can issue GraphQL requests and verify responses against our test service -like this: - -```java - @Test - @Sql(statements = { - "DELETE FROM ArtifactVersion; DELETE FROM ArtifactProduct; DELETE FROM ArtifactGroup;", - "INSERT INTO ArtifactGroup (name, commonName, description) VALUES\n" + - "\t\t('com.example.repository','Example Repository','The code for this project');", - "INSERT INTO ArtifactGroup (name, commonName, description) VALUES\n" + - "\t\t('com.paiondata.elide','Elide','The magical library powering this project');" - }) - void graphqlTest() { - given() - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .body("{ \"query\" : \"" + GraphQLDSL.document( - query( - selection( - field("group", - selections( - field("name"), - field("commonName"), - field("description") - ) - ) - ) - ) - ).toQuery() + "\" }" - ) - .when() - .post("/graphql/api/v1") - .then() - .body(equalTo(GraphQLDSL.document( - selection( - field( - "group", - selections( - field("name", "com.example.repository"), - field( "commonName", "Example Repository"), - field("description", "The code for this project") - ), - selections( - field("name", "com.paiondata.elide"), - field( "commonName", "Elide"), - field("description", "The magical library powering this project") - ) - ) - ) - ).toResponse())) - .statusCode(HttpStatus.SC_OK); - } -``` - -The complete set of static DSL operators for GraphQL can be found [here](https://github.com/paion-data/elide/blob/master/elide-test/src/main/java/com/paiondata/elide/test/graphql/GraphQLDSL.java). - -[elide-demo]: https://github.com/yahoo/elide-spring-boot-example -[elide-spring]: https://github.com/paion-data/elide/tree/master/elide-spring/elide-spring-boot-starter diff --git a/docs/docs/crud/img/graphiql.png b/docs/docs/crud/img/graphiql.png deleted file mode 100644 index c0ecda2b..00000000 Binary files a/docs/docs/crud/img/graphiql.png and /dev/null differ diff --git a/docs/docs/crud/img/instantiation-1.png b/docs/docs/crud/img/instantiation-1.png deleted file mode 100644 index dd352c6f..00000000 Binary files a/docs/docs/crud/img/instantiation-1.png and /dev/null differ diff --git a/docs/docs/crud/img/instantiation-2.png b/docs/docs/crud/img/instantiation-2.png deleted file mode 100644 index 5ecd4c57..00000000 Binary files a/docs/docs/crud/img/instantiation-2.png and /dev/null differ diff --git a/docs/docs/crud/index.mdx b/docs/docs/crud/index.mdx deleted file mode 100644 index a21b8958..00000000 --- a/docs/docs/crud/index.mdx +++ /dev/null @@ -1,298 +0,0 @@ ---- -sidebar_position: 1 -title: Getting Started -description: Standing up a CRUD webservice in a minute ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -:::caution - -Before proceeding, it is important to note that **we DO NOT support Spring/Spring Boot paradigm**. -[Jersey Webservice Template] runs as a **[JAX-RS]** webservice backed by its reference implementation [Jersey] running -as a WAR inside [Jetty] container. - -More info about difference between JAX-RS and Spring can be found in [this thread](https://stackoverflow.com/a/42955575) - -::: - -[![AWS EC2 min size][AWS EC2 min size]](https://aws.amazon.com/ec2/instance-types/) - -So You Want An API? -------------------- - -:::info Please make sure JDK 17, Maven, and Docker Engine have been installed 🤗 - -- We offer [instruction](../setup#installing-java--maven-on-mac) on how to install JDK 17 and Maven -- We also offer [links to Docker Engine installation](../setup#installing-docker-engine) - -::: - -Instantiating the Template --------------------------- - -Please visit [JWT GitHub] and either - -1. clone the repo with `git clone git@github.com:QubitPi/jersey-webservice-template.git` and switch to the `jpa-elide` - branch using `git checkout jpa-elide`, or -2. make it a template instantiation with our own webservice name by doing the following: - - ![Error loading instantiation-1.png](./img/instantiation-1.png) - - :::note - - Please make sure the "__Include all branches__" is checked - - ::: - - ![Error loading instantiation-2.png](./img/instantiation-2.png) - -Creating Models ---------------- - -[Jersey Webservice Template] models are some of the most important code in any webservice project. Our models are the -view of our data that we wish to expose. In this example we will be modeling a _book_ since most people have a -high-level familiarity with books in life. Our _Book_ model has been packaged as a **JAR** file in a -[separate Maven project](https://github.com/QubitPi/jersey-webservice-template-jpa-data-models). It has already been -published to Maven Central and will be installed in this tutorial by default so that user don't need to do anything -to set up the data model at this moment - -Running -------- - -With data models defined, can run _my-webservice_ - -```bash -cd my-webservice -mvn clean package --settings settings.xml.example -MODEL_PACKAGE_NAME=io.github.qubitpi.ws.jersey.template.models docker compose up --build --force-recreate -``` - -Note that the `settings.xml` is the [Maven config file for loading the data model](#loading-data-models) - -:::info - -- `io.github.qubitpi.ws.jersey.template.models` is the name of the model in the aforementioned -[data model project][jersey-webservice-template-jpa-data-models] -- The data model is loaded via a special-purpose Maven settings file called `settings.xml.example`, which instructs - _my-webservice_ to load [data models](#creating-models). It's essentially the regular __~/.m2/settings.xml__: - -::: - -If everything runs successfully, we should be able to see the following output at end of the command line output: - -```console -web-1 | 2024-06-24 06:55:18.427:INFO :oejsh.ContextHandler:main: Started o.e.j.w.WebAppContext@2892dae4{ROOT.war,/,file:///tmp/jetty-0_0_0_0-8080-ROOT_war-_-any-18385652298504253014/webapp/,AVAILABLE}{/jetty-base/webapps/ROOT.war} -web-1 | 2024-06-24 06:55:18.447:INFO :oejs.AbstractConnector:main: Started ServerConnector@56b48163{HTTP/1.1, (http/1.1)}{0.0.0.0:8080} -web-1 | 2024-06-24 06:55:18.459:INFO :oejs.Server:main: Started Server@71d44a3{STARTING}[11.0.15,sto=5000] @13768ms -``` - -A MySQL database container has also started alongside and is accessible via - -```console -mysql -h localhost -D elide -P 3306 --protocol=tcp -u root -proot -``` - -All data is persisted in a database called __elide__ - -Writing Data ------------- - -:::tip[Installing GraphiQL] - -GraphiQL offers a user-friendly UI for issuing GraphQL queries and displaying query responses. We can use it for the -API call below. - -- __Mac__ uses [Homebrew](https://formulae.brew.sh/cask/graphiql) - - ```console - brew install --cask graphiql - ``` - -- __Windows__ binary can be downloaded from the [release page](https://github.com/skevy/graphiql-app/releases). - Then install the downloaded `.exe` file -- __Linux__ users can follow the - [official documentation](https://github.com/skevy/graphiql-app?tab=readme-ov-file#linux-installation) - -When installed, the GraphiQL user interface looks like the following: - -![Error loading graphiql.png](./img/graphiql.png) - -::: - -### Inserting Data - -We have defined our views on the database and exposed those views over HTTP. Next let's use cURL to put data in the -database. - - - - ```bash - curl -X POST http://localhost:8080/v1/data/book \ - -H "Content-Type: application/vnd.api+json" \ - -H "Accept: application/vnd.api+json" \ - -d '{"data": {"type": "book", "attributes": { "title": "Pride and Prejudice" }}}' - ``` - - - ```bash - curl -X POST "http://localhost:8080/v1/data" \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -d '{ "query" : "mutation { book(op: UPSERT, data:{title: \"Pride & Prejudice\"}) { edges { node { id title } } } }" }' - ``` - - - ```graphql - mutation { - book(op: UPSERT, data:{title: "Pride & Prejudice"}) { - edges { - node { - id - title - } - } - } - } - ``` - - - -When we run that cURL call we should see a bunch of JSON returned, that is our newly inserted object! - - - - ```json - { - "data":{ - "type":"book", - "id":"1", - "attributes":{ - "title":"Pride and Prejudice" - } - } - } - ``` - - - ```json - { - "data":{ - "book":{ - "edges":[ - { - "node":{ - "id":"4", - "title":"Pride & Prejudice" - } - } - ] - } - } - } - ``` - - - -### Looking at Our Data - - - - - Simply open up our favorite browser and hit **http://localhost:8080/v1/data/book** - - -:::tip - -It is recommended to view result with some -[JSON formatter](https://chromewebstore.google.com/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa) browser -extension for better viewing experience - -::: - - - - ```bash - curl "http://localhost:8080/v1/data/book" - ``` - - - ```bash - curl -X POST "http://localhost:8080/v1/data" \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -d '{ "query" : "{ book { edges { node { id title } } } }" }' - ``` - - - ```graphql - { - book { - edges { - node { - id - title - } - } - } - } - ``` - - - -### Modifying Data - -Notice that, when we created it, we did not set any of the attributes of our new book record. Updating our data to help -our users is just as easy as it is to add new data. Let's update our model with the following cURL call. - - - - ```bash - curl -X PATCH http://localhost:8080/v1/data/book/1 \ - -H "Content-Type: application/vnd.api+json" \ - -H "Accept: application/vnd.api+json" \ - -d '{"data": {"type": "book", "id": "1", "attributes": { "title": "Emma" }}}' - ``` - - - ```bash - curl -X POST "http://localhost:8080/v1/data" \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -d '{ "query" : "mutation { book(op: UPSERT, data: {id: \"1\", title: \"Emma\"}) { edges { node { id title } } } }" }' - ``` - - - ```graphql - mutation { - book(op: UPSERT, data: {id: "1", title: "Emma"}) { - edges { - node { - id - title - } - } - } - } - ``` - - - -Troubleshooting ---------------- - -### Invalid API Version - -This could be the Elide version difference between used data model repository and Jersey Webservice Template. Ideally -they should use the exact same Elide version - -[AWS EC2 min size]: https://img.shields.io/badge/EC2-%E2%89%A5t2.small-FF9902?style=for-the-badge&logo=amazonec2&logoColor=white - -[JAX-RS]: https://jcp.org/en/jsr/detail?id=370 -[Jersey]: https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/index.html -[Jersey Webservice Template]: https://qubitpi.github.io/jersey-webservice-template/ -[Jetty]: https://eclipse.dev/jetty/ -[JWT GitHub]: https://github.com/QubitPi/jersey-webservice-template - -[jersey-webservice-template-jpa-data-models]: https://github.com/QubitPi/jersey-webservice-template-jpa-data-models diff --git a/docs/docs/setup.md b/docs/docs/setup.md index 20c20f49..71c6b505 100644 --- a/docs/docs/setup.md +++ b/docs/docs/setup.md @@ -126,6 +126,6 @@ If tabs still come out at 2 spaces when hitting TAB or Enter, not 4 spaces, try: [Jersey]: https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/index.html [Jersey Webservice Template]: https://qubitpi.github.io/jersey-webservice-template/ [jersey-webservice-template Dockerfile]: https://github.com/QubitPi/jersey-webservice-template/blob/master/Dockerfile -[Jetty]: https://eclipse.dev/jetty/ +[Jetty]: https://en.wikipedia.org/wiki/Jetty_(web_server) [style config]: https://github.com/QubitPi/jersey-webservice-template/blob/master/Jersey-Webservice-Template-Project-intellij-code-style.xml diff --git a/.github/actions/ci-setup/action.yml b/markdownlint.rb similarity index 72% rename from .github/actions/ci-setup/action.yml rename to markdownlint.rb index cda83144..d03c8b2b 100644 --- a/.github/actions/ci-setup/action.yml +++ b/markdownlint.rb @@ -12,11 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: 'setup' -description: 'CI setup, such as installing JDK' - -runs: - using: "composite" - steps: - - name: Set up JDK - uses: QubitPi/jersey-webservice-deployment-actions/.github/actions/jdk-setup@master +all +rule 'MD003', style: :setext_with_atx +rule 'MD004', style: :sublist +rule 'MD013', line_length: 120 +rule 'MD029', style: :ordered +rule 'MD026', punctuation: ".,;:!?"