Skip to content

Commit

Permalink
Merge branch 'minor' into major
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Apr 1, 2022
2 parents 4dc2a30 + e75ea9c commit b882b91
Show file tree
Hide file tree
Showing 40 changed files with 7,067 additions and 5,732 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
## <small>1.5.1 (2022-03-31)</small>


#### Fixes

* **admin-ui** Allow stockOnHand to match outOfStockThreshold ([f89bfbe](https://github.com/vendure-ecommerce/vendure/commit/f89bfbe)), closes [#1483](https://github.com/vendure-ecommerce/vendure/issues/1483)
* **admin-ui** Fix error with FacetValue localeString custom field ([80ef31a](https://github.com/vendure-ecommerce/vendure/commit/80ef31a)), closes [#1442](https://github.com/vendure-ecommerce/vendure/issues/1442)
* **core** Add missing OrderLine.order field resolver (#1478) ([c6cf4d4](https://github.com/vendure-ecommerce/vendure/commit/c6cf4d4)), closes [#1478](https://github.com/vendure-ecommerce/vendure/issues/1478)
* **core** Allow stockOnHand adjustments to match outOfStockThreshold ([77239b2](https://github.com/vendure-ecommerce/vendure/commit/77239b2)), closes [#1483](https://github.com/vendure-ecommerce/vendure/issues/1483)
* **core** Correctly save relation custom fields on CustomerGroup ([1634ed9](https://github.com/vendure-ecommerce/vendure/commit/1634ed9)), closes [#1493](https://github.com/vendure-ecommerce/vendure/issues/1493)
* **core** Fix error when pro-rating order with 0 price variant ([44cc46d](https://github.com/vendure-ecommerce/vendure/commit/44cc46d)), closes [#1492](https://github.com/vendure-ecommerce/vendure/issues/1492)
* **core** Fix importing products when 2 options have same name ([316f5e9](https://github.com/vendure-ecommerce/vendure/commit/316f5e9)), closes [#1445](https://github.com/vendure-ecommerce/vendure/issues/1445)
* **core** Promotion usage limits account for cancelled orders ([ce34f14](https://github.com/vendure-ecommerce/vendure/commit/ce34f14)), closes [#1466](https://github.com/vendure-ecommerce/vendure/issues/1466)
* **core** Truthy check for custom fields in importer ([a8c44d1](https://github.com/vendure-ecommerce/vendure/commit/a8c44d1))
* **core** Use subscribers passed in to the dbConnectionOptions ([ea63784](https://github.com/vendure-ecommerce/vendure/commit/ea63784))
* **payments-plugin** Fix state transitioning error case in Stripe webhook (#1485) ([280d2e3](https://github.com/vendure-ecommerce/vendure/commit/280d2e3)), closes [#1485](https://github.com/vendure-ecommerce/vendure/issues/1485)
* **payments-plugin** Send 200 response from Stripe webhook (#1487) ([4d55949](https://github.com/vendure-ecommerce/vendure/commit/4d55949)), closes [#1487](https://github.com/vendure-ecommerce/vendure/issues/1487)

## 1.5.0 (2022-03-15)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ By default, the "intensity" field will be displayed as a number input:
But let's say we want to display a range slider instead. Here's how we can do this using our shared extension module combined with the [registerFormInputComponent function]({{< relref "register-form-input-component" >}}):

```TypeScript
// project/ui-extensions/shared.module.ts
import { NgModule, Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { CustomFieldConfig } from '@vendure/common/lib/generated-types';
Expand Down Expand Up @@ -58,6 +59,25 @@ export class SliderControl implements FormInputComponent<CustomFieldConfig> {
})
export class SharedExtensionModule {}
```
The `SharedExtensionModule` is then passed to the `compileUiExtensions()` function as described in the [UI Extensions With Angular guide]({{< relref "using-angular" >}}#4-pass-the-extension-to-the-compileuiextensions-function):

```TypeScript
// project/vendure-config.ts
AdminUiPlugin.init({
port: 5001,
app: compileUiExtensions({
outputPath: path.join(__dirname, 'admin-ui'),
extensions: [{
extensionPath: path.join(__dirname, 'ui-extensions'),
ngModules: [{
type: 'shared',
ngModuleFileName: 'shared.module.ts',
ngModuleName: 'SharedExtensionModule',
}],
}],
}),
})
```

Once registered, this new slider input can be used in our custom field config:

Expand Down
105 changes: 105 additions & 0 deletions docs/content/plugins/plugin-examples/defining-db-subscribers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: "Defining database subscribers"
showtoc: true
---

# Defining database subscribers

TypeORM allows us to define [subscribers](https://typeorm.io/listeners-and-subscribers#what-is-a-subscriber). With a subscriber, we can listen to specific entity events and take actions based on inserts, updates, deletions and more.

If you need lower-level access to database changes that you get with the [Vendure EventBus system]({{< relref "event-bus" >}}), TypeORM subscribers can be useful.

## Simple subscribers

The simplest way to register a subscriber is to pass it to the `dbConnectionOptions.subscribers` array:

```TypeScript
import { Product, VendureConfig } from '@vendure/core';
import { EntitySubscriberInterface, EventSubscriber, UpdateEvent } from 'typeorm';

@EventSubscriber()
export class ProductSubscriber implements EntitySubscriberInterface<Product> {
listenTo() {
return Product;
}

beforeUpdate(event: UpdateEvent<Product>) {
console.log(`BEFORE PRODUCT UPDATED: `, event.entity);
}
}

// ...
export const config: VendureConfig = {
dbConnectionOptions: {
// ...
subscribers: [ProductSubscriber],
}
}
```
The limitation of this method is that the `ProductSubscriber` class cannot make use of dependency injection, since it is not known to the underlying NestJS application and is instead instantiated by TypeORM directly.

If you need to make use of providers in your subscriber class, you'll need to use the following pattern.

## Injectable subscribers

By defining the subscriber as an injectable provider, and passing it to a Vendure plugin, you can take advantage of Nest's dependency injection inside the subscriber methods.

```TypeScript
import {
PluginCommonModule,
Product,
TransactionalConnection,
VendureConfig,
VendurePlugin,
} from '@vendure/core';
import { Injectable } from '@nestjs/common';
import { EntitySubscriberInterface, EventSubscriber, UpdateEvent } from 'typeorm';
import { MyService } from './services/my.service';

@Injectable()
@EventSubscriber()
export class ProductSubscriber implements EntitySubscriberInterface<Product> {
constructor(private connection: TransactionalConnection,
private myService: MyService) {
// This is how we can dynamically register the subscriber
// with TypeORM
connection.rawConnection.subscribers.push(this);
}

listenTo() {
return Product;
}

async beforeUpdate(event: UpdateEvent<Product>) {
console.log(`BEFORE PRODUCT UPDATED: `, event.entity);
// Now we can make use of our injected provider
await this.myService.handleProductUpdate(event);
}
}

@VendurePlugin({
imports: [PluginCommonModule],
providers: [ProductSubscriber, MyService],
})
class MyPlugin {}

// ...
export const config: VendureConfig = {
dbConnectionOptions: {
// We no longer need to pass the subscriber here
// subscribers: [ProductSubscriber],
},
plugins: [
MyPlugin,
],
}
```

## Troubleshooting subscribers

An important factor when working with TypeORM subscribers is that they are very low-level and require some understanding of the Vendure schema.

For example consider the `ProductSubscriber` above. If an admin changes a product's name in the Admin UI, this subscriber **will not fire**. The reason is that the `name` property is actually stored on the `ProductTranslation` entity, rather than on the `Product` entity.

So if your subscribers do not seem to work as expected, check your database schema and make sure you are really targeting the correct entity which has the property that you are interested in.

119 changes: 119 additions & 0 deletions docs/content/storefront/configuring-a-graphql-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
title: "Configuring a GraphQL Client"
weight: 2
showtoc: true
---

# Configuring a GraphQL Client

This guide provides examples of how to set up popular GraphQL clients to work with the Vendure Shop API. These examples are designed to work with both the `bearer` and `cookie` methods of [managing sessions]({{< relref "managing-sessions" >}}).

## Apollo Client

Here's an example configuration for [Apollo Client](https://www.apollographql.com/docs/react/) with a React app.

```TypeScript
import {
ApolloClient,
ApolloLink,
HttpLink,
InMemoryCache,
} from '@apollo/client'
import { setContext } from '@apollo/client/link/context'

const AUTH_TOKEN_KEY = 'auth_token';

const httpLink = new HttpLink({
uri: `${process.env.NEXT_PUBLIC_URL_SHOP_API}/shop-api`,
withCredentials: true,
});

const afterwareLink = new ApolloLink((operation, forward) => {
return forward(operation).map((response) => {
const context = operation.getContext();
const authHeader = context.response.headers.get('vendure-auth-token');
if (authHeader) {
// If the auth token has been returned by the Vendure
// server, we store it in localStorage
localStorage.setItem(AUTH_TOKEN_KEY, authHeader);
}
return response;
});
});

const client = new ApolloClient({
link: ApolloLink.from([
setContext(() => {
const authToken = localStorage.getItem(AUTH_TOKEN_KEY)
if (authToken) {
// If we have stored the authToken from a previous
// response, we attach it to all subsequent requests.
return {
headers: {
authorization: `Bearer ${authToken}`,
},
}
}
}),
afterwareLink,
httpLink,
]),
cache: new InMemoryCache(),
})

export default client;
```

## Urql

Here's an example using the [urql](https://formidable.com/open-source/urql/) client:

```tsx
import * as React from "react"
import { createClient, dedupExchange, fetchExchange, Provider } from "urql"
import { cacheExchange} from "@urql/exchange-graphcache"
import { makeOperation} from "@urql/core"

const AUTH_TOKEN_KEY = "auth_token"

const client = createClient({
fetch: (input, init) => {
const token = localStorage.getItem(AUTH_TOKEN_KEY)
if (token) {
const headers = input instanceof Request ? input.headers : init.headers;
headers['Authorization'] = `Bearer ${token}`;
}
return fetch(input, init).then(response => {
const token = response.headers.get("vendure-auth-token")
if (token) {
localStorage.setItem(AUTH_TOKEN_KEY, token)
}
return response
})
},
url: process.env.NEXT_PUBLIC_URL_SHOP_API,
exchanges: [
dedupExchange,
cacheExchange({
updates: {
Mutation: {
addItemToOrder: (parent, args, cache) => {
const activeOrder = cache.resolve('Query', 'activeOrder');
if (activeOrder == null) {
// The first time that the `addItemToOrder` mutation is called in a session,
// the `activeOrder` query needs to be manually updated to point to the newly-created
// Order type. From then on, the graphcache will handle keeping it up-to-date.
cache.link('Query', 'activeOrder', parent.addItemToOrder);
}
},
},
},
}),
fetchExchange,
],
})

export const App = () => (
<Provider value={client}><YourRoutes /></Provider>
)
```
2 changes: 2 additions & 0 deletions docs/content/storefront/managing-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,5 @@ export async function request(query: string, variables: any) {
return response.data;
}
```

Real-world examples with specific clients can be found in the [Configuring a GraphQL Client guide]({{< relref "configuring-a-graphql-client" >}}).
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ export class FacetDetailComponent
const key = fieldDef.name;
const fieldValue =
fieldDef.type === 'localeString'
? (valueTranslation as any).customFields[key]
? (valueTranslation as any | undefined)?.customFields?.[key]
: (value as any).customFields[key];
const control = customValueFieldsGroup.get(key);
if (control) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
[class.inventory-untracked]="inventoryIsNotTracked(formGroup)"
clrInput
type="number"
min="0"
[min]="getStockOnHandMinValue(formGroup)"
step="1"
formControlName="stockOnHand"
[readonly]="!(updatePermission | hasPermission)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
return '';
}

getStockOnHandMinValue(variant: FormGroup) {
const effectiveOutOfStockThreshold = variant.get('useGlobalOutOfStockThreshold')?.value
? this.globalOutOfStockThreshold
: variant.get('outOfStockThreshold')?.value;
return effectiveOutOfStockThreshold;
}

getSaleableStockLevel(variant: ProductVariantFragment) {
const effectiveOutOfStockThreshold = variant.useGlobalOutOfStockThreshold
? this.globalOutOfStockThreshold
Expand Down
25 changes: 25 additions & 0 deletions packages/admin-ui/src/lib/core/src/common/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3583,6 +3583,31 @@ export type PaymentStateTransitionError = ErrorResult & {
* Permissions for administrators and customers. Used to control access to
* GraphQL resolvers via the {@link Allow} decorator.
*
* ## Understanding Permission.Owner
*
* `Permission.Owner` is a special permission which is used in some of the Vendure resolvers to indicate that that resolver should only
* be accessible to the "owner" of that resource.
*
* For example, the Shop API `activeCustomer` query resolver should only return the Customer object for the "owner" of that Customer, i.e.
* based on the activeUserId of the current session. As a result, the resolver code looks like this:
*
* @example
* ```TypeScript
* \@Query()
* \@Allow(Permission.Owner)
* async activeCustomer(\@Ctx() ctx: RequestContext): Promise<Customer | undefined> {
* const userId = ctx.activeUserId;
* if (userId) {
* return this.customerService.findOneByUserId(ctx, userId);
* }
* }
* ```
*
* Here we can see that the "ownership" must be enforced by custom logic inside the resolver. Since "ownership" cannot be defined generally
* nor statically encoded at build-time, any resolvers using `Permission.Owner` **must** include logic to enforce that only the owner
* of the resource has access. If not, then it is the equivalent of using `Permission.Public`.
*
*
* @docsCategory common
*/
export enum Permission {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3348,6 +3348,31 @@ export type PaymentStateTransitionError = ErrorResult & {
* Permissions for administrators and customers. Used to control access to
* GraphQL resolvers via the {@link Allow} decorator.
*
* ## Understanding Permission.Owner
*
* `Permission.Owner` is a special permission which is used in some of the Vendure resolvers to indicate that that resolver should only
* be accessible to the "owner" of that resource.
*
* For example, the Shop API `activeCustomer` query resolver should only return the Customer object for the "owner" of that Customer, i.e.
* based on the activeUserId of the current session. As a result, the resolver code looks like this:
*
* @example
* ```TypeScript
* \@Query()
* \@Allow(Permission.Owner)
* async activeCustomer(\@Ctx() ctx: RequestContext): Promise<Customer | undefined> {
* const userId = ctx.activeUserId;
* if (userId) {
* return this.customerService.findOneByUserId(ctx, userId);
* }
* }
* ```
*
* Here we can see that the "ownership" must be enforced by custom logic inside the resolver. Since "ownership" cannot be defined generally
* nor statically encoded at build-time, any resolvers using `Permission.Owner` **must** include logic to enforce that only the owner
* of the resource has access. If not, then it is the equivalent of using `Permission.Public`.
*
*
* @docsCategory common
*/
export enum Permission {
Expand Down
Loading

0 comments on commit b882b91

Please sign in to comment.