Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL Array<T> vs T[] inconsistencies #1017

Open
4 tasks done
MichaelHirn opened this issue Jan 29, 2022 · 1 comment
Open
4 tasks done

GraphQL Array<T> vs T[] inconsistencies #1017

MichaelHirn opened this issue Jan 29, 2022 · 1 comment
Labels
bug Something isn't working dev-experience Developer Experience difficulty: medium package:core Affects the core package

Comments

@MichaelHirn
Copy link
Contributor

MichaelHirn commented Jan 29, 2022

Bug Report

Current Behavior

A read model that uses Array<T> can be queried via GraphQL, while a read model that uses (the semantically identical notation) T[] will throw the following GraphQL error JSONObject cannot represent non-object value

✅ this works

export class OrdersByOwner {
  public constructor(
    public id: UUID,
    readonly orderIds: Array<UUID>
  ) {}

❌ this fails with above error when queried via GraphQL

export class OrdersByOwner {
  public constructor(
    public id: UUID,
    readonly orderIds: UUID[]
  ) {}

Expected behavior

Both Array<T> and T[] to work and not throw any errors (I assume that in typescript both ways are semantically identical)

Additional information

Potentially related/similar issues: #338, #436

Environment

  • Booster version: @boostercloud/cli/0.24.2 darwin-x64 node-v14.18.1 ([email protected])
  • OS: OSX 12.1
Checklist
  • packages/framework-core/src/library/graphql-adapter.ts ✅ Commit 8394c32
  • packages/framework-provider-aws/src/library/graphql-adapter.ts ⚠️ No Changes Made
  • packages/framework-core/test/library/graphql-adapter.test.ts ✅ Commit f77b15e
  • packages/framework-provider-aws/test/library/graphql-adapter.test.ts ✅ Commit 4fde026
@javiertoledo javiertoledo added bug Something isn't working dev-experience Developer Experience difficulty: medium package:core Affects the core package labels Dec 19, 2022
@sweep-ai
Copy link
Contributor

sweep-ai bot commented Oct 16, 2023

Here's the PR! #1471.

⚡ Sweep Basic Tier: I'm creating this ticket using GPT-4. You have 4 GPT-4 tickets left for the month and 2 for the day. For more GPT-4 tickets, visit our payment portal.

Actions (click)

