Browse Source

Merge branch 'master' into minor

Michael Bromley 4 years ago
parent
commit
d5f7f4e13e
42 changed files with 2673 additions and 2203 deletions
  1. 11 0
      CHANGELOG.md
  2. 217 0
      docs/content/developer-guide/uploading-files.md
  3. 1 1
      lerna.json
  4. 3 3
      packages/admin-ui-plugin/package.json
  5. 2 2
      packages/admin-ui/package.json
  6. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  7. 22 0
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-base.pipe.ts
  8. 1 1
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency-name.pipe.ts
  9. 1 1
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency.pipe.ts
  10. 1 1
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-date.pipe.ts
  11. 1 1
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-language-name.pipe.ts
  12. 1 1
      packages/admin-ui/src/lib/core/src/shared/pipes/locale-region-name.pipe.ts
  13. 22 22
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  14. 3 3
      packages/asset-server-plugin/package.json
  15. 1 1
      packages/common/package.json
  16. 747 732
      packages/common/src/generated-shop-types.ts
  17. 14 0
      packages/core/e2e/entity-hydrator.e2e-spec.ts
  18. 24 0
      packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts
  19. 710 695
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  20. 2 2
      packages/core/package.json
  21. 3 0
      packages/core/src/api/resolvers/shop/shop-auth.resolver.ts
  22. 1 0
      packages/core/src/api/schema/shop-api/shop.api.graphql
  23. 2 2
      packages/core/src/config/promotion/promotion-action.ts
  24. 4 1
      packages/core/src/entity/asset/orderable-asset.entity.ts
  25. 1 1
      packages/core/src/i18n/messages/pt_PT.json
  26. 4 1
      packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts
  27. 40 2
      packages/core/src/service/services/asset.service.ts
  28. 3 3
      packages/create/package.json
  29. 9 9
      packages/dev-server/package.json
  30. 7 0
      packages/dev-server/test-plugins/customer-avatar/api-extensions.ts
  31. 22 0
      packages/dev-server/test-plugins/customer-avatar/customer-avatar-plugin.ts
  32. 55 0
      packages/dev-server/test-plugins/customer-avatar/customer-avatar.resolver.ts
  33. 3 3
      packages/elasticsearch-plugin/package.json
  34. 3 3
      packages/email-plugin/package.json
  35. 7 2
      packages/email-plugin/src/dev-mailbox.ts
  36. 3 3
      packages/job-queue-plugin/package.json
  37. 710 695
      packages/payments-plugin/e2e/graphql/generated-shop-types.ts
  38. 4 4
      packages/payments-plugin/package.json
  39. 3 3
      packages/testing/package.json
  40. 4 4
      packages/ui-devkit/package.json
  41. 0 0
      schema-admin.json
  42. 0 0
      schema-shop.json

+ 11 - 0
CHANGELOG.md

