|
# inLeague Development Manual (CFML)
|
|
# inLeague Development Manual (CFML)
|
|
|
|
|
|
This document governs best practices for writing CFML for inLeague applications (primarily, "the" inLeague application). It is distinct from the [Coding Style Guide](https://github.com/Ortus-Solutions/coding-standards "Ortus Solutions Style Guides") from Ortus that is concerned primarily with formatting and readability; this document is similar to [CFSnippets](https://cfsnippets.com/ "Michael Born's CFSnippets") -- a collection of solutions to development challenges sourced from our experience and the community.
|
|
This document governs best practices for writing CFML for inLeague applications (primarily, "the" inLeague application). It is distinct from the [Coding Style Guide](https://github.com/Ortus-Solutions/coding-standards "Ortus Solutions Style Guides") from Ortus that is concerned primarily with formatting and readability; this document is similar to [CFSnippets](https://cfsnippets.com/ "Michael Born's CFSnippets") -- a collection of solutions to development challenges sourced from our experience and the community.
|
|
|
|
|
|
# Table of Contents
|
|
# Table of Contents
|
|
|
|
|
|
1. [Function Architecture: Handlers, Services, Models](#function-architecture)
|
|
1. [Function Architecture: Handlers, Services, Models](#function-architecture)
|
|
2. [Writing API Endpoints](#writing-api-endpoints)
|
|
2. [Writing API Endpoints](#writing-api-endpoints)
|
|
3. [Documenting API Endpoints](#documenting-api-endpoints)
|
|
3. [Documenting API Endpoints](#documenting-api-endpoints)
|
|
4. [Request Security Object](#request-security-object)
|
|
4. [Request Security Object](#request-security-object)
|
|
|
|
5. Report Builder
|
|
## Function Architecture
|
|
|
|
|
|
## Function Architecture
|
|
### Function definitions
|
|
|
|
* **Return Type Mapping**: Functions should declare a return type without an extended path name (e.g. `public Response function doSomething()` and not `public api.models.Response function doSomething()`. This is because the full mapping for a component may not be consistent across different versions of Lucee.
|
|
### Function definitions
|
|
* **Return One Thing Or Don't Specify**: Functions should return a single type, but may return null. If a function can return either something or null, don't specify a return type in the method signature. Functions should never return two different, non-null types (e.g. a struct or an array)
|
|
|
|
* **Returning Metadata and Data**: If a function is returning both data and metadata about what the function did, use a **Response@api** object, as it has plumbing for error status, messages, and data, and it maps very well to either cbmessagebox results or API endpoints. When in doubt, return a `Response`.
|
|
* **Return Type Mapping**: Functions should declare a return type without an extended path name (e.g. `public Response function doSomething()` and not `public api.models.Response function doSomething()`. This is because the full mapping for a component may not be consistent across different versions of Lucee.
|
|
|
|
* **Return One Thing Or Don't Specify**: Functions should return a single type, but may return null. If a function can return either something or null, don't specify a return type in the method signature. Functions should never return two different, non-null types (e.g. a struct or an array)
|
|
### What to put in Handlers, Models, or Services
|
|
* **Returning Metadata and Data**: If a function is returning both data and metadata about what the function did, use a **Response@api** object, as it has plumbing for error status, messages, and data, and it maps very well to either cbmessagebox results or API endpoints. When in doubt, return a `Response`.
|
|
Handlers' job is to handle input. Their job is not to apply business rules, or even be aware of business rules. They are concerned with:
|
|
|
|
|
|
### What to put in Handlers, Models, or Services
|
|
* processing incoming data in the request scope
|
|
|
|
* either validating the request or else referring the request collection to a validation layer
|
|
Handlers' job is to handle input. Their job is not to apply business rules, or even be aware of business rules. They are concerned with:
|
|
* referring the validated request to the business object responsible for Doing The Thing
|
|
|
|
* returning or rendering a result to satisfy the request
|
|
* processing incoming data in the request scope
|
|
|
|
* either validating the request or else referring the request collection to a validation layer
|
|
(TODO)
|
|
* referring the validated request to the business object responsible for Doing The Thing
|
|
|
|
* returning or rendering a result to satisfy the request
|
|
## Writing API Endpoints
|
|
|
|
|
|
(TODO)
|
|
All API endpoints are Coldbox events in handlers extending **BaseHandler**. Each endpoint has the following considerations:
|
|
|
|
|
|
## Writing API Endpoints
|
|
1. [Authentication](#api-endpoint-authentication)
|
|
|
|
2. [Authorization](#api-endpoint-authorization)
|
|
All API endpoints are Coldbox events in handlers extending **BaseHandler**. Each endpoint has the following considerations:
|
|
3. [Routing](#api-endpoint-routing)
|
|
|
|
4. [Caching](#api-endpoint-caching)
|
|
1. [Authentication](#api-endpoint-authentication)
|
|
5. [REST Convention](#api-rest-convention)
|
|
2. [Authorization](#api-endpoint-authorization)
|
|
6. [Validation](#validating-api-requests)
|
|
3. [Routing](#api-endpoint-routing)
|
|
7. [Function](#api-endpoint-function)
|
|
4. [Caching](#api-endpoint-caching)
|
|
8. [Conventions (Expand)](#api-endpoint-conventions)
|
|
5. [REST Convention](#api-rest-convention)
|
|
9. [Conventions (Pagination)](#api-result-pagination)
|
|
6. [Validation](#validating-api-requests)
|
|
|
|
7. [Function](#api-endpoint-function)
|
|
### API Endpoint Authentication
|
|
8. [Conventions (Expand)](#api-endpoint-conventions)
|
|
|
|
9. [Conventions (Pagination)](#api-result-pagination)
|
|
The inLeague API supports several styles of authentication, but only one of them is appropriate for new endpoints:
|
|
|
|
|
|
### API Endpoint Authentication
|
|
* **JSON Web Tokens** (JWT): Specified by the handler's **preHandler** setting **prc.JWTAuth = 1**. Is also triggered by any incoming request containing the header **Authorization**, whether or not the handler explicitly declares **prc.JWTAuth**. All new API methods should use this method.
|
|
|
|
* Other mechanisms: Authorize.net automated API calls and AYSO automated API calls have a unique style of authentication. These can be reviewed in the relevant handlers and interceptors.
|
|
The inLeague API supports several styles of authentication, but only one of them is appropriate for new endpoints:
|
|
|
|
|
|
### API Endpoint Authorization
|
|
* **JSON Web Tokens** (JWT): Specified by the handler's **preHandler** setting **prc.JWTAuth = 1**. Is also triggered by any incoming request containing the header **Authorization**, whether or not the handler explicitly declares **prc.JWTAuth**. All new API methods should use this method.
|
|
|
|
* Other mechanisms: Authorize.net automated API calls and AYSO automated API calls have a unique style of authentication. These can be reviewed in the relevant handlers and interceptors.
|
|
API Handler endpoints should be annotated with the **secured** (or **x-secured**) values matching the AuthService context desired, e,g.
|
|
|
|
|
|
### API Endpoint Authorization
|
|
`function index ( event, rc, prc ) secured="PlayerAdmin,DivisionDirector" {
|
|
|
|
....function
|
|
API Handler endpoints should be annotated with the **secured** (or **x-secured**) values matching the AuthService context desired, e,g.
|
|
}`
|
|
|
|
|
|
`function index ( event, rc, prc ) secured="PlayerAdmin,DivisionDirector" { ....function }`
|
|
The above endpoint will reject any request from a user not authorized in the PlayerAdmin or DivisionDirector roles.
|
|
|
|
|
|
The above endpoint will reject any request from a user not authorized in the PlayerAdmin or DivisionDirector roles.
|
|
*Neglecting to include the secured attribute in an API function definition will make the endpoint available to any authenticated user.*
|
|
|
|
|
|
_Neglecting to include the secured attribute in an API function definition will make the endpoint available to any authenticated user._
|
|
### API Endpoint Routing
|
|
|
|
|
|
### API Endpoint Routing
|
|
Prior to April 2020, the current API routing scheme was simple but disorganized due to all API routes living in the `ModuleConfig` for the `v1` module. For instance, the API endpoint to list all available seasons using the `seasons` handler with the `list` function is:
|
|
|
|
|
|
Prior to April 2020, the current API routing scheme was simple but disorganized due to all API routes living in the `ModuleConfig` for the `v1` module. For instance, the API endpoint to list all available seasons using the `seasons` handler with the `list` function is:
|
|
```.route( "/seasons/list" )
|
|
|
|
.withAction( {
|
|
```.route(:"/seasons/list" )
|
|
GET = "list"
|
|
.withAction( {
|
|
} )
|
|
GET = "list"
|
|
.toHandler( "seasons" )`
|
|
} )
|
|
```
|
|
.toHandler( "seasons" )`
|
|
|
|
```
|
|
Much better is the example of `/vendorIntegration` in which the same routes may be defined, but the definitions live in the relevant module's router and not the API ModuleConfig, e.g.
|
|
|
|
|
|
Much better is the example of `/vendorIntegration` in which the same routes may be defined, but the definitions live in the relevant module's router and not the API ModuleConfig, e.g.
|
|
```
|
|
|
|
route( '/vendorIntegration' )
|
|
```
|
|
.toModuleRouting( 'vendorIntegration' )
|
|
route( '/vendorIntegration' )
|
|
```
|
|
.toModuleRouting( 'vendorIntegration' )
|
|
|
|
```
|
|
Which then maps to the vendorIntegration's ModuleConfig.cfc routing:
|
|
|
|
|
|
Which then maps to the vendorIntegration's ModuleConfig.cfc routing:
|
|
```router
|
|
|
|
.route( "/" )
|
|
```router
|
|
.to( "Home.index" )
|
|
.route( "/" )
|
|
.route( "/certifications" )
|
|
.to( "Home.index" )
|
|
.toHandler( "vendorCertificationsAPI" )
|
|
.route( "/certifications" )
|
|
.route( "/home/:action" )
|
|
.toHandler( "vendorCertificationsAPI" )
|
|
.toHandler( "Home" );`
|
|
.route( "/home/:action" )
|
|
```
|
|
.toHandler( "Home" );`
|
|
|
|
```
|
|
This module's routing has a traditional Coldbox handler of `Home` handling regular requests, and an API handler of `vendorCertificationsAPI`, such that the following endpoint will route to it:
|
|
|
|
|
|
This module's routing has a traditional Coldbox handler of `Home` handling regular requests, and an API handler of `vendorCertificationsAPI`, such that the following endpoint will route to it:
|
|
`https://api.inleague.io/v1/vendorIntegration/certifications`
|
|
|
|
|
|
`https://api.inleague.io/v1/vendorIntegration/certifications`
|
|
### API Endpoint Caching
|
|
|
|
|
|
### API Endpoint Caching
|
|
[Coldbox Event Caching](https://coldbox.ortusbooks.com/the-basics/event-handlers/event-caching) works by caching the literal output of an event in the Coldbox template cache (which, for inLeague, is in Redis). With any API-heavy application, this is a critical component to relieve pressure on the back-end API instance. There are two ways to implement event caching. For an example, look at the **authenticate** handler function **lookupUser**, a simple function that retrieves a user based on a username (which, for inLeague, is an email address). **Very Important**: This function is not an API endpoint: it is a helper function used by other API endpoints, like the `authenticate` `GET` endpoint that serves as our login handler. **Do not ever cache API endpoints requiring any kind of authentication** because the relevant `aroundHandler` and interceptors do not fire when a Coldbox event is cached. This means that, for now, API endpoints should have two functions: an internal (private) `_endpoint()` event to go with the public `endpoint()` event, where only the private event is cached.
|
|
|
|
|
|
[Coldbox Event Caching](https://coldbox.ortusbooks.com/the-basics/event-handlers/event-caching) works by caching the literal output of an event in the Coldbox template cache (which, for inLeague, is in Redis). With any API-heavy application, this is a critical component to relieve pressure on the back-end API instance. There are two ways to implement event caching. For an example, look at the **authenticate** handler function **lookupUser**, a simple function that retrieves a user based on a username (which, for inLeague, is an email address). **Very Important**: This function is not an API endpoint: it is a helper function used by other API endpoints, like the `authenticate` `GET` endpoint that serves as our login handler. **Do not ever cache API endpoints requiring any kind of authentication** because the relevant `aroundHandler` and interceptors do not fire when a Coldbox event is cached. This means that, for now, API endpoints should have two functions: an internal (private) `_endpoint()` event to go with the public `endpoint()` event, where only the private event is cached.
|
|
1. **Event function definition**: Specify `cache=true` and `cacheTimeout=someNumber` in the event, e.g.
|
|
|
|
|
|
1. **Event function definition**: Specify `cache=true` and `cacheTimeout=someNumber` in the event, e.g.
|
|
`private function lookupUser( event, rc, prc, string username ) cache=true cacheTimeout=10 {`
|
|
|
|
|
|
`private function lookupUser( event, rc, prc, string username ) cache=true cacheTimeout=10 {`
|
|
2. **runEvent()**: When calling a Coldbox event internally, you can specify (or override) cache values using the `cache` and `cacheTimeout` arguments. You must also specify `prePostExempt=true` so that it does not try to execute the aroundHandler, which would fail on a private function.
|
|
|
|
|
|
2. **runEvent()**: When calling a Coldbox event internally, you can specify (or override) cache values using the `cache` and `cacheTimeout` arguments. You must also specify `prePostExempt=true` so that it does not try to execute the aroundHandler, which would fail on a private function.
|
|
There are two considerations to keep in mind when using event caching:
|
|
|
|
|
|
There are two considerations to keep in mind when using event caching:
|
|
1. **Every unique combination of arguments is cached separately**. In our `lookupUser` example, passing a different `username` argument to the event will create a new cache key for that event and that username. Changing any part of the query string or event arguments will result in a new cache key.
|
|
|
|
|
|
1. **Every unique combination of arguments is cached separately**. In our `lookupUser` example, passing a different `username` argument to the event will create a new cache key for that event and that username. Changing any part of the query string or event arguments will result in a new cache key.
|
|
2. **Cache Eviction**. Whenever the underlying data changes, it is essential to update the cache. This is best done through the convention of an interceptor, e.g. a Quick `postSave()` interception point that will clear all related API events. The example of `lookupUser` has a scenario (however unlikely) where a request is made for a login that doesn't yet exist, but is then added before the ten minute timeout expires. We would clear the relevant cache with this snipper:
|
|
2. **Cache Eviction**. Whenever the underlying data changes, it is essential to update the cache. This is best done through the convention of an interceptor, e.g. a Quick `postSave()` interception point that will clear all related API events. The example of `lookupUser` has a scenario (however unlikely) where a request is made for a login that doesn't yet exist, but is then added before the ten minute timeout expires. We would clear the relevant cache with this snipper:
|
|
|
|
|
|
`cachebox.getCache( "template" ).clearEvent('v1:authenticate.lookupUser','username=' & arguments.username )`
|
|
`cachebox.getCache( "template" ).clearEvent('v1:authenticate.lookupUser','username=' & arguments.username )`
|
|
|
|
|
|
## API REST Convention
|
|
## API REST Convention
|
|
|
|
|
|
API endpoints should respond only to the appropriate HTTP verbs, which should always conform to REST conventions. Note that they are conventions and not standards, because REST tends to advise rather than require. This concerns both the name of the resource and the path. See the [REST Cheat Sheet](https://devhints.io/rest-api) and [REST Resource Naming](https://restfulapi.net/resource-naming/) for more information.
|
|
API endpoints should respond only to the appropriate HTTP verbs, which should always conform to REST conventions. Note that they are conventions and not standards, because REST tends to advise rather than require. This concerns both the name of the resource and the path. See the [REST Cheat Sheet](https://devhints.io/rest-api) and [REST Resource Naming](https://restfulapi.net/resource-naming/) for more information.
|
|
|
|
|
|
## Validating API Requests
|
|
## Validating API Requests
|
|
|
|
|
|
When validating parameters of an API request, there is a quick, lightweight strategy with some limitations, and a more thorough strategy that requires a populated domain object. There is also a hybrid.
|
|
When validating parameters of an API request, there is a quick, lightweight strategy with some limitations, and a more thorough strategy that requires a populated domain object. There is also a hybrid.
|
|
|
|
|
|
### Lightweight API Validation (example: /api/public/handlers/user.cfc, **existsByName**)
|
|
### Lightweight API Validation (example: /api/public/handlers/user.cfc, **existsByName**)
|
|
|
|
|
|
The **existsByName** public API endpoint requires a valid date of birth and a last name field. Because it is an API endpoint, it will automatically catch a `ValidationException` (see the API BaseHandler's `catch( ValidationException e )` section).
|
|
The **existsByName** public API endpoint requires a valid date of birth and a last name field. Because it is an API endpoint, it will automatically catch a `ValidationException` (see the API BaseHandler's `catch( ValidationException e )` section).
|
|
|
|
|
|
Specify the validation constraints right in the handler:
|
|
Specify the validation constraints right in the handler:
|
|
|
|
|
|
```
|
|
```
|
|
public function existsByName(event, rc, prc) {
|
|
public function existsByName(event, rc, prc) {
|
|
validateOrFail(
|
|
validateOrFail(
|
|
target = event.getCollection(), // this could just be **rc** but event.getCollection() is a little more explicit
|
|
target = event.getCollection(), // this could just be **rc** but event.getCollection() is a little more explicit
|
|
constraints = {
|
|
constraints = {
|
|
dob: {
|
|
dob: {
|
|
required: true,
|
|
required: true,
|
|
type: 'date',
|
|
type: 'date',
|
|
typeMessage: 'Invalid date of birth. Please providate a US Date (mm/dd/yyyy)'
|
|
typeMessage: 'Invalid date of birth. Please providate a US Date (mm/dd/yyyy)'
|
|
},
|
|
},
|
|
lastName: {required: true}
|
|
lastName: {required: true}
|
|
}
|
|
}
|
|
);
|
|
);
|
|
...(rest of function)
|
|
...(rest of function)
|
|
```
|
|
```
|
|
|
|
|
|
The advantage to in-line validation is that it is quick and concise. The primary disadvantage is that validation cannot refer to domain business logic: for example, if a field depends on the value of another field that may or not be provided in one request, or validation depends upon a complex business rule that is defined on an ORM component, it doesn't make sense to reproduce that validation rule in a handler.
|
|
The advantage to in-line validation is that it is quick and concise. The primary disadvantage is that validation cannot refer to domain business logic: for example, if a field depends on the value of another field that may or not be provided in one request, or validation depends upon a complex business rule that is defined on an ORM component, it doesn't make sense to reproduce that validation rule in a handler.
|
|
|
|
|
|
### Model API Validation (example: /api/v1/handlers/content.cfc, **overrideUpdate**)
|
|
### Model API Validation (example: /api/v1/handlers/content.cfc, **overrideUpdate**)
|
|
|
|
|
|
Model validation is the bread and butter of the cbValidation library: all of the rules are stored in a `this.constraints` property on the domain object (component). Validation rules be simple, user-defined functions, or custom-built validators.
|
|
Model validation is the bread and butter of the cbValidation library: all of the rules are stored in a `this.constraints` property on the domain object (component). Validation rules be simple, user-defined functions, or custom-built validators.
|
|
|
|
|
|
The disadvantage to model validation is that you must have a hydrated model object before you can perform it. This requires the overhead of instantiating a Quick entity in Wirebox, and using `.fill()` or `.hydrate()` to set values from a source that has not yet been validated -- for example, if you try to `.fill()` a property with `type="date"` from a value that is not a date, CF will throw an exception. This could be a reason not to `type` your property, or it could be a reason to use the hybrid model (below).
|
|
The disadvantage to model validation is that you must have a hydrated model object before you can perform it. This requires the overhead of instantiating a Quick entity in Wirebox, and using `.fill()` or `.hydrate()` to set values from a source that has not yet been validated -- for example, if you try to `.fill()` a property with `type="date"` from a value that is not a date, CF will throw an exception. This could be a reason not to `type` your property, or it could be a reason to use the hybrid model (below).
|
|
|
|
|
|
### Hybrid API Validation (example: /api/public/handlers/user.cfc, **create**)
|
|
### Hybrid API Validation (example: /api/public/handlers/user.cfc, **create**)
|
|
|
|
|
|
This approach leverages `constraints` on a domain object, but pulls those constraints into in-line, handler validation. It allows for some integration with models and reduces the likelihood of business rule duplication. It doesn't allow all the possible constraints that model validation does (e.g. method) but in-line UDFs or custom validators can be used.
|
|
This approach leverages `constraints` on a domain object, but pulls those constraints into in-line, handler validation. It allows for some integration with models and reduces the likelihood of business rule duplication. It doesn't allow all the possible constraints that model validation does (e.g. method) but in-line UDFs or custom validators can be used.
|
|
|
|
|
|
## API Endpoint Function
|
|
## API Endpoint Function
|
|
|
|
|
|
The content of a single Coldbox event should be as short as possible to achieve the desired results. This is an area of concern for **DRY**: If there is a significant amount of business logic going on, that should take place in a model or service layer and not the API handler, unless it is absolutely, positively certain that the business logic will only ever be used in one place. Even then, envision a subsequent version of your API: if you wanted to take your endpoint and update it from **v1** to **v2**, you would ideally only concern yourself with writing the changes in business logic in the **v2** endpoint. If you would have to replicate a lot of unchanged code from your **v1** handler, probably that portion should have gone into a service layer (whether on a model object or a separate service object).
|
|
The content of a single Coldbox event should be as short as possible to achieve the desired results. This is an area of concern for **DRY**: If there is a significant amount of business logic going on, that should take place in a model or service layer and not the API handler, unless it is absolutely, positively certain that the business logic will only ever be used in one place. Even then, envision a subsequent version of your API: if you wanted to take your endpoint and update it from **v1** to **v2**, you would ideally only concern yourself with writing the changes in business logic in the **v2** endpoint. If you would have to replicate a lot of unchanged code from your **v1** handler, probably that portion should have gone into a service layer (whether on a model object or a separate service object).
|
|
|
|
|
|
Remember: **Fat models, skinny handlers.**
|
|
Remember: **Fat models, skinny handlers.**
|
|
|
|
|
|
## API Endpoint Conventions
|
|
## API Endpoint Conventions
|
|
|
|
|
|
### Expand
|
|
### Expand
|
|
|
|
|
|
As of October 2020, the inLeague API supports an **expand** array parameter to all requests. The convention is borrowed from Stripe's [Expanding Requests](https://stripe.com/docs/api/expanding_objects) feature as a mechanism for an API consumer to request additional data from an endpoint without having to establish a whole new endpoint.
|
|
As of October 2020, the inLeague API supports an **expand** array parameter to all requests. The convention is borrowed from Stripe's [Expanding Requests](https://stripe.com/docs/api/expanding_objects) feature as a mechanism for an API consumer to request additional data from an endpoint without having to establish a whole new endpoint.
|
|
|
|
|
|
To request one or more expandable fields, provide an `expand` parameter as an array of values in the body (for `POST` or `PUT` or `PATCH`) or as a comma-delimited list in the `expand` query parameter for `GET` requests.
|
|
To request one or more expandable fields, provide an `expand` parameter as an array of values in the body (for `POST` or `PUT` or `PATCH`) or as a comma-delimited list in the `expand` query parameter for `GET` requests.
|
|
|
|
|
|
To enable an expandable field on the back end, see `qInvoiceInstance.cfc` as an example. The `expandable` property on `this.memento` contains an array of possible expandable items. When calling for the memento via `.asMemento()` or `.getMemento()`, it is necessary to specify an `includes` argument, as with the `invoice.cfc` `view` handler:
|
|
To enable an expandable field on the back end, see `qInvoiceInstance.cfc` as an example. The `expandable` property on `this.memento` contains an array of possible expandable items. When calling for the memento via `.asMemento()` or `.getMemento()`, it is necessary to specify an `includes` argument, as with the `invoice.cfc` `view` handler:
|
|
|
|
|
|
```
|
|
```
|
|
invoice.getMemento( includes = invoice.getExpandedIncludes( rc.expand ) )
|
|
invoice.getMemento( includes = invoice.getExpandedIncludes( rc.expand ) )
|
|
```
|
|
```
|
|
|
|
|
|
The default behavior of `getExpandedIncludes()` is to append any values from the `expand` parameter to the `defaultIncludes` of the memento. If you do not want to use the `defaultIncludes`, you can specify a second argument called `includes` to `getExpandedIncludes()` and it will use that value in place of the defaultIncludes.
|
|
The default behavior of `getExpandedIncludes()` is to append any values from the `expand` parameter to the `defaultIncludes` of the memento. If you do not want to use the `defaultIncludes`, you can specify a second argument called `includes` to `getExpandedIncludes()` and it will use that value in place of the defaultIncludes.
|
|
|
|
|
|
`getExpandedIncludes()` is defined on the inLegaue base Quick entity.
|
|
`getExpandedIncludes()` is defined on the inLegaue base Quick entity.
|
|
|
|
|
|
#### Expandables with custom authorization
|
|
#### Expandables with custom authorization
|
|
By default, there are no authorization checks performed when requesting expandables; the API assumes that, if you have access to the endpoint invoking the expandable, you have access to the expandable. A simple, additional mechanism is available by specifying a value in the `expandableAuth` struct on the memento whose key matches the expandable name, e.g.
|
|
|
|
|
|
By default, there are no authorization checks performed when requesting expandables; the API assumes that, if you have access to the endpoint invoking the expandable, you have access to the expandable. A simple, additional mechanism is available by specifying a value in the `expandableAuth` struct on the memento whose key matches the expandable name, e.g.
|
|
```
|
|
|
|
this.memento.expandable = [ "transactions" ]
|
|
```
|
|
this.memento.expandableAuth = { 'transactions' : 'Registrar' }
|
|
this.memento.expandable = [ "transactions" ]
|
|
```
|
|
this.memento.expandableAuth = { 'transactions' : 'Registrar' }
|
|
|
|
```
|
|
This will ignore the transactions expandable for any non-registrar user.
|
|
|
|
|
|
This will ignore the transactions expandable for any non-registrar user.
|
|
#### Expandables that alter the object query
|
|
|
|
|
|
#### Expandables that alter the object query
|
|
The `qInvoiceInstance` example adds additional database queries, because the expanded objects are loaded separately, after the memento call. If an expandable needs to invoke a Quick scope to alter the initial query rather than performing additional queries, it can do so by passing `.withExpandables( rc.expand )` to the `getInstance` call prior to `.get()` or `.first()`. See `user.cfc:_view()` for an example: if `isSafeHavenCertified` is passed through the `expand` parameter, the inLeague Base Entity will check for a `scopeWithIsSafeHavenCertified` function on the entity and invoke it as part of the `.withExpandables()` call.
|
|
|
|
|
|
The `qInvoiceInstance` example adds additional database queries, because the expanded objects are loaded separately, after the memento call. If an expandable needs to invoke a Quick scope to alter the initial query rather than performing additional queries, it can do so by passing `.withExpandables( rc.expand )` to the `getInstance` call prior to `.get()` or `.first()`. See `user.cfc:_view()` for an example: if `isSafeHavenCertified` is passed through the `expand` parameter, the inLeague Base Entity will check for a `scopeWithIsSafeHavenCertified` function on the entity and invoke it as part of the `.withExpandables()` call.
|
|
Only explicitly named expandables will be invoked, and only if `.withExpandables( rc.expand )` is present in the object getter. The naming convention of `scopeWith{Expandable}` must be obeyed.
|
|
|
|
|
|
Only explicitly named expandables will be invoked, and only if `.withExpandables( rc.expand )` is present in the object getter. The naming convention of `scopeWith{Expandable}` must be obeyed.
|
|
## Documenting API Endpoints
|
|
|
|
|
|
## Documenting API Endpoints
|
|
inLeague uses [cbSwagger](https://forgebox.io/view/cbswagger) for API documentation according to the OpenAPI standard. API routes are documented in Javadoc-style "blocks" above the handler function definition, e.g.
|
|
|
|
|
|
inLeague uses [cbSwagger](https://forgebox.io/view/cbswagger) for API documentation according to the OpenAPI standard. API routes are documented in Javadoc-style "blocks" above the handler function definition, e.g.
|
|
```
|
|
|
|
/**
|
|
```
|
|
* /api/v1/contentChunk GET (ID)
|
|
/**
|
|
* @hint Retrieve a content chunk by chunkID. Overrides are applied automatically unless explicitly disabled.
|
|
* /api/v1/contentChunk GET (ID)
|
|
* @param-chunkID { "schema":{ "type":"integer" }, "in":"path", "description" : "Integer Chunk ID to retrieve." }
|
|
* @hint Retrieve a content chunk by chunkID. Overrides are applied automatically unless explicitly disabled.
|
|
* @param-override { "schema":{ "type": "boolean", "default" : true }, "in" : "query", "description" : "Applies client overrides if they exist." }
|
|
* @param-chunkID { "schema":{ "type":"integer" }, "in":"path", "description" : "Integer Chunk ID to retrieve." }
|
|
* @param-obeyDates { "schema" : { "type" : "boolean", "default" : true }, "in" : "query", "description" : "If false, ignores content embargo dates. Requires inLeague authorization." }
|
|
* @param-override { "schema":{ "type": "boolean", "default" : true }, "in" : "query", "description" : "Applies client overrides if they exist." }
|
|
* @responses ~contentChunk/get.response.yml
|
|
* @param-obeyDates { "schema" : { "type" : "boolean", "default" : true }, "in" : "query", "description" : "If false, ignores content embargo dates. Requires inLeague authorization." }
|
|
*/
|
|
* @responses ~contentChunk/get.response.yml
|
|
|
|
*/
|
|
public function show( event, rc, prc ) {
|
|
|
|
... function definition
|
|
public function show( event, rc, prc ) {
|
|
}
|
|
... function definition
|
|
```
|
|
}
|
|
|
|
```
|
|
1. The first comment line is not visible in the docs; it is purely an in-line comment for developers reading the handler code.
|
|
|
|
2. The @hint comment is displayed in the header for the endpoint documentation.
|
|
1. The first comment line is not visible in the docs; it is purely an in-line comment for developers reading the handler code.
|
|
3. The request structure for many simple GET requests may be described in-line (as in this example) as opposed to an external schema definition using the `@requestBody` declaration. Any request with a real "body" (e.g. PUT, POST) should use `@requestBody`.
|
|
2. The @hint comment is displayed in the header for the endpoint documentation.
|
|
4. When describing parameters in-line, they still must conform to the syntax above to specify whether the parameter is in the path (`/foo/{GUID}`) or a query parameter (`/foo?someID={GUID}`)
|
|
3. The request structure for many simple GET requests may be described in-line (as in this example) as opposed to an external schema definition using the `@requestBody` declaration. Any request with a real "body" (e.g. PUT, POST) should use `@requestBody`.
|
|
5. External schemas are located in the `/resources/api` folder and grouped in subfolders according to the parent domain object. They should be written in YAML (not JSON, whenever possible) and re-used -- do not repeat schema definitions.
|
|
4. When describing parameters in-line, they still must conform to the syntax above to specify whether the parameter is in the path (`/foo/{GUID}`) or a query parameter (`/foo?someID={GUID}`)
|
|
|
|
5. External schemas are located in the `/resources/api` folder and grouped in subfolders according to the parent domain object. They should be written in YAML (not JSON, whenever possible) and re-used -- do not repeat schema definitions.
|
|
# Request Security Object
|
|
|
|
|
|
# Request Security Object
|
|
The request security object is generated after a successful authentication and then stored in the cache layer (Redis). It is a `Security.cfc` object that is hydrated from the cache at the beginning of each request, whether to the API or the legacy application.
|
|
|
|
|
|
The request security object is generated after a successful authentication and then stored in the cache layer (Redis). It is a `Security.cfc` object that is hydrated from the cache at the beginning of each request, whether to the API or the legacy application.
|
|
The security object is available to the application engine as `request.securityObj` and exists for the life of the request. It is available to the front-end application as the result of the `authenticate` endpoint and should be cached locally, and then refreshed whenever the authenticated session is renewed.
|
|
|
|
|
|
The security object is available to the application engine as `request.securityObj` and exists for the life of the request. It is available to the front-end application as the result of the `authenticate` endpoint and should be cached locally, and then refreshed whenever the authenticated session is renewed.
|
|
The request object contains commonly-used fields for the logged-in user to reduce the need to pull the full user object from the database. Consult the API documentation or `Security.cfc` for a full list.
|
|
|
|
|
|
The request object contains commonly-used fields for the logged-in user to reduce the need to pull the full user object from the database. Consult the API documentation or `Security.cfc` for a full list.
|
|
## Retrieving the User Object for a Request
|
|
|
|
|
|
## Retrieving the User Object for a Request
|
|
In the event that the `qUser` object is needed at any point in the request on the back end, load it with the `getUser()` function on the security object so that it will be cached in that object and remain available for the duration of the request.
|
|
|
|
|
|
In the event that the `qUser` object is needed at any point in the request on the back end, load it with the `getUser()` function on the security object so that it will be cached in that object and remain available for the duration of the request.
|
|
## API Result Pagination
|
|
|
|
|
|
## API Result Pagination
|
|
The inLeague API Response object was originally based on Coldbox 5's HMVC template. We have back-ported the addition of the pagination struct in CB6. The handler must have `prc.pagination` enabled ((`true`)), and then any data-execution functions must use Quick or QB's `.paginate()` functions, and `setDataWithPagination()` on the Response object instead of `setData()`. See this commit for an example in `invoice.cfc`: https://gitlab.inleague.io/inLeague/inLeague/-/commit/d8557f871fa3ac6cefdb3fa01b3d36976d1c0b23 |
|
|
|
|
|
The inLeague API Response object was originally based on Coldbox 5's HMVC template. We have back-ported the addition of the pagination struct in CB6. The handler must have `prc.pagination` enabled ((`true`)), and then any data-execution functions must use Quick or QB's `.paginate()` functions, and `setDataWithPagination()` on the Response object instead of `setData()`. See this commit for an example in `invoice.cfc`: https://gitlab.inleague.io/inLeague/inLeague/-/commit/d8557f871fa3ac6cefdb3fa01b3d36976d1c0b23
|
|
|
|
|
|
|
|
# Report Builder (v4: 2024+)
|
|
|
|
|
|
|
|
The Report Builder is the library service for all reportable entities within inLeague and consists of:
|
|
|
|
|
|
|
|
* **Common Entities** that form the basis of each report: Players, Registered Players, Volunteers, Teams, Games, Volunteer Points, Score Transactions (e.g. yellow cards), and financial transactions. Some entities share data (e.g. **PlayerCommon** contains filters and output columns common to both Players and Registered Players).
|
|
|
|
* Entities map to a Quick 'root entity' (e.g. `/reportBuilder/CommonEntities/Player.cfc:getFreshEntity()` upon which all operations are performed, before dropping to `.asQuery()` and returning an array of structs
|
|
|
|
* **Resolvers** on each entity that map to data fields containing one or both of a **where resolver** (aka 'filter') that governs the criteria of the report (e.g. 'all (entities) where (field) is (value) and a **select resolver** that governs the display.
|
|
|
|
* **Utilities** for generating and evaluating resolvers and populating select lists |
|
|
|
\ No newline at end of file |