  • ↻ Restart Sweep
Install Sweep Configs: Pull Request

Step 1: 🔎 Searching

I found the following snippets in your repository. I will now analyze these snippets and come up with a plan.

Some code snippets I looked at (click to expand). If some file is missing from here, you can mention the path in the ticket description.

{
"id": "1",
"title": "My first post",
"content": "This is the content of my first post"
},
{
"id": "2",
"title": "My second post",
"content": "This is the content of my second post"
}
]
}
}
}
```
Notice that getters are not cached in the read models database, so the getters will be executed every time you include these fields in the queries. If access to nested queries is frequent or the size of the responses are big, you could improe your API response performance by querying the read models separately and joining the results in the client application.
## Authorizing a read model
Read models are part of the public API of a Booster application, so you can define who is authorized to submit them. All read models are protected by default, which means that no one can query them. In order to allow users to query a read model, you must explicitly authorize them. You can use the `authorize` field of the `@ReadModel` decorator to specify the authorization rule.
```typescript title="src/read-model/product-read-model.ts"
@ReadModel({
authorize: 'all',
})
export class ProductReadModel {
public constructor(public id: UUID, readonly name: string, readonly description: string, readonly price: number) {}
@Projects(Product, 'id')
public static projectProduct(entity: Product, current?: ProductReadModel): ProjectionResult<ProductReadModel> {
return new ProductReadModel(entity.id, entity.name, entity.description, entity.price)
}
}
```
You can read more about this on the [Authorization section](/security/authorization).
## Querying a read model
Booster read models are accessible to the outside world through GraphQL queries. GrahpQL fits very well with Booster's CQRS approach because it has two kinds of reading operations: Queries and Subscriptions. They are read-only operations that do not modify the state of the application. Booster uses them to fetch data from the read models.
Booster automatically creates the queries and subscriptions for each read model. You can use them to fetch the data from the read models. For example, given the following read model:
```typescript title="src/read-model/cart-read-model.ts"
@ReadModel({
authorize: 'all',
})
export class CartReadModel {
public constructor(public id: UUID, readonly items: Array<CartItem>) {}
@Projects(Cart, 'id')
public static projectCart(entity: Cart, currentReadModel: CartReadModel): ProjectionResult<CartReadModel> {
return new CartReadModel(entity.id, entity.items)
}
}
```
You will get the following GraphQL query and subscriptions:
```graphql
query CartReadModel(id: ID!): CartReadModel
subscription CartReadModel(id: ID!): CartReadModel
subscription CartReadModels(id: UUIDPropertyFilter!): CartReadModel
```
For more information about queries and how to use them, please check the [GraphQL API](/graphql) section.
### Filtering a read model
Booster GraphQL API provides support for filtering Read Models on `queries` and `subscriptions`. To get more information about it go to the [GraphQL API](/graphql#filtering-a-read-model) section.
## Subscribing to a read model
Booster GraphQL API also provides support for real-time updates using subscriptions and a web-socket. To get more information about it go to the [GraphQL API](/graphql#subscribing-to-read-models) section.
## Sorting Read Models
There are some cases when it's desirable to query your read models sorted a particular field. An example could be a chat app where you want to fetch the messages of a channel sorted by the time they were sent. Booster provides a special decorator to tag a specific property as a sequence key for a read model:
```typescript title="src/read-model/message-read-model.ts"
export class MessageReadModel {
public constructor(
readonly id: UUID, // A channel ID
@sequencedBy readonly timestamp: string,
readonly contents: string
)
@Projects(Message, 'id')
public static projectMessage(
entity: Message,
currentReadModel: MessageReadModel
): ProjectionResult<MessageReadModel> {
return new MessageReadModel(entity.id, entity.timestamp, entity.contents)
}
}
```
### Querying time sequences
Adding a sequence key to a read model changes the behavior of the singular query, which now accepts the sequence key as an optional parameter:
```graphql
query MessageReadModel(id: ID!, timestamp: string): [MessageReadModel]
```
Using this query, when only the id is provided, you get an array of all the messages in the channel sorted by timestamp in ascending order (from older to newer). When you also provide an specific timestamp, you still get an array, but it will only contain the message sent in that exact moment.
It is important to guarantee that the sequence key is unique for each message. This could be difficult to achieve if you are using a timestamp as the sequence key. Booster provides a utility function to generate unique timestamps that you can use in your read models: `TimeKey.generate()`. It will generate a timestamp with a random UUID as a suffix to avoid any coincidences.
For more information about queries and how to use them, please check the [GraphQL API](/graphql#reading-read-models) section.

import { ReadModel } from '@boostercloud/framework-core'
import { UUID } from '@boostercloud/framework-types'
@ReadModel({
authorize: // Specify authorized roles here. Use 'all' to authorize anyone
})
export class CartWithFieldsReadModel {
public constructor(
public id: UUID,
readonly items: Array<Item>,
) {}

import { ReadModel, Projects } from '@boostercloud/framework-core'
import { UUID, ProjectionResult } from '@boostercloud/framework-types'
import { Product } from '../entities/product'
interface ProductIDWithPrice {
productId: UUID
priceCents: number
}
@ReadModel({
authorize: 'all',
})
export class ProductsBySKU {
public constructor(
public id: UUID,
readonly products: Array<ProductIDWithPrice> = [],
readonly firstProduct?: ProductIDWithPrice,
readonly record?: Record<string, number>
) {}
@Projects(Product, 'sku')
public static projectProduct(entity: Product, currentProductsBySKU?: ProductsBySKU): ProjectionResult<ProductsBySKU> {
// The purpose of this projection is to test the Optimistic concurrency. We have a read model that accumulates entities
// with different IDs. One instance of this read model (with id == product sku) will be updated when multiple instances
// of the product entity (with the same SKU but different id) are changed.
// This scenario will force optimistic concurrency issues to appear when combined with high contention load tests
if (!currentProductsBySKU) {
currentProductsBySKU = new ProductsBySKU(entity.sku, [])
}
const newAndSortedProducts = currentProductsBySKU.products
.filter((pp) => pp.productId != entity.id) // Remove the product if it was present
.concat({ productId: entity.id, priceCents: entity.price.cents }) // Add the new product or the existing, removed, product with the latest information
.sort((a, b) => b.priceCents - a.priceCents)
return new ProductsBySKU(currentProductsBySKU.id, newAndSortedProducts, newAndSortedProducts[0], {
items: newAndSortedProducts.length,
})
}

import { Money } from '../../../common/money'
import { Picture } from '../../../common/picture'
import { UUID } from '@boostercloud/framework-types'
import { Product } from '../../../entities/product'
export class ProductV1 {
public constructor(
public id: UUID,
readonly sku: string,
readonly name: string,
readonly description: string,
readonly price: Money,
readonly pictures: Array<Picture>,
public deleted: boolean = false
) {}
}

import { ReadModel, Projects } from '@boostercloud/framework-core'
import { UUID, ProjectionResult } from '@boostercloud/framework-types'
import { Cart } from '../entities/cart'
@ReadModel({
authorize: // Specify authorized roles here. Use 'all' to authorize anyone
})
export class CartWithProjectionReadModel {
public constructor(
public id: UUID,
readonly items: Array<Item>,
) {}
@Projects(Cart, "id")
public static projectCart(entity: Cart, currentCartWithProjectionReadModel?: CartWithProjectionReadModel): ProjectionResult<CartWithProjectionReadModel> {
return /* NEW CartWithProjectionReadModel HERE */
}


Step 2: ⌨️ Coding

  • packages/framework-core/src/library/graphql-adapter.ts ✅ Commit 8394c32
Create packages/framework-core/src/library/graphql-adapter.ts with contents:
• Locate the function that is responsible for transforming the read model fields to GraphQL fields.
• Add a condition to check if the field type is an array using `T[]` notation.
• If the field type is an array using `T[]` notation, transform it to `Array` notation before processing it further.

  • packages/framework-provider-aws/src/library/graphql-adapter.ts ⚠️ No Changes Made
Modify packages/framework-provider-aws/src/library/graphql-adapter.ts with contents:
• Repeat the same steps as in `packages/framework-core/src/library/graphql-adapter.ts`.

  • packages/framework-core/test/library/graphql-adapter.test.ts ✅ Commit f77b15e
Create packages/framework-core/test/library/graphql-adapter.test.ts with contents:
• Create a new test file for `graphql-adapter.ts` if it does not exist.
• Add a test case to verify that the GraphQL adapter can handle read models with `T[]` notation correctly.
• The test case should define a read model with a field using `T[]` notation, pass it to the GraphQL adapter, and assert that no errors are thrown and the output is as expected.

  • packages/framework-provider-aws/test/library/graphql-adapter.test.ts ✅ Commit 4fde026
Modify packages/framework-provider-aws/test/library/graphql-adapter.test.ts with contents:
• Repeat the same steps as in `packages/framework-core/test/library/graphql-adapter.test.ts`.


Step 3: 🔁 Code Review

I have finished reviewing the code for completeness. I did not find errors for sweep/fix-graphql-array-t-inconsistencies.

.


🎉 Latest improvements to Sweep:

  • Sweep can now passively improve your repository! Check out Rules to learn more.

💡 To recreate the pull request edit the issue title or description. To tweak the pull request, leave a comment on the pull request.
Join Our Discord

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working dev-experience Developer Experience difficulty: medium package:core Affects the core package
Projects
Status: No status
Development

Successfully merging a pull request may close this issue.

2 participants