orbitalapi / demos

A series of demos, showing features of Orbital and TaxiQL
https://orbitalhq.com
Apache License 2.0
1 stars 1 forks source link

Brain dump of demos #7

Open martypitt opened 1 month ago

martypitt commented 1 month ago

Add comments to this issue to capture demos that we need to build. We'll review, then split them out into seperate issues

martypitt commented 1 month ago

This is a dump of interesting snippets we need to document / showcase in Taxi playground.

Add to this rough 'n' ready whenever we encounter something that we want to ensure is shown.

One's we've built on-the-fly:

can use a variable from a saved query in a field projection

      query FindSomeFilms( starring : PersonName ) {
         find { Film[] } as  {
            title: Title
            starring: filter(Actor[], (PersonName) -> PersonName == starring) as {
              name : PersonName
           }[]
         }[]
      }

Filtering a stream using .filterEach()

Defining / refining scope in a projection:

assigning a name to the scope:

See can project using a named scope at the top level

find { Film[] } as (film:Film) -> {
   movieName : film.title
}[]

Refining to a property:

see: a projection can refine whats in scope

            model Film {
               title : Title inherits String
               cast : Actor[]
            }
            model Actor {
               name : Name inherits String
            }

find { Film } as (Actor[]) -> {
  actorName : Name
  filmTitle : Title // should be null, as it's out-of-scope on Actor
}[]

Refining using a function

see: expressions in projection scopes can trigger discovery:

         find { Film } as (first(Actor[])) -> { // note that film has been removed from the scope...
            title : Title //... so we expect this isn't discoverable.
            starring : ActorName
         }

Alternatively, with film in scope:

         find { Film } as (Film, first(Actor[])) -> { // Here, Film is in scope...
            title : Title // .. so this is knowable
            starring : ActorName
         }

not sure what to call this:

         find { Film[] } as (film:Film) -> {
               title : FilmTitle
               star : singleBy(film.cast, (Actor) -> Actor::ActorId, film.headliner) as (actor:Actor) -> {
                  name : actor.name
                  title : film.title
               }
            }[]

Note - if we make singleBy an extension function (and we should), that gets simplified to:

         find { Film[] } as (film:Film) -> {
               title : FilmTitle
               star : film.cast.singleBy((Actor) -> Actor::ActorId, film.headliner) as (actor:Actor) -> {
                  name : actor.name
                  title : film.title
               }
            }[]

Projecting an inline array:

see ProjectionsOnArraysTest.wtf:

            type FilmId inherits Int
            model Film {
               title : FilmTitle inherits String
               cast : CastId inherits Int
            }
            model Actor {
               name : PersonName inherits String
            }
            model FilmCast {
               actors : Actor[]
            }
            service Movies {
               operation findFilm(FilmId):Film
               operation getCast(CastId):FilmCast
            }

Queried, loads from multiple services:

         given { FilmId = 1 }
         find { Film } as {
            actor : first(Actor[]) as {
               firstName : PersonName
            }
         }

Scoped params on tests:

Passing a filmId as input to query

query FindFilm( filmId : FilmId ) {
   find { Film( FilmId == filmId ) }
}

Casting

see: can use a cast expression against a parameter in a given clause

         type PersonId inherits String
         type HumanId inherits String

         query findPerson(personId : PersonId) {
            given { humanId : HumanId = (HumanId) personId }
            find { human : HumanId }
         }

Casting in a given clause:

see: can use a cast expression in a given clause

         type PersonId inherits String
         type HumanId inherits String

            given { humanId : HumanId = (HumanId) "123" }
            find { human : HumanId }

Using a query param in a function