@@ -1,3 +1,14 @@
+## <small>1.4.4 (2022-01-10)</small>
+
+
+#### Fixes
+
+* **admin-ui** Improve handling of locale combinations ([87f9f78](https://github.com/vendure-ecommerce/vendure/commit/87f9f78))
+* **core** Correctly hydrate nested relations of empty array ([4a11666](https://github.com/vendure-ecommerce/vendure/commit/4a11666)), closes [#1324](https://github.com/vendure-ecommerce/vendure/issues/1324)
+* **core** Fix PromotionActions not passing state correctly (#1323) ([fc739c5](https://github.com/vendure-ecommerce/vendure/commit/fc739c5)), closes [#1323](https://github.com/vendure-ecommerce/vendure/issues/1323) [#1322](https://github.com/vendure-ecommerce/vendure/issues/1322)
+* **core** Return NotVerifiedError for resetPassword on unverified user ([8257d27](https://github.com/vendure-ecommerce/vendure/commit/8257d27)), closes [#1321](https://github.com/vendure-ecommerce/vendure/issues/1321)
+* **email-plugin** Fix sorting of emails in dev-mailbox ([57cc26e](https://github.com/vendure-ecommerce/vendure/commit/57cc26e))
+
 ## <small>1.4.3 (2021-12-22)</small>
 ## <small>1.4.3 (2021-12-22)</small>
 
 
 
 

+ 217 - 0
docs/content/developer-guide/uploading-files.md

@@ -0,0 +1,217 @@
+---
+title: "Uploading Files"
+showtoc: true
+---
+
+# Uploading Files 
+
+Vendure handles file uploads with the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). Internally, we use the [graphql-upload package](https://github.com/jaydenseric/graphql-upload). Once uploaded, a file is known as an [Asset]({{< relref "/docs/typescript-api/entities/asset" >}}). Assets are typically used for images, but can represent any kind of binary data such as PDF files or videos.
+
+## Upload clients
+
+Here is a [list of client implementations](https://github.com/jaydenseric/graphql-multipart-request-spec#client) which will allow you to upload files using the spec. If you are using Apollo Client, then you should install the [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client) npm package.
+
+For testing, it is even possible to use a [plain curl request](https://github.com/jaydenseric/graphql-multipart-request-spec#single-file).
+
+## The `createAssets` mutation
+
+The [createAssets mutation]({{< relref "/docs/graphql-api/admin/mutations" >}}#createassets) in the Admin API is the only means of uploading files by default. 
+
+Here's an example of how a file upload would look using the `apollo-upload-client` package:
+
+```TypeScript
+import { gql, useMutation } from "@apollo/client";
+
+const MUTATION = gql`
+  mutation CreateAssets($input: [CreateAssetInput!]!) {
+    createAssets(input: $input) {
+      ... on Asset {
+        id
+        name
+        fileSize
+      }
+      ... on ErrorResult {
+        message
+      }
+    }
+  }
+`;
+
+function UploadFile() {
+  const [mutate] = useMutation(MUTATION);
+
+  function onChange(event) {
+    const { target } = event;  
+    if (target.validity.valid) {
+      mutate({ 
+        variables: {
+          input: target.files.map(file => ({ file }))
+        }  
+      });
+    }
+  }
+
+  return <input type="file" required onChange={onChange} />;
+}
+```
+
+## Custom upload mutations
+
+How about if you want to implement a custom mutation for file uploads? Let's take an example where we want to allow customers to set an avatar image. To do this, we'll add a [custom field]({{< relref "customizing-models" >}}) to the Customer entity and then define a new mutation in the Shop API.
+
+### Configuration
+
+Let's define a custom field to associate the avatar Asset with the Customer:
+
+```TypeScript
+import { Asset } from '@vendure/core';
+
+const config = {
+  // ...
+  customFields: {
+    Customer: [
+      { 
+        name: 'avatar',
+        type: 'relation',
+        entity: Asset,
+        nullable: true,
+      },
+    ],
+  },
+}
+```
+
+In a later step, we will refactor this config to encapsulate it in a plugin.
+
+### Schema definition
+
+Next we will define the schema for the mutation:
+
+```TypeScript
+// api-extensions.ts
+import gql from 'graphql-tag';
+
+export const shopApiExtensions = gql`
+extend type Mutation {
+  setCustomerAvatar(file: Upload!): Asset
+}`
+```
+
+### Resolver
+
+The resolver will make use of the built-in [AssetService]({{< relref "asset-service" >}}) to handle the processing of the uploaded file into an Asset.
+
+```TypeScript
+// customer-avatar.resolver.ts
+import { Args, Mutation, Resolver } from '@nestjs/graphql';
+import { Asset } from '@vendure/common/lib/generated-types';
+import { Allow, AssetService, Ctx, CustomerService, isGraphQlErrorResult,
+  Permission, RequestContext, Transaction } from '@vendure/core';
+
+@Resolver()
+export class CustomerAvatarResolver {
+  constructor(private assetService: AssetService, private customerService: CustomerService) {}
+
+  @Transaction()
+  @Mutation()
+  @Allow(Permission.Authenticated)
+  async setCustomerAvatar(
+    @Ctx() ctx: RequestContext,
+    @Args() args: { file: any },
+  ): Promise<Asset | undefined> {
+    const userId = ctx.activeUserId;
+    if (!userId) {
+      return;
+    }
+    const customer = await this.customerService.findOneByUserId(ctx, userId);
+    if (!customer) {
+      return;
+    }
+    // Create an Asset from the uploaded file
+    const asset = await this.assetService.create(ctx, {
+      file: args.file,
+      tags: ['avatar'],
+    });
+    // Check to make sure there was no error when
+    // creating the Asset
+    if (isGraphQlErrorResult(asset)) {
+      // MimeTypeError
+      throw asset;
+    }
+    // Asset created correctly, so assign it as the
+    // avatar of the current Customer
+    await this.customerService.update(ctx, {
+      id: customer.id,
+      customFields: {
+        avatarId: asset.id,
+      },
+    });
+
+    return asset;
+  }
+}
+```
+
+### Complete Customer Avatar Plugin
+
+We can group all of this together into a plugin:
+
+```TypeScript
+import { Asset, PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+import { shopApiExtensions } from './api-extensions';
+import { CustomerAvatarResolver } from './customer-avatar.resolver';
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  shopApiExtensions: {
+    schema: shopApiExtensions,
+    resolvers: [CustomerAvatarResolver],
+  },
+  configuration: config => {
+    config.customFields.Customer.push({
+      name: 'avatar',
+      type: 'relation',
+      entity: Asset,
+      nullable: true,
+    });
+    return config;
+  },
+})
+export class CustomerAvatarPlugin {}
+```
+
+### Uploading a Customer Avatar
+
+In our storefront, we would then upload a Customer's avatar like this:
+
+```TypeScript
+import { gql, useMutation } from "@apollo/client";
+
+const MUTATION = gql`
+  mutation SetCustomerAvatar($file: Upload!) {
+    setCustomerAvatar(file: $file) {
+      id
+      name
+      fileSize
+    }
+  }
+`;
+
+function UploadAvatar() {
+  const [mutate] = useMutation(MUTATION);
+
+  function onChange(event) {
+    const { target } = event;  
+    if (target.validity.valid && target.files.length === 1) {
+      mutate({ 
+        variables: {
+          file: target.files[0],
+        }  
+      });
+    }
+  }
+
+  return <input type="file" required onChange={onChange} />;
+}
+```

+ 1 - 1
lerna.json

@@ -2,7 +2,7 @@
   "packages": [
   "packages": [
     "packages/*"
     "packages/*"
   ],
   ],
-  "version": "1.4.3",
+  "version": "1.4.4",
   "npmClient": "yarn",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "useWorkspaces": true,
   "command": {
   "command": {

+ 3 - 3
packages/admin-ui-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/admin-ui-plugin",
   "name": "@vendure/admin-ui-plugin",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "types": "lib/index.d.ts",
   "files": [
   "files": [
@@ -21,8 +21,8 @@
   "devDependencies": {
   "devDependencies": {
     "@types/express": "^4.17.8",
     "@types/express": "^4.17.8",
     "@types/fs-extra": "^9.0.1",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^1.4.3",
-    "@vendure/core": "^1.4.3",
+    "@vendure/common": "^1.4.4",
+    "@vendure/core": "^1.4.4",
     "express": "^4.17.1",
     "express": "^4.17.1",
     "rimraf": "^3.0.2",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"
     "typescript": "4.3.5"

+ 2 - 2
packages/admin-ui/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/admin-ui",
   "name": "@vendure/admin-ui",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "ng": "ng",
     "ng": "ng",
@@ -39,7 +39,7 @@
     "@ng-select/ng-select": "^7.2.0",
     "@ng-select/ng-select": "^7.2.0",
     "@ngx-translate/core": "^13.0.0",
     "@ngx-translate/core": "^13.0.0",
     "@ngx-translate/http-loader": "^6.0.0",
     "@ngx-translate/http-loader": "^6.0.0",
-    "@vendure/common": "^1.4.3",
+    "@vendure/common": "^1.4.4",
     "@webcomponents/custom-elements": "^1.4.3",
     "@webcomponents/custom-elements": "^1.4.3",
     "apollo-angular": "^2.6.0",
     "apollo-angular": "^2.6.0",
     "apollo-upload-client": "^16.0.0",
     "apollo-upload-client": "^16.0.0",

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/version.ts

@@ -1,2 +1,2 @@
 // Auto-generated by the set-version.js script.
 // Auto-generated by the set-version.js script.
-export const ADMIN_UI_VERSION = '1.4.3';
+export const ADMIN_UI_VERSION = '1.4.4';

+ 22 - 0
packages/admin-ui/src/lib/core/src/shared/pipes/locale-base.pipe.ts

@@ -33,5 +33,27 @@ export abstract class LocaleBasePipe implements OnDestroy, PipeTransform {
         }
         }
     }
     }
 
 
+    /**
+     * Returns the active locale after attempting to ensure that the locale string
+     * is valid for the Intl API.
+     */
+    protected getActiveLocale(localeOverride?: unknown): string {
+        const locale = typeof localeOverride === 'string' ? localeOverride : this.locale ?? 'en';
+        const hyphenated = locale?.replace(/_/g, '-');
+
+        // Check for a double-region string, containing 2 region codes like
+        // pt-BR-BR, which is invalid. In this case, the second region is used
+        // and the first region discarded. This would only ever be an issue for
+        // those languages where the translation file itself encodes the region,
+        // as in pt_BR & pt_PT.
+        const matches = hyphenated?.match(/^([a-zA-Z_-]+)(-[A-Z][A-Z])(-[A-Z][A-z])$/);
+        if (matches?.length) {
+            const overriddenLocale = matches[1] + matches[3];
+            return overriddenLocale;
+        } else {
+            return hyphenated;
+        }
+    }
+
     abstract transform(value: any, ...args): any;
     abstract transform(value: any, ...args): any;
 }
 }

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency-name.pipe.ts

@@ -32,7 +32,7 @@ export class LocaleCurrencyNamePipe extends LocaleBasePipe implements PipeTransf
         }
         }
         let name = '';
         let name = '';
         let symbol = '';
         let symbol = '';
-        const activeLocale = typeof locale === 'string' ? locale : this.locale ?? 'en';
+        const activeLocale = this.getActiveLocale(locale);
 
 
         // Awaiting TS types for this API: https://github.com/microsoft/TypeScript/pull/44022/files
         // Awaiting TS types for this API: https://github.com/microsoft/TypeScript/pull/44022/files
         const DisplayNames = (Intl as any).DisplayNames;
         const DisplayNames = (Intl as any).DisplayNames;

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/pipes/locale-currency.pipe.ts

@@ -28,7 +28,7 @@ export class LocaleCurrencyPipe extends LocaleBasePipe implements PipeTransform
     transform(value: unknown, ...args: unknown[]): string | unknown {
     transform(value: unknown, ...args: unknown[]): string | unknown {
         const [currencyCode, locale] = args;
         const [currencyCode, locale] = args;
         if (typeof value === 'number' && typeof currencyCode === 'string') {
         if (typeof value === 'number' && typeof currencyCode === 'string') {
-            const activeLocale = typeof locale === 'string' ? locale : this.locale;
+            const activeLocale = this.getActiveLocale(locale);
             const majorUnits = value / 100;
             const majorUnits = value / 100;
             return new Intl.NumberFormat(activeLocale, { style: 'currency', currency: currencyCode }).format(
             return new Intl.NumberFormat(activeLocale, { style: 'currency', currency: currencyCode }).format(
                 majorUnits,
                 majorUnits,

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/pipes/locale-date.pipe.ts

@@ -27,7 +27,7 @@ export class LocaleDatePipe extends LocaleBasePipe implements PipeTransform {
     transform(value: unknown, ...args: unknown[]): unknown {
     transform(value: unknown, ...args: unknown[]): unknown {
         const [format, locale] = args;
         const [format, locale] = args;
         if (this.locale || typeof locale === 'string') {
         if (this.locale || typeof locale === 'string') {
-            const activeLocale = typeof locale === 'string' ? locale : this.locale;
+            const activeLocale = this.getActiveLocale(locale);
             const date =
             const date =
                 value instanceof Date ? value : typeof value === 'string' ? new Date(value) : undefined;
                 value instanceof Date ? value : typeof value === 'string' ? new Date(value) : undefined;
             if (date) {
             if (date) {

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/pipes/locale-language-name.pipe.ts

@@ -30,7 +30,7 @@ export class LocaleLanguageNamePipe extends LocaleBasePipe implements PipeTransf
         if (typeof value !== 'string') {
         if (typeof value !== 'string') {
             return `Invalid language code "${value as any}"`;
             return `Invalid language code "${value as any}"`;
         }
         }
-        const activeLocale = typeof locale === 'string' ? locale : this.locale ?? 'en';
+        const activeLocale = this.getActiveLocale(locale);
 
 
         // Awaiting TS types for this API: https://github.com/microsoft/TypeScript/pull/44022/files
         // Awaiting TS types for this API: https://github.com/microsoft/TypeScript/pull/44022/files
         const DisplayNames = (Intl as any).DisplayNames;
         const DisplayNames = (Intl as any).DisplayNames;

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/pipes/locale-region-name.pipe.ts

@@ -30,7 +30,7 @@ export class LocaleRegionNamePipe extends LocaleBasePipe implements PipeTransfor
         if (typeof value !== 'string') {
         if (typeof value !== 'string') {
             return `Invalid region code "${value as any}"`;
             return `Invalid region code "${value as any}"`;
         }
         }
-        const activeLocale = typeof locale === 'string' ? locale : this.locale ?? 'en';
+        const activeLocale = this.getActiveLocale(locale);
 
 
         // Awaiting TS types for this API: https://github.com/microsoft/TypeScript/pull/44022/files
         // Awaiting TS types for this API: https://github.com/microsoft/TypeScript/pull/44022/files
         const DisplayNames = (Intl as any).DisplayNames;
         const DisplayNames = (Intl as any).DisplayNames;

+ 22 - 22
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -40,7 +40,7 @@
     "modifying": "A modificar",
     "modifying": "A modificar",
     "orders": "Encomendas",
     "orders": "Encomendas",
     "payment-methods": "Métodos de pagamentos",
     "payment-methods": "Métodos de pagamentos",
-    "product-options": "",
+    "product-options": "Opções",
     "products": "Produtos",
     "products": "Produtos",
     "profile": "Perfil",
     "profile": "Perfil",
     "promotions": "Promoções",
     "promotions": "Promoções",
@@ -113,7 +113,7 @@
     "option-values": "Valor da opção",
     "option-values": "Valor da opção",
     "out-of-stock-threshold": "Limite para fora de estoque",
     "out-of-stock-threshold": "Limite para fora de estoque",
     "out-of-stock-threshold-tooltip": "Define o limite para a variante ser considerada sem estoque. Usar um valor negativo activa o suporte a pedidos pendentes.",
     "out-of-stock-threshold-tooltip": "Define o limite para a variante ser considerada sem estoque. Usar um valor negativo activa o suporte a pedidos pendentes.",
-    "pending-search-index-updates": "",
+    "pending-search-index-updates": "O índice de pesquisa tem atualizações pendentes para executar",
     "price": "Preço",
     "price": "Preço",
     "price-conversion-factor": "Factor de conversão de preço",
     "price-conversion-factor": "Factor de conversão de preço",
     "price-in-channel": "Preço no canal { channel }",
     "price-in-channel": "Preço no canal { channel }",
@@ -132,11 +132,11 @@
     "remove-option": "Eliminar opção",
     "remove-option": "Eliminar opção",
     "remove-product-from-channel": "Eliminar produto do canal",
     "remove-product-from-channel": "Eliminar produto do canal",
     "remove-product-variant-from-channel": "Remover variante do canal?",
     "remove-product-variant-from-channel": "Remover variante do canal?",
-    "run-pending-search-index-updates": "",
-    "running-search-index-updates": "",
+    "run-pending-search-index-updates": "Executar {count, plural, one {1 actualização pendente} other {{count} actualizações pendentes}}",
+    "running-search-index-updates": "A executar {count, plural, one {1 actualização} other {{count} actualizações}}",
     "search-asset-name-or-tag": "Pesquisar pelo nome ou tag",
     "search-asset-name-or-tag": "Pesquisar pelo nome ou tag",
     "search-for-term": "Pesquisar termo",
     "search-for-term": "Pesquisar termo",
-    "search-index-controls": "",
+    "search-index-controls": "Controles de índice de pesquisa",
     "search-product-name-or-code": "Pesquisar por nome ou código do produto",
     "search-product-name-or-code": "Pesquisar por nome ou código do produto",
     "select-product": "Seleccione o produto",
     "select-product": "Seleccione o produto",
     "select-product-variant": "Seleccione a variante do produto",
     "select-product-variant": "Seleccione a variante do produto",
@@ -170,7 +170,7 @@
     "add-new-variants": "Adicionar {count, plural, one {variante} other {{count} variantes}}",
     "add-new-variants": "Adicionar {count, plural, one {variante} other {{count} variantes}}",
     "add-note": "Adicionar nota",
     "add-note": "Adicionar nota",
     "available-languages": "Idiomas disponíveis",
     "available-languages": "Idiomas disponíveis",
-    "browser-default": "",
+    "browser-default": "Navegador padrão",
     "cancel": "Cancelar",
     "cancel": "Cancelar",
     "cancel-navigation": "Continuar a editar",
     "cancel-navigation": "Continuar a editar",
     "change-selection": "Alterar seleccionados",
     "change-selection": "Alterar seleccionados",
@@ -200,18 +200,18 @@
     "expand-entries": "Expandir entradas",
     "expand-entries": "Expandir entradas",
     "extension-running-in-separate-window": "A extensão está a ser executada em uma janela separada",
     "extension-running-in-separate-window": "A extensão está a ser executada em uma janela separada",
     "filter": "Filtro",
     "filter": "Filtro",
-    "general": "",
+    "general": "Geral",
     "guest": "Convidado",
     "guest": "Convidado",
     "items-per-page-option": "{ count } por página",
     "items-per-page-option": "{ count } por página",
     "language": "Idioma",
     "language": "Idioma",
     "launch-extension": "Iniciar extensão",
     "launch-extension": "Iniciar extensão",
     "live-update": "Actualização em tempo real",
     "live-update": "Actualização em tempo real",
-    "locale": "",
+    "locale": "Localidade",
     "log-out": "Sair",
     "log-out": "Sair",
     "login": "Entrar",
     "login": "Entrar",
     "manage-tags": "Gerir tags",
     "manage-tags": "Gerir tags",
     "manage-tags-description": "Atualize ou elimine tags globalmente.",
     "manage-tags-description": "Atualize ou elimine tags globalmente.",
-    "medium-date": "",
+    "medium-date": "Data média",
     "more": "Mais...",
     "more": "Mais...",
     "name": "Nome",
     "name": "Nome",
     "no-results": "Nenhum resultado encontrado",
     "no-results": "Nenhum resultado encontrado",
@@ -219,11 +219,11 @@
     "notify-create-error": "Ocorreu um erro. Não foi possível criar { entity }",
     "notify-create-error": "Ocorreu um erro. Não foi possível criar { entity }",
     "notify-create-success": "Novo(a) { entity } adicionado(a)",
     "notify-create-success": "Novo(a) { entity } adicionado(a)",
     "notify-delete-error": "Ocorreu um erro, não foi possível eliminar { entity }",
     "notify-delete-error": "Ocorreu um erro, não foi possível eliminar { entity }",
-    "notify-delete-success": "Excluído { entity }",
+    "notify-delete-success": "{ entity } excluído(a)",
     "notify-save-changes-error": "Ocorreu um erro. Não foi possível guardar as alterações",
     "notify-save-changes-error": "Ocorreu um erro. Não foi possível guardar as alterações",
     "notify-saved-changes": "Alterações guardadas",
     "notify-saved-changes": "Alterações guardadas",
-    "notify-update-error": "Ocorreu um erro. Não foi possível actualizar { entity }",
-    "notify-update-success": "{ entity } actualizado(a)",
+    "notify-update-error": "Ocorreu um erro. Não foi possível actualizar a entidade { entity }",
+    "notify-update-success": "Entidade ({ entity }) actualizada com sucesso",
     "notify-updated-tags-success": "Tags actualizadas com sucesso",
     "notify-updated-tags-success": "Tags actualizadas com sucesso",
     "open": "Visualizar",
     "open": "Visualizar",
     "password": "Palavra passe",
     "password": "Palavra passe",
@@ -235,12 +235,12 @@
     "remove": "Eliminar",
     "remove": "Eliminar",
     "remove-item-from-list": "Remover item da lista",
     "remove-item-from-list": "Remover item da lista",
     "results-count": "{ count } {count, plural, one {resultado} other {resultados}}",
     "results-count": "{ count } {count, plural, one {resultado} other {resultados}}",
-    "sample-formatting": "",
+    "sample-formatting": "Formatação de amostra",
     "select": "Seleccione...",
     "select": "Seleccione...",
     "select-display-language": "Seleccionar idioma",
     "select-display-language": "Seleccionar idioma",
     "select-today": "Seleccione a data de hoje",
     "select-today": "Seleccione a data de hoje",
-    "set-language": "",
-    "short-date": "",
+    "set-language": "Definir idioma",
+    "short-date": "Data abreviada",
     "tags": "Tags",
     "tags": "Tags",
     "theme": "Tema",
     "theme": "Tema",
     "there-are-unsaved-changes": "Há alterações por guardar. Navegar para outra página fará com que essas alterações sejam perdidas.",
     "there-are-unsaved-changes": "Há alterações por guardar. Navegar para outra página fará com que essas alterações sejam perdidas.",
@@ -287,7 +287,7 @@
     "history-customer-email-update-verified": "Actualização do e-mail confirmada",
     "history-customer-email-update-verified": "Actualização do e-mail confirmada",
     "history-customer-password-reset-requested": "Redefinição de palavra passe solicitada",
     "history-customer-password-reset-requested": "Redefinição de palavra passe solicitada",
     "history-customer-password-reset-verified": "Redefinição de palavra passe verificada",
     "history-customer-password-reset-verified": "Redefinição de palavra passe verificada",
-    "history-customer-password-updated": "Palavra passe atualizada",
+    "history-customer-password-updated": "Palavra passe actualizada",
     "history-customer-registered": "Cliente registado",
     "history-customer-registered": "Cliente registado",
     "history-customer-removed-from-group": "Cliente removido do grupo \"{ groupName }\"",
     "history-customer-removed-from-group": "Cliente removido do grupo \"{ groupName }\"",
     "history-customer-verified": "Cliente verificado",
     "history-customer-verified": "Cliente verificado",
@@ -376,7 +376,7 @@
   },
   },
   "error": {
   "error": {
     "403-forbidden": "No momento, você não está autorizado a aceder \"{ path }\". Você não tem permissão ou a sua sessão expirou.",
     "403-forbidden": "No momento, você não está autorizado a aceder \"{ path }\". Você não tem permissão ou a sua sessão expirou.",
-    "could-not-connect-to-server": "Não foi possível ao servidor Mercado NetBrit no link { url }",
+    "could-not-connect-to-server": "Não foi possível conectar-se ao servidor Vendure",
     "facet-value-form-values-do-not-match": "O número de valores no formulário de etiqueta não corresponde ao número real de valores",
     "facet-value-form-values-do-not-match": "O número de valores no formulário de etiqueta não corresponde ao número real de valores",
     "health-check-failed": "Falha na verificação de integridade do sistema",
     "health-check-failed": "Falha na verificação de integridade do sistema",
     "no-default-shipping-zone-set": "O canal não possui regiões de entrega padrão. Poderá ocorrer erros ao calcular as despesas de envio da encomenda.",
     "no-default-shipping-zone-set": "O canal não possui regiões de entrega padrão. Poderá ocorrer erros ao calcular as despesas de envio da encomenda.",
@@ -403,7 +403,7 @@
     "channels": "Canais",
     "channels": "Canais",
     "collections": "Categorias",
     "collections": "Categorias",
     "countries": "Países",
     "countries": "Países",
-    "customer-groups": "Grupos de clientes",
+    "customer-groups": "Grupos",
     "customers": "Clientes",
     "customers": "Clientes",
     "facets": "Etiquetas",
     "facets": "Etiquetas",
     "global-settings": "Configurações Gerais",
     "global-settings": "Configurações Gerais",
@@ -508,9 +508,9 @@
     "payment-method": "Método de pagamento",
     "payment-method": "Método de pagamento",
     "payment-state": "Estado",
     "payment-state": "Estado",
     "payment-to-refund": "Pagamento para reembolso",
     "payment-to-refund": "Pagamento para reembolso",
-    "placed-at": "Posicionado em",
-    "placed-at-end": "Posicionado no final",
-    "placed-at-start": "Posicionado no início",
+    "placed-at": "Adicionada em",
+    "placed-at-end": "Adicionada até",
+    "placed-at-start": "Adicionada a partir de",
     "preview-changes": "Revisar mudanças",
     "preview-changes": "Revisar mudanças",
     "product-name": "Nome do produto",
     "product-name": "Nome do produto",
     "product-sku": "SKU",
     "product-sku": "SKU",
@@ -550,7 +550,7 @@
     "shipping-method": "Método de envio",
     "shipping-method": "Método de envio",
     "state": "Estado",
     "state": "Estado",
     "sub-total": "Subtotal",
     "sub-total": "Subtotal",
-    "successfully-updated-fulfillment": "Envio atualizado com sucesso",
+    "successfully-updated-fulfillment": "Envio actualizado com sucesso",
     "surcharges": "Sobretaxas",
     "surcharges": "Sobretaxas",
     "tax-base": "Base tributária",
     "tax-base": "Base tributária",
     "tax-description": "Descrição de tributação",
     "tax-description": "Descrição de tributação",

+ 3 - 3
packages/asset-server-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/asset-server-plugin",
   "name": "@vendure/asset-server-plugin",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "types": "lib/index.d.ts",
   "files": [
   "files": [
@@ -24,8 +24,8 @@
     "@types/fs-extra": "^9.0.8",
     "@types/fs-extra": "^9.0.8",
     "@types/node-fetch": "^2.5.8",
     "@types/node-fetch": "^2.5.8",
     "@types/sharp": "^0.27.1",
     "@types/sharp": "^0.27.1",
-    "@vendure/common": "^1.4.3",
-    "@vendure/core": "^1.4.3",
+    "@vendure/common": "^1.4.4",
+    "@vendure/core": "^1.4.4",
     "aws-sdk": "^2.856.0",
     "aws-sdk": "^2.856.0",
     "express": "^4.17.1",
     "express": "^4.17.1",
     "node-fetch": "^2.6.1",
     "node-fetch": "^2.6.1",

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/common",
   "name": "@vendure/common",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "main": "index.js",
   "main": "index.js",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {

File diff suppressed because it is too large
+ 747 - 732
packages/common/src/generated-shop-types.ts


+ 14 - 0
packages/core/e2e/entity-hydrator.e2e-spec.ts

@@ -140,6 +140,15 @@ describe('Entity hydration', () => {
         expect(hydrateProductAsset.assets).toEqual([]);
         expect(hydrateProductAsset.assets).toEqual([]);
     });
     });
 
 
+    // https://github.com/vendure-ecommerce/vendure/issues/1324
+    it('correctly handles empty nested array relations', async () => {
+        const { hydrateProductWithNoFacets } = await adminClient.query<{
+            hydrateProductWithNoFacets: Product;
+        }>(GET_HYDRATED_PRODUCT_NO_FACETS);
+
+        expect(hydrateProductWithNoFacets.facetValues).toEqual([]);
+    });
+
     // https://github.com/vendure-ecommerce/vendure/issues/1161
     // https://github.com/vendure-ecommerce/vendure/issues/1161
     it('correctly expands missing relations', async () => {
     it('correctly expands missing relations', async () => {
         const { hydrateProductVariant } = await adminClient.query<{ hydrateProductVariant: ProductVariant }>(
         const { hydrateProductVariant } = await adminClient.query<{ hydrateProductVariant: ProductVariant }>(
@@ -224,6 +233,11 @@ const GET_HYDRATED_PRODUCT = gql`
         hydrateProduct(id: $id)
         hydrateProduct(id: $id)
     }
     }
 `;
 `;
+const GET_HYDRATED_PRODUCT_NO_FACETS = gql`
+    query GetHydratedProductWithNoFacets {
+        hydrateProductWithNoFacets
+    }
+`;
 const GET_HYDRATED_PRODUCT_ASSET = gql`
 const GET_HYDRATED_PRODUCT_ASSET = gql`
     query GetHydratedProductAsset($id: ID!) {
     query GetHydratedProductAsset($id: ID!) {
         hydrateProductAsset(id: $id)
         hydrateProductAsset(id: $id)

+ 24 - 0
packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts

@@ -6,9 +6,11 @@ import {
     Ctx,
     Ctx,
     EntityHydrator,
     EntityHydrator,
     ID,
     ID,
+    LanguageCode,
     OrderService,
     OrderService,
     PluginCommonModule,
     PluginCommonModule,
     Product,
     Product,
+    ProductService,
     ProductVariantService,
     ProductVariantService,
     RequestContext,
     RequestContext,
     TransactionalConnection,
     TransactionalConnection,
@@ -23,6 +25,7 @@ export class TestAdminPluginResolver {
         private orderService: OrderService,
         private orderService: OrderService,
         private channelService: ChannelService,
         private channelService: ChannelService,
         private productVariantService: ProductVariantService,
         private productVariantService: ProductVariantService,
+        private productService: ProductService,
         private entityHydrator: EntityHydrator,
         private entityHydrator: EntityHydrator,
     ) {}
     ) {}
 
 
@@ -67,6 +70,26 @@ export class TestAdminPluginResolver {
         return variant;
         return variant;
     }
     }
 
 
+    // Test case for https://github.com/vendure-ecommerce/vendure/issues/1324
+    @Query()
+    async hydrateProductWithNoFacets(@Ctx() ctx: RequestContext) {
+        const product = await this.productService.create(ctx, {
+            enabled: true,
+            translations: [
+                {
+                    languageCode: LanguageCode.en,
+                    name: 'test',
+                    slug: 'test',
+                    description: 'test',
+                },
+            ],
+        });
+        await this.entityHydrator.hydrate(ctx, product, {
+            relations: ['facetValues', 'facetValues.facet'],
+        });
+        return product;
+    }
+
     // Test case for https://github.com/vendure-ecommerce/vendure/issues/1172
     // Test case for https://github.com/vendure-ecommerce/vendure/issues/1172
     @Query()
     @Query()
     async hydrateOrder(@Ctx() ctx: RequestContext, @Args() args: { id: ID }) {
     async hydrateOrder(@Ctx() ctx: RequestContext, @Args() args: { id: ID }) {
@@ -110,6 +133,7 @@ export class TestAdminPluginResolver {
         schema: gql`
         schema: gql`
             extend type Query {
             extend type Query {
                 hydrateProduct(id: ID!): JSON
                 hydrateProduct(id: ID!): JSON
+                hydrateProductWithNoFacets: JSON
                 hydrateProductAsset(id: ID!): JSON
                 hydrateProductAsset(id: ID!): JSON
                 hydrateProductVariant(id: ID!): JSON
                 hydrateProductVariant(id: ID!): JSON
                 hydrateOrder(id: ID!): JSON
                 hydrateOrder(id: ID!): JSON

File diff suppressed because it is too large
+ 710 - 695
packages/core/e2e/graphql/generated-e2e-shop-types.ts


+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/core",
   "name": "@vendure/core",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "description": "A modern, headless ecommerce framework",
   "description": "A modern, headless ecommerce framework",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
@@ -49,7 +49,7 @@
     "@nestjs/testing": "7.6.17",
     "@nestjs/testing": "7.6.17",
     "@nestjs/typeorm": "7.1.5",
     "@nestjs/typeorm": "7.1.5",
     "@types/fs-extra": "^9.0.1",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^1.4.3",
+    "@vendure/common": "^1.4.4",
     "apollo-server-express": "2.24.1",
     "apollo-server-express": "2.24.1",
     "bcrypt": "^5.0.0",
     "bcrypt": "^5.0.0",
     "body-parser": "^1.19.0",
     "body-parser": "^1.19.0",

+ 3 - 0
packages/core/src/api/resolvers/shop/shop-auth.resolver.ts

@@ -237,6 +237,9 @@ export class ShopAuthResolver extends BaseAuthResolver {
             req,
             req,
             res,
             res,
         );
         );
+        if (isGraphQlErrorResult(authResult) && authResult.__typename === 'NotVerifiedError') {
+            return authResult;
+        }
         if (isGraphQlErrorResult(authResult)) {
         if (isGraphQlErrorResult(authResult)) {
             // This should never occur in theory
             // This should never occur in theory
             throw authResult;
             throw authResult;

+ 1 - 0
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -239,6 +239,7 @@ union ResetPasswordResult =
     | PasswordResetTokenInvalidError
     | PasswordResetTokenInvalidError
     | PasswordResetTokenExpiredError
     | PasswordResetTokenExpiredError
     | NativeAuthStrategyError
     | NativeAuthStrategyError
+    | NotVerifiedError
 union NativeAuthenticationResult =
 union NativeAuthenticationResult =
       CurrentUser
       CurrentUser
     | InvalidCredentialsError
     | InvalidCredentialsError

+ 2 - 2
packages/core/src/config/promotion/promotion-action.ts

@@ -309,7 +309,7 @@ export class PromotionOrderAction<
 
 
     /** @internal */
     /** @internal */
     execute(ctx: RequestContext, order: Order, args: ConfigArg[], state: PromotionState) {
     execute(ctx: RequestContext, order: Order, args: ConfigArg[], state: PromotionState) {
-        const actionState = this.conditions ? pick(state, Object.keys(this.conditions)) : {};
+        const actionState = this.conditions ? pick(state, this.conditions.map(c => c.code)) : {};
         return this.executeFn(ctx, order, this.argsArrayToHash(args), actionState as ConditionState<U>);
         return this.executeFn(ctx, order, this.argsArrayToHash(args), actionState as ConditionState<U>);
     }
     }
 }
 }
@@ -340,7 +340,7 @@ export class PromotionShippingAction<
         args: ConfigArg[],
         args: ConfigArg[],
         state: PromotionState,
         state: PromotionState,
     ) {
     ) {
-        const actionState = this.conditions ? pick(state, Object.keys(this.conditions)) : {};
+        const actionState = this.conditions ? pick(state, this.conditions.map(c => c.code)) : {};
         return this.executeFn(
         return this.executeFn(
             ctx,
             ctx,
             shippingLine,
             shippingLine,

+ 4 - 1
packages/core/src/entity/asset/orderable-asset.entity.ts

@@ -6,11 +6,14 @@ import { Asset } from '../asset/asset.entity';
 import { VendureEntity } from '../base/base.entity';
 import { VendureEntity } from '../base/base.entity';
 
 
 /**
 /**
+ * @description
  * This base class is extended in order to enable specific ordering of the one-to-many
  * This base class is extended in order to enable specific ordering of the one-to-many
  * Entity -> Assets relation. Using a many-to-many relation does not provide a way
  * Entity -> Assets relation. Using a many-to-many relation does not provide a way
  * to guarantee order of the Assets, so this entity is used in place of the
  * to guarantee order of the Assets, so this entity is used in place of the
  * usual join table that would be created by TypeORM.
  * usual join table that would be created by TypeORM.
  * See https://typeorm.io/#/many-to-many-relations/many-to-many-relations-with-custom-properties
  * See https://typeorm.io/#/many-to-many-relations/many-to-many-relations-with-custom-properties
+ *
+ * @docsCategory entities
  */
  */
 export abstract class OrderableAsset extends VendureEntity implements Orderable {
 export abstract class OrderableAsset extends VendureEntity implements Orderable {
     protected constructor(input?: DeepPartial<OrderableAsset>) {
     protected constructor(input?: DeepPartial<OrderableAsset>) {
@@ -20,7 +23,7 @@ export abstract class OrderableAsset extends VendureEntity implements Orderable
     @Column()
     @Column()
     assetId: ID;
     assetId: ID;
 
 
-    @ManyToOne((type) => Asset, { eager: true, onDelete: 'CASCADE' })
+    @ManyToOne(type => Asset, { eager: true, onDelete: 'CASCADE' })
     asset: Asset;
     asset: Asset;
 
 
     @Column()
     @Column()

+ 1 - 1
packages/core/src/i18n/messages/pt_PT.json

@@ -115,4 +115,4 @@
     "zone-used-in-channels": "A região seleccionada não pode ser eliminada porque é utilizada como padrão nos seguintes canais: {channelCodes}",
     "zone-used-in-channels": "A região seleccionada não pode ser eliminada porque é utilizada como padrão nos seguintes canais: {channelCodes}",
     "zone-used-in-tax-rates": "A região seleccionada não pode ser eliminada, porque é utilizada nos seguintes impostos: {taxRateNames}"
     "zone-used-in-tax-rates": "A região seleccionada não pode ser eliminada, porque é utilizada nos seguintes impostos: {taxRateNames}"
   }
   }
-}
+}

+ 4 - 1
packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts

@@ -218,7 +218,10 @@ export class EntityHydrator {
             const part = path[i];
             const part = path[i];
             const isLast = i === path.length - 1;
             const isLast = i === path.length - 1;
             if (relation[part]) {
             if (relation[part]) {
-                relation = Array.isArray(relation[part]) && !isLast ? relation[part][0] : relation[part];
+                relation =
+                    Array.isArray(relation[part]) && relation[part].length && !isLast
+                        ? relation[part][0]
+                        : relation[part];
             } else {
             } else {
                 return;
                 return;
             }
             }

+ 40 - 2
packages/core/src/service/services/asset.service.ts

@@ -51,11 +51,26 @@ import { TagService } from './tag.service';
 // tslint:disable-next-line:no-var-requires
 // tslint:disable-next-line:no-var-requires
 const sizeOf = require('image-size');
 const sizeOf = require('image-size');
 
 
+/**
+ * @description
+ * Certain entities (Product, ProductVariant, Collection) use this interface
+ * to model a featured asset and then a list of assets with a defined order.
+ *
+ * @docsCategory services
+ * @docsPage AssetService
+ */
 export interface EntityWithAssets extends VendureEntity {
 export interface EntityWithAssets extends VendureEntity {
     featuredAsset: Asset | null;
     featuredAsset: Asset | null;
     assets: OrderableAsset[];
     assets: OrderableAsset[];
 }
 }
 
 
+/**
+ * @description
+ * Used when updating entities which implement {@link EntityWithAssets}.
+ *
+ * @docsCategory services
+ * @docsPage AssetService
+ */
 export interface EntityAssetInput {
 export interface EntityAssetInput {
     assetIds?: ID[] | null;
     assetIds?: ID[] | null;
     featuredAssetId?: ID | null;
     featuredAssetId?: ID | null;
@@ -66,6 +81,7 @@ export interface EntityAssetInput {
  * Contains methods relating to {@link Asset} entities.
  * Contains methods relating to {@link Asset} entities.
  *
  *
  * @docsCategory services
  * @docsCategory services
+ * @docsWeight 0
  */
  */
 @Injectable()
 @Injectable()
 export class AssetService {
 export class AssetService {
@@ -152,6 +168,11 @@ export class AssetService {
         return (entityWithFeaturedAsset && entityWithFeaturedAsset.featuredAsset) || undefined;
         return (entityWithFeaturedAsset && entityWithFeaturedAsset.featuredAsset) || undefined;
     }
     }
 
 
+    /**
+     * @description
+     * Returns the Assets of an entity which has a well-ordered list of Assets, such as Product,
+     * ProductVariant or Collection.
+     */
     async getEntityAssets<T extends EntityWithAssets>(
     async getEntityAssets<T extends EntityWithAssets>(
         ctx: RequestContext,
         ctx: RequestContext,
         entity: T,
         entity: T,
@@ -216,6 +237,7 @@ export class AssetService {
     }
     }
 
 
     /**
     /**
+     * @description
      * Updates the assets / featuredAsset of an entity, ensuring that only valid assetIds are used.
      * Updates the assets / featuredAsset of an entity, ensuring that only valid assetIds are used.
      */
      */
     async updateEntityAssets<T extends EntityWithAssets>(
     async updateEntityAssets<T extends EntityWithAssets>(
@@ -241,7 +263,12 @@ export class AssetService {
     }
     }
 
 
     /**
     /**
-     * Create an Asset based on a file uploaded via the GraphQL API.
+     * @description
+     * Create an Asset based on a file uploaded via the GraphQL API. The file should be uploaded
+     * using the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec),
+     * e.g. using the [apollo-upload-client](https://github.com/jaydenseric/apollo-upload-client) npm package.
+     *
+     * See the [Uploading Files docs](/docs/developer-guide/uploading-files) for an example of usage.
      */
      */
     async create(ctx: RequestContext, input: CreateAssetInput): Promise<CreateAssetResult> {
     async create(ctx: RequestContext, input: CreateAssetInput): Promise<CreateAssetResult> {
         return new Promise(async (resolve, reject) => {
         return new Promise(async (resolve, reject) => {
@@ -272,6 +299,10 @@ export class AssetService {
         });
         });
     }
     }
 
 
+    /**
+     * @description
+     * Updates the name, focalPoint, tags & custom fields of an Asset.
+     */
     async update(ctx: RequestContext, input: UpdateAssetInput): Promise<Asset> {
     async update(ctx: RequestContext, input: UpdateAssetInput): Promise<Asset> {
         const asset = await this.connection.getEntityOrThrow(ctx, Asset, input.id);
         const asset = await this.connection.getEntityOrThrow(ctx, Asset, input.id);
         if (input.focalPoint) {
         if (input.focalPoint) {
@@ -289,6 +320,11 @@ export class AssetService {
         return updatedAsset;
         return updatedAsset;
     }
     }
 
 
+    /**
+     * @description
+     * Deletes an Asset after performing checks to ensure that the Asset is not currently in use
+     * by a Product, ProductVariant or Collection.
+     */
     async delete(
     async delete(
         ctx: RequestContext,
         ctx: RequestContext,
         ids: ID[],
         ids: ID[],
@@ -386,7 +422,8 @@ export class AssetService {
     }
     }
 
 
     /**
     /**
-     * Create an Asset from a file stream created during data import.
+     * @description
+     * Create an Asset from a file stream, for example to create an Asset during data import.
      */
      */
     async createFromFileStream(stream: ReadStream): Promise<CreateAssetResult>;
     async createFromFileStream(stream: ReadStream): Promise<CreateAssetResult>;
     async createFromFileStream(stream: Readable, filePath: string): Promise<CreateAssetResult>;
     async createFromFileStream(stream: Readable, filePath: string): Promise<CreateAssetResult>;
@@ -406,6 +443,7 @@ export class AssetService {
     }
     }
 
 
     /**
     /**
+     * @description
      * Unconditionally delete given assets.
      * Unconditionally delete given assets.
      * Does not remove assets from channels
      * Does not remove assets from channels
      */
      */

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/create",
   "name": "@vendure/create",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "license": "MIT",
   "license": "MIT",
   "bin": {
   "bin": {
     "create": "./index.js"
     "create": "./index.js"
@@ -28,13 +28,13 @@
     "@types/handlebars": "^4.1.0",
     "@types/handlebars": "^4.1.0",
     "@types/listr": "^0.14.2",
     "@types/listr": "^0.14.2",
     "@types/semver": "^6.2.2",
     "@types/semver": "^6.2.2",
-    "@vendure/core": "^1.4.3",
+    "@vendure/core": "^1.4.4",
     "rimraf": "^3.0.2",
     "rimraf": "^3.0.2",
     "ts-node": "^10.2.1",
     "ts-node": "^10.2.1",
     "typescript": "4.3.5"
     "typescript": "4.3.5"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@vendure/common": "^1.4.3",
+    "@vendure/common": "^1.4.4",
     "chalk": "^4.1.0",
     "chalk": "^4.1.0",
     "commander": "^7.1.0",
     "commander": "^7.1.0",
     "cross-spawn": "^7.0.3",
     "cross-spawn": "^7.0.3",

+ 9 - 9
packages/dev-server/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "dev-server",
   "name": "dev-server",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "main": "index.js",
   "main": "index.js",
   "license": "MIT",
   "license": "MIT",
   "private": true,
   "private": true,
@@ -14,18 +14,18 @@
     "load-test:100k": "node -r ts-node/register load-testing/run-load-test.ts 100000"
     "load-test:100k": "node -r ts-node/register load-testing/run-load-test.ts 100000"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@vendure/admin-ui-plugin": "^1.4.3",
-    "@vendure/asset-server-plugin": "^1.4.3",
-    "@vendure/common": "^1.4.3",
-    "@vendure/core": "^1.4.3",
-    "@vendure/elasticsearch-plugin": "^1.4.3",
-    "@vendure/email-plugin": "^1.4.3",
+    "@vendure/admin-ui-plugin": "^1.4.4",
+    "@vendure/asset-server-plugin": "^1.4.4",
+    "@vendure/common": "^1.4.4",
+    "@vendure/core": "^1.4.4",
+    "@vendure/elasticsearch-plugin": "^1.4.4",
+    "@vendure/email-plugin": "^1.4.4",
     "typescript": "4.3.5"
     "typescript": "4.3.5"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@types/csv-stringify": "^3.1.0",
     "@types/csv-stringify": "^3.1.0",
-    "@vendure/testing": "^1.4.3",
-    "@vendure/ui-devkit": "^1.4.3",
+    "@vendure/testing": "^1.4.4",
+    "@vendure/ui-devkit": "^1.4.4",
     "concurrently": "^5.0.0",
     "concurrently": "^5.0.0",
     "csv-stringify": "^5.3.3"
     "csv-stringify": "^5.3.3"
   }
   }

+ 7 - 0
packages/dev-server/test-plugins/customer-avatar/api-extensions.ts

@@ -0,0 +1,7 @@
+import gql from 'graphql-tag';
+
+export const shopApiExtensions = gql`
+    extend type Mutation {
+        setCustomerAvatar(file: Upload!): Asset
+    }
+`;

+ 22 - 0
packages/dev-server/test-plugins/customer-avatar/customer-avatar-plugin.ts

@@ -0,0 +1,22 @@
+import { Asset, PluginCommonModule, VendurePlugin } from '@vendure/core';
+
+import { shopApiExtensions } from './api-extensions';
+import { CustomerAvatarResolver } from './customer-avatar.resolver';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    shopApiExtensions: {
+        schema: shopApiExtensions,
+        resolvers: [CustomerAvatarResolver],
+    },
+    configuration: config => {
+        config.customFields.Customer.push({
+            name: 'avatar',
+            type: 'relation',
+            entity: Asset,
+            nullable: true,
+        });
+        return config;
+    },
+})
+export class CustomerAvatarPlugin {}

+ 55 - 0
packages/dev-server/test-plugins/customer-avatar/customer-avatar.resolver.ts

@@ -0,0 +1,55 @@
+import { Args, Mutation, Resolver } from '@nestjs/graphql';
+import { Asset } from '@vendure/common/lib/generated-types';
+import {
+    Allow,
+    AssetService,
+    Ctx,
+    CustomerService,
+    isGraphQlErrorResult,
+    Permission,
+    RequestContext,
+    Transaction,
+} from '@vendure/core';
+
+@Resolver()
+export class CustomerAvatarResolver {
+    constructor(private assetService: AssetService, private customerService: CustomerService) {}
+
+    @Transaction()
+    @Mutation()
+    @Allow(Permission.Authenticated)
+    async setCustomerAvatar(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { file: any },
+    ): Promise<Asset | undefined> {
+        const userId = ctx.activeUserId;
+        if (!userId) {
+            return;
+        }
+        const customer = await this.customerService.findOneByUserId(ctx, userId);
+        if (!customer) {
+            return;
+        }
+        // Create an Asset from the uploaded file
+        const asset = await this.assetService.create(ctx, {
+            file: args.file,
+            tags: ['avatar'],
+        });
+        // Check to make sure there was no error when
+        // creating the Asset
+        if (isGraphQlErrorResult(asset)) {
+            // MimeTypeError
+            throw asset;
+        }
+        // Asset created correctly, so assign it as the
+        // avatar of the current Customer
+        await this.customerService.update(ctx, {
+            id: customer.id,
+            customFields: {
+                avatarId: asset.id,
+            },
+        });
+
+        return asset;
+    }
+}

+ 3 - 3
packages/elasticsearch-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/elasticsearch-plugin",
   "name": "@vendure/elasticsearch-plugin",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "types": "lib/index.d.ts",
@@ -25,8 +25,8 @@
     "fast-deep-equal": "^3.1.3"
     "fast-deep-equal": "^3.1.3"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@vendure/common": "^1.4.3",
-    "@vendure/core": "^1.4.3",
+    "@vendure/common": "^1.4.4",
+    "@vendure/core": "^1.4.4",
     "rimraf": "^3.0.2",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"
     "typescript": "4.3.5"
   }
   }

+ 3 - 3
packages/email-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/email-plugin",
   "name": "@vendure/email-plugin",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "license": "MIT",
   "license": "MIT",
   "main": "lib/index.js",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "types": "lib/index.d.ts",
@@ -35,8 +35,8 @@
     "@types/fs-extra": "^9.0.1",
     "@types/fs-extra": "^9.0.1",
     "@types/handlebars": "^4.1.0",
     "@types/handlebars": "^4.1.0",
     "@types/mjml": "^4.0.4",
     "@types/mjml": "^4.0.4",
-    "@vendure/common": "^1.4.3",
-    "@vendure/core": "^1.4.3",
+    "@vendure/common": "^1.4.4",
+    "@vendure/core": "^1.4.4",
     "rimraf": "^3.0.2",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"
     "typescript": "4.3.5"
   }
   }

+ 7 - 2
packages/email-plugin/src/dev-mailbox.ts

@@ -82,7 +82,12 @@ export class DevMailbox {
 
 
     private async getEmailList(outputPath: string) {
     private async getEmailList(outputPath: string) {
         const list = await fs.readdir(outputPath);
         const list = await fs.readdir(outputPath);
-        const contents: any[] = [];
+        const contents: Array<{
+            fileName: string;
+            date: string;
+            subject: string;
+            recipient: string;
+        }> = [];
         for (const fileName of list.filter(name => name.endsWith('.json'))) {
         for (const fileName of list.filter(name => name.endsWith('.json'))) {
             const json = await fs.readFile(path.join(outputPath, fileName), 'utf-8');
             const json = await fs.readFile(path.join(outputPath, fileName), 'utf-8');
             const content = JSON.parse(json);
             const content = JSON.parse(json);
@@ -94,7 +99,7 @@ export class DevMailbox {
             });
             });
         }
         }
         contents.sort((a, b) => {
         contents.sort((a, b) => {
-            return +new Date(b.date) - +new Date(a.date);
+            return a.fileName < b.fileName ? 1 : -1;
         });
         });
         return contents;
         return contents;
     }
     }

+ 3 - 3
packages/job-queue-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/job-queue-plugin",
   "name": "@vendure/job-queue-plugin",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "license": "MIT",
   "license": "MIT",
   "main": "package/index.js",
   "main": "package/index.js",
   "types": "package/index.d.ts",
   "types": "package/index.d.ts",
@@ -24,8 +24,8 @@
   "devDependencies": {
   "devDependencies": {
     "@google-cloud/pubsub": "^2.8.0",
     "@google-cloud/pubsub": "^2.8.0",
     "@types/redis": "^2.8.28",
     "@types/redis": "^2.8.28",
-    "@vendure/common": "^1.4.3",
-    "@vendure/core": "^1.4.3",
+    "@vendure/common": "^1.4.4",
+    "@vendure/core": "^1.4.4",
     "bullmq": "^1.40.1",
     "bullmq": "^1.40.1",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
     "rimraf": "^3.0.2",
     "rimraf": "^3.0.2",

File diff suppressed because it is too large
+ 710 - 695
packages/payments-plugin/e2e/graphql/generated-shop-types.ts


+ 4 - 4
packages/payments-plugin/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "@vendure/payments-plugin",
     "name": "@vendure/payments-plugin",
-    "version": "1.4.3",
+    "version": "1.4.4",
     "license": "MIT",
     "license": "MIT",
     "main": "package/index.js",
     "main": "package/index.js",
     "types": "package/index.d.ts",
     "types": "package/index.d.ts",
@@ -27,9 +27,9 @@
     "devDependencies": {
     "devDependencies": {
         "@mollie/api-client": "^3.5.1",
         "@mollie/api-client": "^3.5.1",
         "@types/braintree": "^2.22.15",
         "@types/braintree": "^2.22.15",
-        "@vendure/common": "^1.4.3",
-        "@vendure/core": "^1.4.3",
-        "@vendure/testing": "^1.4.3",
+        "@vendure/common": "^1.4.4",
+        "@vendure/core": "^1.4.4",
+        "@vendure/testing": "^1.4.4",
         "braintree": "^3.0.0",
         "braintree": "^3.0.0",
         "nock": "^13.1.4",
         "nock": "^13.1.4",
         "rimraf": "^3.0.2",
         "rimraf": "^3.0.2",

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/testing",
   "name": "@vendure/testing",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "description": "End-to-end testing tools for Vendure projects",
   "description": "End-to-end testing tools for Vendure projects",
   "keywords": [
   "keywords": [
     "vendure",
     "vendure",
@@ -34,7 +34,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@types/node-fetch": "^2.5.4",
     "@types/node-fetch": "^2.5.4",
-    "@vendure/common": "^1.4.3",
+    "@vendure/common": "^1.4.4",
     "faker": "^4.1.0",
     "faker": "^4.1.0",
     "form-data": "^3.0.0",
     "form-data": "^3.0.0",
     "graphql": "15.5.1",
     "graphql": "15.5.1",
@@ -45,7 +45,7 @@
   "devDependencies": {
   "devDependencies": {
     "@types/mysql": "^2.15.15",
     "@types/mysql": "^2.15.15",
     "@types/pg": "^7.14.5",
     "@types/pg": "^7.14.5",
-    "@vendure/core": "^1.4.3",
+    "@vendure/core": "^1.4.4",
     "mysql": "^2.18.1",
     "mysql": "^2.18.1",
     "pg": "^8.4.0",
     "pg": "^8.4.0",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",

+ 4 - 4
packages/ui-devkit/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/ui-devkit",
   "name": "@vendure/ui-devkit",
-  "version": "1.4.3",
+  "version": "1.4.4",
   "description": "A library for authoring Vendure Admin UI extensions",
   "description": "A library for authoring Vendure Admin UI extensions",
   "keywords": [
   "keywords": [
     "vendure",
     "vendure",
@@ -40,8 +40,8 @@
     "@angular/cli": "12.2.2",
     "@angular/cli": "12.2.2",
     "@angular/compiler": "12.2.2",
     "@angular/compiler": "12.2.2",
     "@angular/compiler-cli": "12.2.2",
     "@angular/compiler-cli": "12.2.2",
-    "@vendure/admin-ui": "^1.4.3",
-    "@vendure/common": "^1.4.3",
+    "@vendure/admin-ui": "^1.4.4",
+    "@vendure/common": "^1.4.4",
     "chalk": "^4.1.0",
     "chalk": "^4.1.0",
     "chokidar": "^3.5.1",
     "chokidar": "^3.5.1",
     "fs-extra": "^10.0.0",
     "fs-extra": "^10.0.0",
@@ -52,7 +52,7 @@
     "@rollup/plugin-node-resolve": "^11.2.0",
     "@rollup/plugin-node-resolve": "^11.2.0",
     "@types/fs-extra": "^9.0.8",
     "@types/fs-extra": "^9.0.8",
     "@types/glob": "^7.1.3",
     "@types/glob": "^7.1.3",
-    "@vendure/core": "^1.4.3",
+    "@vendure/core": "^1.4.4",
     "rimraf": "^3.0.2",
     "rimraf": "^3.0.2",
     "rollup": "^2.40.0",
     "rollup": "^2.40.0",
     "rollup-plugin-terser": "^7.0.2",
     "rollup-plugin-terser": "^7.0.2",

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


Some files were not shown because too many files changed in this diff