vendure-ecommerce / vendure

The commerce platform with customization in its DNA.
https://www.vendure.io
Other
5.58k stars 989 forks source link

Consider splitting into public/private APIs #65

Closed michaelbromley closed 5 years ago

michaelbromley commented 5 years ago

Currently we have a single GraphQL API which is used by both public (storefront) and private (admin ui) clients.

Certain data is relevant to both clients, but its exact shape and properties may differ depending on whether the context is public or private.

Example: unpublished Products

Let's say we add a published flag to the Product entity. If a product is published, it should appear in the storefront but if not it should only appear in the admin ui.

When a client makes a products query, how do we know whether to include unpublished products in the results?

  1. We could implicitly filter out all unpublished results if the client does not have the ReadCatalog permission.
  2. We could add a flag, includeUnpublished which the admin ui app sets to true. If an unauthorized client tries to set this to true, an error could be thrown.
  3. We could create a new query just for admins.
  4. We could split the API into entirely separate endpoints, one for private clients and one for public clients.

In this issue I will explore the fourth option above.

Analysis

Here is a list of all the current queries & mutations, and whether they are used in a private or public context, or both.

Queries public/private analysis Query | Private | Public ------|---------|--------- administrators | ☑ | administrator | ☑ | assets | ☑ | asset | ☑ | me | ☑ | channels | ☑ | channel | ☑ | activeChannel | ☑ | config | ☑ | countries | ☑ | country | ☑ | availableCountries | | ☑ customerGroups | ☑ | customerGroup | ☑ | customers | ☑ | customer | ☑ | activeCustomer | | ☑ facets | ☑ | facet | ☑ | globalSettings | ☑ | order | ☑ | ☑ activeOrder | | ☑ orderByCode | | ☑ nextOrderStates | ☑ | ☑ orders | ☑ | eligibleShippingMethods | | ☑ paymentMethods | ☑ | paymentMethod | ☑ | productCategories | ☑ | ☑ productCategory | ☑ | ☑ productOptionGroups | ☑ | productOptionGroup | ☑ | products | ☑ | ☑ product | ☑ | ☑ promotion | ☑ | promotions | ☑ | adjustmentOperations | ☑ | roles | ☑ | role | ☑ | search | ☑ | ☑ shippingMethods | ☑ | shippingMethod | ☑ | shippingEligibilityCheckers | ☑ | shippingCalculators | ☑ | taxCategories | ☑ | taxCategory | ☑ | taxRates | ☑ | taxRate | ☑ | zones | ☑ | zone | ☑ |
Mutations public/private analysis Mutation | Private | Public ------|---------|--------- createAdministrator | ☑ | updateAdministrator | ☑ | assignRoleToAdministrator | ☑ | createAssets | ☑ | login | ☑ | ☑ logout | ☑ | ☑ registerCustomerAccount | | ☑ verifyCustomerAccount | | ☑ refreshCustomerVerification | | ☑ createChannel | ☑ | updateChannel | ☑ | createCountry | ☑ | updateCountry | ☑ | deleteCountry | ☑ | createCustomerGroup | ☑ | updateCustomerGroup | ☑ | addCustomersToGroup | ☑ | removeCustomersFromGroup | ☑ | createCustomer | ☑ | updateCustomer | ☑ | deleteCustomer | ☑ | createCustomerAddress | ☑ | ☑ updateCustomerAddress | ☑ | ☑ createFacet | ☑ | updateFacet | ☑ | deleteFacet | ☑ | createFacetValues | ☑ | updateFacetValues | ☑ | deleteFacetValues | ☑ | updateGlobalSettings | ☑ | importProducts | ☑ | addItemToOrder | | ☑ removeItemFromOrder | | ☑ adjustItemQuantity | | ☑ transitionOrderToState | ☑ | ☑ setOrderShippingAddress | | ☑ setOrderShippingMethod | | ☑ addPaymentToOrder | | ☑ setCustomerForOrder | | ☑ updatePaymentMethod | ☑ | createProductCategory | ☑ | updateProductCategory | ☑ | moveProductCategory | ☑ | createProductOptionGroup | ☑ | updateProductOptionGroup | ☑ | createProduct | ☑ | updateProduct | ☑ | deleteProduct | ☑ | addOptionGroupToProduct | ☑ | removeOptionGroupFromProduct | ☑ | generateVariantsForProduct | ☑ | updateProductVariants | ☑ | createPromotion | ☑ | updatePromotion | ☑ | deletePromotion | ☑ | createRole | ☑ | updateRole | ☑ | reindex | ☑ | createShippingMethod | ☑ | updateShippingMethod | ☑ | createTaxCategory | ☑ | updateTaxCategory | ☑ | createTaxRate | ☑ | updateTaxRate | ☑ | createZone | ☑ | updateZone | ☑ | deleteZone | ☑ | addMembersToZone | ☑ | removeMembersFromZone | ☑ |

Currently there are only 7 queries and 5 mutations which are shared between the public and private contexts.

Property resolvers

This is complicated somewhat by the fact that individual property resolvers also operate across the boundary between public and private.

For example, the productOptionGroup query is restricted to those with the ReadCatalog permission. The ProductOptionGroup type has a property, options which has a resolver also restricted to ReadCatalog.

However, it is possible to create a public query which attempts to list the options of a groups and fails due to lack of permissions for that resolver:

query {
  product {
    id
     optionGroups {
       id
       options {  # <- fails here
          id
          code
        }
     }
  }
}

Feedback

I created a thread in the Spectrum GraphQL community asking for feedback on the idea. So far there are 2 responses who both say they split their API like this.

Security

One major benefit of splitting the API is potentially better security. E.g. the private API could be kept inaccessible by setting up a reverse proxy server which only exposes the public API to the internet. A less strict mode could be where the private is available to the internet, but can only be read with a valid authentication token.

michaelbromley commented 5 years ago

This is now done, and makes things much simpler for developing a storefront app.