In this example, we're filtering at the top level:

     query FindSomeFilms( starring : PersonName ) {
      find { Film[] } as (filter(Actor[], (PersonName) -> PersonName == starring)) -> {
         name: PersonName
      }[]

(see: can use a variable from a saved query in projection type expression)

can also filter at the field level:

      query FindSomeFilms( starring : PersonName ) {
         find { Film[] } as  {
            title: Title
            starring: filter(Actor[], (PersonName) -> PersonName == starring) as {
              name : PersonName
           }[]
         }[]
      }

(see: can use a variable from a saved query in a field projection)

Collections

Can use properties of collections to discover other collections:

see: can populate a collection attrib with a value returned from a service

         model Person {
            @Id
            id : PersonId inherits Int
            name : PersonName inherits String
         }
         model Friend inherits Person
         service Foo {
            operation findAllPeople():PersonId[]
            operation findPerson(PersonId):Person
            operation findAllFriends(PersonId):Friend[]
         }

query:

find { PersonId[] } as {
 name : PersonName
  friends : Friend[]
}[]

retrurns:

[
  {
    "name": "Doug",
    "friends": [
      { "id": 1, "name": "Jimmy" },
      { "id": 2, "name": "Jack" }
    ]
  }
]

Using ids in collections

see: given an array of discovered values, ids present in those arrays can look up attributes from other types

Given a movie:

         model Movie {
            title : MovieTitle inherits String
            cast : Cast
         }
         model Cast {
            actors : ActorId[]
         }

And an actor:

         model Actor {
            @Id
            id : ActorId
            name : ActorName inherits String
         }

Data is returned from seperate services:

         service MovieService {
            operation findAllMovies():Movie[]
            operation findActor(ActorId):Actor
         }

We can traverse the collections:

find { Movie[] } as {
   movieTitle: MovieTitle
   actors: ActorName[]
}

Here's that example as a Voyager example:

import {StubQueryMessageWithSlug} from "../../app/services/query.service";

export const example: StubQueryMessageWithSlug = {
   "title": "Title goes here",
   "slug": "slug-goes-here",
   "query": {
   "schema": ` // Given a movie:

 type ActorId inherits Int
 model Movie {
    title : MovieTitle inherits String
    cast : {
        actors: ActorId[]
    }
 }

// And an actor:
 model Actor {
    @Id
    id : ActorId
    name : ActorName inherits String
 }

// Data is returned from different APIs:
 service MovieService {
    operation findAllMovies():Movie[]
    operation findActor(ActorId):Actor
 }
`,
   "query": `
// We can traverse these collections:
find { Movie[] } as {
   movieTitle: MovieTitle
   actors: ActorName[]
}[]

`,
   "parameters": {},
   "stubs": [
      {
         "operationName": "findAllMovies",
         "response": "  [ { \"title\" : \"The ducks take Manhattan\", \"cast\" : { \"actors\" : [1,2,3] } } ]"
      },
      {
         "operationName": "findActor",
         "response": "{ \"name\" : \"Mickey Mouse\" }"
      }
   ]
}
}

Joining streams

Joining streams using |

      service TweetService {
         operation tweets():Stream<Tweet>
         operation analytics():Stream<TweetAnalytics>
         operation getUser(UserId):User
      }

stream { Tweet | TweetAnalytics }
  as {
     id : MessageId
     body : Message
     views : ViewCount?
}[]

Mutations

Output of a stream / query is transformed to the input of a mutation function

see: `can call mutation for each member of a stream``

// given a stream:
         model UserUpdateMessage {
            userId : UserId inherits String
            message : StatusMessage inherits String
         }

service UserUpdates {
   stream updates: Stream<UserUpdateMessage>
}

// Elsewhere, User is defined:
         model User {
            id : UserId
            name : UserName inherits String
         }

         service UserService {
            operation getUser(UserId):User
         }

// Finally, we want to persist updates, combining the message and user information:

         parameter model RichUserUpdateMessage {
            userId : UserId
            name : UserName
            message : StatusMessage
         }

         service UserService {
             write operation storeUpdate(RichUserUpdateMessage):RichUserUpdateMessage
         }

// The query:
         stream { UserUpdateMessage }
         call UserService::storeUpdate

// Will transform UserUpdateMessage into  RichUserUpdateMessage, enriching it by calling getUser()

Parameter and Closed

      closed parameter model Film {
         @Id
         filmId : FilmId
         title : Title
      }
      @HazelcastService(connectionName = "notUsed")
      service HazelcastService {
         @UpsertOperation
         write operation upsert(Film):Film

         @DeleteOperation(mapName = "films")
         write operation deleteAll()

         @DeleteOperation(mapName = "films")
         write operation deleteByKey(FilmId):Film

         table films : Film[]
      }

Converting things

Converting using as

converting using convert()

see can convert from one type to another using convert

      [[
      Converts the provided source into the target type reference.
       Conversions are performed locally, using only the data provided in source - ie.,
       no services or graph searches are performed.

       This method is less powerful than using a standard projection (eg., A as B), because:

        - Only the exact facts passed in the source are considered
        - No graph searches or remote invocations are performed

        As a result, it's also more performant.
       ]]
      declare function <T> convert(source: Any, targetType: lang.taxi.Type<T>): T"""
         model Person {
            name : FirstName inherits String
            age : PersonAge inherits String
         }
         model Dude {
            knownBy : FirstName
         }
         model Thing {
            person : Person
            dude : convert(this.person, Dude)
         }

Spread operator

see: generates the correct fields with excluded fields

... and ... except:

find { Person } as {
               name : FirstName
               address : Address as {
                 isOddNumbered : Boolean
                 house : HouseNumber
                 ... except {secretCode}
               }
               ... except {secretAge}
            }

Projection shorthand

find { Person } as {
   name, // reads name from Person
   address // etc
}

Restricting which services get called:

These examples from TaxiQlServiceRestrictionsSpec:

Only calling services with using:

         closed model Film {
            title : Title inherits String
         }
         service FilmService {
            operation getFilms():Film[]
            operation getBlockbusters():Film[]
         }

         find { Film[] }
         using { FilmService::getFilms }

Mixing services and operations:

            service NetflixService {
               operation getFilms():Film[]
               operation getBlockbusters():Film[]
            }

            find { Film[] }
            using { FilmService::getBlockbusters , FilmService::getFilms, NetflixService }

Excluding a service:

            find { Film[] }
            excluding { FilmService::films }
andrewgkew commented 3 weeks ago

Examples from Metro Bank implementation

1. Complex Enums

type ErrorCode inherits String
type ErrorMessage inherits String
type ErrorMessage inherits String

model ResponseErrors {
  message: ErrorMessage
}

model ErrorResponse {
  code: ErrorCode
  message: ErrorMessage
  errors: ResponseErrors[]
}

enum Errors<ErrorResponse> {
  CODE1({code: "400", message: "Bad Request", error: [{ message: "Error message" }] }),
  CODE2({code: "500", message: "Internal Server Error", error: [{ message: "Error message 2" }] })
}

2. Access enum fields for enrichment

Using the custom errors above

  Errors.enumFromName('CODE1').code
  Errors.enumFromName('CODE1').message
  Errors.enumFromName('CODE1').error

3. Type that is an expression

You can define a type that has inputs and runs an expression which can be used in conditional statement

type Date inherits Instant
type Status inherits String

model Permissions {
  account : Account inherits String
  permissions: String[]
}

type isValid by (Date, Permissions, Status) -> Date > now() && Permissions != null && lowerCase(Status) == 'authorized'

Then using this type in a when clause

  when {
   isValid -> xxx
   else -> yyy
}

4. Custom Errors (Exceptions)

If you are defining custom errors that are thrown from an API you can do so by inheriting from a base Error type and then can define the error body model

@taxi.http.ResponseBody
@taxi.http.ResponseCode(500)
model InternalServerError inherits com.orbitalhq.errors.Error {
  code: ErrorResponseCode inherits String
  id: CorrelationId inherits String
  message: ErrorMessage inherits String
}

Then you can throw the error and set the body

throw( (InternalServerError) {
  code: "500"
  id: "UUID"
  message: "An Internal Server error occured"
})

Then combing this with the enum above you can use the enumFromName method

throw( (InternalServerError) {
  code: Errors.enumFromName('CODE1').code
  id: "UUID"
  message: Errors.enumFromName('CODE1').message
})

5. Other enum methods

You can also use a new enum method to check if an emun name exists, which can be helpful in a when clause

when {
  Errors.hasEnumNamed('CODE3') -> xxx
  else -> yyy
}

6. Conditional statement in a type

import taxi.stdlib.lowerCase

type AccountType inherits String by when(lowerCase(CustomerType)) {
  'type1' -> 'Personal'
  'type2' -> 'Personal'
  else -> 'Business'
}

7. Default a type to static string

type SchemeName inherits String by "STATIC.STRING"

8. API Key using on a service

Create an auth.conf file in config directory as follows passing the key in via env vars to Orbital

authenticationToken {
  "com.example.ExampleService" {
    type: HttpHeader
    value: ${API_KEY}
    prefix: ""
    headerName: apikey
  }
}

9. Parameterize the url for an API being called

If you have a service which is an API to be called and want to parameterize the URL so that it works across env

@HttpService(baseUrl="http://exampleService")
service ExampleService {
  @HttpOperations(method = 'GET', url = '/path/{id}')
  operation getExample(@PathVariable(value = "id") id: ExampleId): ExampleResponse
}

With the above service defined we have create a parameter called exampleServiceUrl which we now can set via an env variable in a conf file in the config directory and pass EXAMPLE_URL as an env var

services {
  exampleService {
    url = ${EXAMPLE_URL}
  }
}

11. Extension functions

extension function filterByCustomerType(
  keyValuePair: Params[]
  type: CustomerType
):ParamValue[] -> keyValuePair.filter( (name:ParamName) -> name == type)

extension function filterByCategoryId(
  keyValuePair: Params[],
  validCat: String[],
  catId: Category
):ParamValue[] -> keyValuePair.filter( (name: ParamName, value: ParamValue) -> value == catId && validCat.contains(name.toRawType()))

12. Using functions in a complex type

Two things to show here firstly using the functions in the type but also passing the 1st element of an array using consent:Consent = first(Consent[])

type ParamValue inherits String
type ParamName inherits String

model SavedParams {
  id: ParamId
  params: Params[]
}

model Params {
  name: ParamName
  type: ParamValue
}

type AccountSubType inherits String by (paramSet:SavedParams(id == 'ID'), catId:CategoryId, consent:Consent = first(Consent[])) ->
   filterByCategoryId(
    paramSet.params,
    filterByCustomerType(paramSet.params, consent.CustomerType).convert(ParamValue[]).toRawType(),
    catId
    )
    .convert(ParamValue[])
    .exactlyOne()
    .toRawType()

13. Using a projection in a query to set context

This example may need some more info

given {
  accountId
}
find {
  account(id == accountId)
} as (
  consent: Consent = Response:Consent[].first(),
  account: Account
) -> {
  data: AccountResponse
}