From fd8a77746ccc566cb0edc7e6d19ca3dcf36a10a5 Mon Sep 17 00:00:00 2001 From: Andreas Sonnleitner <56999154+asonnleitner@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:14:58 +0200 Subject: [PATCH 01/22] fix(payments-plugin): Fix stripe payment transaction handling (#2402) Implement transactional handling for Stripe Webhook. This is in response to race conditions arising in setups using DB replication. - Wrap all Stripe webhook-related operations inside a single transaction using `TransactionalConnection.withTransaction()`. - Ensures that all database operations within the webhook use the "master" instance. This change aims to solve the issue of database operations in the Stripe webhook not consistently using the master instance, which led to inconsistencies in low-latency environments. --- packages/payments-plugin/src/stripe/index.ts | 3 +- .../src/stripe/stripe-client.ts | 2 +- .../src/stripe/stripe-utils.ts | 14 +- .../src/stripe/stripe.controller.ts | 131 +++++++++--------- .../src/stripe/stripe.resolver.ts | 15 +- .../src/stripe/stripe.service.ts | 20 +-- packages/payments-plugin/src/stripe/types.ts | 6 +- 7 files changed, 81 insertions(+), 110 deletions(-) diff --git a/packages/payments-plugin/src/stripe/index.ts b/packages/payments-plugin/src/stripe/index.ts index ae146a8dac..cdd439f581 100644 --- a/packages/payments-plugin/src/stripe/index.ts +++ b/packages/payments-plugin/src/stripe/index.ts @@ -1,2 +1 @@ -export * from './stripe.plugin'; -export * from './'; +export { StripePlugin } from './stripe.plugin'; diff --git a/packages/payments-plugin/src/stripe/stripe-client.ts b/packages/payments-plugin/src/stripe/stripe-client.ts index 86d1277931..6891b7b23d 100644 --- a/packages/payments-plugin/src/stripe/stripe-client.ts +++ b/packages/payments-plugin/src/stripe/stripe-client.ts @@ -6,7 +6,7 @@ import Stripe from 'stripe'; export class VendureStripeClient extends Stripe { constructor(private apiKey: string, public webhookSecret: string) { super(apiKey, { - apiVersion: null as any, // Use accounts default version + apiVersion: null as unknown as Stripe.LatestApiVersion, // Use accounts default version }); } } diff --git a/packages/payments-plugin/src/stripe/stripe-utils.ts b/packages/payments-plugin/src/stripe/stripe-utils.ts index 92daeccab8..3f5c55ef71 100644 --- a/packages/payments-plugin/src/stripe/stripe-utils.ts +++ b/packages/payments-plugin/src/stripe/stripe-utils.ts @@ -12,10 +12,7 @@ import { CurrencyCode, Order } from '@vendure/core'; * stores money amounts multiplied by 100). See https://github.com/vendure-ecommerce/vendure/issues/1630 */ export function getAmountInStripeMinorUnits(order: Order): number { - const amountInStripeMinorUnits = currencyHasFractionPart(order.currencyCode) - ? order.totalWithTax - : Math.round(order.totalWithTax / 100); - return amountInStripeMinorUnits; + return currencyHasFractionPart(order.currencyCode) ? order.totalWithTax : Math.round(order.totalWithTax / 100); } /** @@ -24,10 +21,7 @@ export function getAmountInStripeMinorUnits(order: Order): number { * used by Vendure. */ export function getAmountFromStripeMinorUnits(order: Order, stripeAmount: number): number { - const amountInVendureMinorUnits = currencyHasFractionPart(order.currencyCode) - ? stripeAmount - : stripeAmount * 100; - return amountInVendureMinorUnits; + return currencyHasFractionPart(order.currencyCode) ? stripeAmount : stripeAmount * 100; } function currencyHasFractionPart(currencyCode: CurrencyCode): boolean { @@ -36,6 +30,6 @@ function currencyHasFractionPart(currencyCode: CurrencyCode): boolean { currency: currencyCode, currencyDisplay: 'symbol', }).formatToParts(123.45); - const hasFractionPart = !!parts.find(p => p.type === 'fraction'); - return hasFractionPart; + + return !!parts.find(p => p.type === 'fraction'); } diff --git a/packages/payments-plugin/src/stripe/stripe.controller.ts b/packages/payments-plugin/src/stripe/stripe.controller.ts index f23d40f24f..672fbb1e89 100644 --- a/packages/payments-plugin/src/stripe/stripe.controller.ts +++ b/packages/payments-plugin/src/stripe/stripe.controller.ts @@ -1,18 +1,9 @@ import { Controller, Headers, HttpStatus, Post, Req, Res } from '@nestjs/common'; -import { - InternalServerError, - LanguageCode, - Logger, - Order, - OrderService, - PaymentMethod, - PaymentMethodService, - RequestContext, - RequestContextService, -} from '@vendure/core'; +import type { PaymentMethod, RequestContext } from '@vendure/core'; +import { InternalServerError, LanguageCode, Logger, Order, OrderService, PaymentMethodService, RequestContextService, TransactionalConnection } from '@vendure/core'; import { OrderStateTransitionError } from '@vendure/core/dist/common/error/generated-graphql-shop-errors'; -import { Response } from 'express'; -import Stripe from 'stripe'; +import type { Response } from 'express'; +import type Stripe from 'stripe'; import { loggerCtx } from './constants'; import { stripePaymentMethodHandler } from './stripe.handler'; @@ -30,6 +21,7 @@ export class StripeController { private orderService: OrderService, private stripeService: StripeService, private requestContextService: RequestContextService, + private connection: TransactionalConnection, ) {} @Post('stripe') @@ -43,72 +35,76 @@ export class StripeController { response.status(HttpStatus.BAD_REQUEST).send(missingHeaderErrorMessage); return; } + const event = request.body as Stripe.Event; const paymentIntent = event.data.object as Stripe.PaymentIntent; + if (!paymentIntent) { Logger.error(noPaymentIntentErrorMessage, loggerCtx); response.status(HttpStatus.BAD_REQUEST).send(noPaymentIntentErrorMessage); return; } + const { metadata: { channelToken, orderCode, orderId } = {} } = paymentIntent; - const ctx = await this.createContext(channelToken, request); - const order = await this.orderService.findOneByCode(ctx, orderCode); - if (!order) { - throw Error(`Unable to find order ${orderCode}, unable to settle payment ${paymentIntent.id}!`); - } - try { - // Throws an error if the signature is invalid - await this.stripeService.constructEventFromPayload(ctx, order, request.rawBody, signature); - } catch (e: any) { - Logger.error(`${signatureErrorMessage} ${signature}: ${(e as Error)?.message}`, loggerCtx); - response.status(HttpStatus.BAD_REQUEST).send(signatureErrorMessage); - return; - } - if (event.type === 'payment_intent.payment_failed') { - const message = paymentIntent.last_payment_error?.message ?? 'unknown error'; - Logger.warn(`Payment for order ${orderCode} failed: ${message}`, loggerCtx); - response.status(HttpStatus.OK).send('Ok'); - return; - } - if (event.type !== 'payment_intent.succeeded') { - // This should never happen as the webhook is configured to receive - // payment_intent.succeeded and payment_intent.payment_failed events only - Logger.info(`Received ${event.type} status update for order ${orderCode}`, loggerCtx); - return; - } - if (order.state !== 'ArrangingPayment') { - const transitionToStateResult = await this.orderService.transitionToState( - ctx, - orderId, - 'ArrangingPayment', - ); - - if (transitionToStateResult instanceof OrderStateTransitionError) { - Logger.error( - `Error transitioning order ${orderCode} to ArrangingPayment state: ${transitionToStateResult.message}`, - loggerCtx, - ); + const outerCtx = await this.createContext(channelToken, request); + + await this.connection.withTransaction(outerCtx, async (ctx) => { + const order = await this.orderService.findOneByCode(ctx, orderCode); + + if (!order) { + throw new Error(`Unable to find order ${orderCode}, unable to settle payment ${paymentIntent.id}!`); + } + + try { + // Throws an error if the signature is invalid + await this.stripeService.constructEventFromPayload(ctx, order, request.rawBody, signature); + } catch (e: any) { + Logger.error(`${signatureErrorMessage} ${signature}: ${(e as Error)?.message}`, loggerCtx); + response.status(HttpStatus.BAD_REQUEST).send(signatureErrorMessage); return; } - } - const paymentMethod = await this.getPaymentMethod(ctx); + if (event.type === 'payment_intent.payment_failed') { + const message = paymentIntent.last_payment_error?.message ?? 'unknown error'; + Logger.warn(`Payment for order ${orderCode} failed: ${message}`, loggerCtx); + response.status(HttpStatus.OK).send('Ok'); + return; + } - const addPaymentToOrderResult = await this.orderService.addPaymentToOrder(ctx, orderId, { - method: paymentMethod.code, - metadata: { - paymentIntentAmountReceived: paymentIntent.amount_received, - paymentIntentId: paymentIntent.id, - }, - }); + if (event.type !== 'payment_intent.succeeded') { + // This should never happen as the webhook is configured to receive + // payment_intent.succeeded and payment_intent.payment_failed events only + Logger.info(`Received ${event.type} status update for order ${orderCode}`, loggerCtx); + return; + } - if (!(addPaymentToOrderResult instanceof Order)) { - Logger.error( - `Error adding payment to order ${orderCode}: ${addPaymentToOrderResult.message}`, - loggerCtx, - ); - return; - } + if (order.state !== 'ArrangingPayment') { + const transitionToStateResult = await this.orderService.transitionToState( + ctx, + orderId, + 'ArrangingPayment', + ); + + if (transitionToStateResult instanceof OrderStateTransitionError) { + Logger.error(`Error transitioning order ${orderCode} to ArrangingPayment state: ${transitionToStateResult.message}`, loggerCtx); + return; + } + } + + const paymentMethod = await this.getPaymentMethod(ctx); + + const addPaymentToOrderResult = await this.orderService.addPaymentToOrder(ctx, orderId, { + method: paymentMethod.code, + metadata: { + paymentIntentAmountReceived: paymentIntent.amount_received, + paymentIntentId: paymentIntent.id, + }, + }); + + if (!(addPaymentToOrderResult instanceof Order)) { + Logger.error(`Error adding payment to order ${orderCode}: ${addPaymentToOrderResult.message}`, loggerCtx); + } + }); Logger.info(`Stripe payment intent id ${paymentIntent.id} added to order ${orderCode}`, loggerCtx); response.status(HttpStatus.OK).send('Ok'); @@ -127,8 +123,9 @@ export class StripeController { const method = (await this.paymentMethodService.findAll(ctx)).items.find( m => m.handler.code === stripePaymentMethodHandler.code, ); + if (!method) { - throw new InternalServerError(`[${loggerCtx}] Could not find Stripe PaymentMethod`); + throw new InternalServerError(`[${loggerCtx}] Could not find Stripe PaymentMethod`); } return method; diff --git a/packages/payments-plugin/src/stripe/stripe.resolver.ts b/packages/payments-plugin/src/stripe/stripe.resolver.ts index efbec0dd78..6addf8cdad 100644 --- a/packages/payments-plugin/src/stripe/stripe.resolver.ts +++ b/packages/payments-plugin/src/stripe/stripe.resolver.ts @@ -1,19 +1,14 @@ import { Mutation, Resolver } from '@nestjs/graphql'; -import { - ActiveOrderService, - Allow, - Ctx, - Permission, - RequestContext, - UnauthorizedError, - UserInputError, -} from '@vendure/core'; +import { ActiveOrderService, Allow, Ctx, Permission, RequestContext, UnauthorizedError, UserInputError } from '@vendure/core'; import { StripeService } from './stripe.service'; @Resolver() export class StripeResolver { - constructor(private stripeService: StripeService, private activeOrderService: ActiveOrderService) {} + constructor( + private stripeService: StripeService, + private activeOrderService: ActiveOrderService, + ) {} @Mutation() @Allow(Permission.Owner) diff --git a/packages/payments-plugin/src/stripe/stripe.service.ts b/packages/payments-plugin/src/stripe/stripe.service.ts index 7c3342a076..2a094da02d 100644 --- a/packages/payments-plugin/src/stripe/stripe.service.ts +++ b/packages/payments-plugin/src/stripe/stripe.service.ts @@ -1,18 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { ConfigArg } from '@vendure/common/lib/generated-types'; -import { - Ctx, - Customer, - Injector, - Logger, - Order, - Payment, - PaymentMethodService, - RequestContext, - TransactionalConnection, - UserInputError, -} from '@vendure/core'; +import { Customer, Injector, Logger, Order, Payment, PaymentMethodService, RequestContext, TransactionalConnection, UserInputError } from '@vendure/core'; import Stripe from 'stripe'; import { loggerCtx, STRIPE_PLUGIN_OPTIONS } from './constants'; @@ -25,9 +14,9 @@ import { StripePluginOptions } from './types'; @Injectable() export class StripeService { constructor( + @Inject(STRIPE_PLUGIN_OPTIONS) private options: StripePluginOptions, private connection: TransactionalConnection, private paymentMethodService: PaymentMethodService, - @Inject(STRIPE_PLUGIN_OPTIONS) private options: StripePluginOptions, private moduleRef: ModuleRef, ) {} @@ -64,10 +53,7 @@ export class StripeService { if (!client_secret) { // This should never happen - Logger.warn( - `Payment intent creation for order ${order.code} did not return client secret`, - loggerCtx, - ); + Logger.warn(`Payment intent creation for order ${order.code} did not return client secret`, loggerCtx); throw Error('Failed to create payment intent'); } diff --git a/packages/payments-plugin/src/stripe/types.ts b/packages/payments-plugin/src/stripe/types.ts index 998783f243..7532c2bd1d 100644 --- a/packages/payments-plugin/src/stripe/types.ts +++ b/packages/payments-plugin/src/stripe/types.ts @@ -1,7 +1,7 @@ -import { Injector, Order, RequestContext } from '@vendure/core'; import '@vendure/core/dist/entity/custom-entity-fields'; -import { Request } from 'express'; -import Stripe from 'stripe'; +import type { Injector, Order, RequestContext } from '@vendure/core'; +import type { Request } from 'express'; +import type Stripe from 'stripe'; // Note: deep import is necessary here because CustomCustomerFields is also extended in the Braintree // plugin. Reference: https://github.com/microsoft/TypeScript/issues/46617 From 0ddee961d5385c5feb381b9fbe73a19433263bc8 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 18 Sep 2023 15:56:42 +0200 Subject: [PATCH 02/22] test(admin-ui): Fix failing unit test --- .../src/common/utilities/create-updated-translatable.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/admin-ui/src/lib/core/src/common/utilities/create-updated-translatable.spec.ts b/packages/admin-ui/src/lib/core/src/common/utilities/create-updated-translatable.spec.ts index 4313fc4986..ffba1d7465 100644 --- a/packages/admin-ui/src/lib/core/src/common/utilities/create-updated-translatable.spec.ts +++ b/packages/admin-ui/src/lib/core/src/common/utilities/create-updated-translatable.spec.ts @@ -241,10 +241,10 @@ fdescribe('createUpdatedTranslatable()', () => { languageCode: LanguageCode.en, }); - expect(result.customFields.a).toBe(false); + expect(result.customFields.a).toBe(null); expect(result.customFields.b).toBe(null); expect(result.customFields.c).toBe(null); expect(result.customFields.d).toBe(null); - expect(result.customFields.e).toBe(''); + expect(result.customFields.e).toBe(null); }); }); From 0740c8733458207ab12fbccd05cd225c6bb6cca2 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 21 Sep 2023 17:26:38 +0200 Subject: [PATCH 03/22] fix(core): Prevent negative total from compounded promotions Fixes #2385 --- packages/core/e2e/order-promotion.e2e-spec.ts | 83 ++++++++++++++++++- .../entity/order-line/order-line.entity.ts | 11 ++- 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/packages/core/e2e/order-promotion.e2e-spec.ts b/packages/core/e2e/order-promotion.e2e-spec.ts index 9bad677c16..2eb0f495a7 100644 --- a/packages/core/e2e/order-promotion.e2e-spec.ts +++ b/packages/core/e2e/order-promotion.e2e-spec.ts @@ -1550,8 +1550,8 @@ describe('Promotions applied to Orders', () => { >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE }); orderResultGuard.assertSuccess(applyCouponCode); - expect(applyCouponCode!.totalWithTax).toBe(0); - expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]); + expect(applyCouponCode.totalWithTax).toBe(0); + expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]); await shopClient.query( SET_CUSTOMER, @@ -1565,8 +1565,8 @@ describe('Promotions applied to Orders', () => { ); const { activeOrder } = await shopClient.query(GET_ACTIVE_ORDER); - expect(activeOrder!.couponCodes).toEqual([TEST_COUPON_CODE]); - expect(applyCouponCode!.totalWithTax).toBe(0); + expect(activeOrder.couponCodes).toEqual([TEST_COUPON_CODE]); + expect(applyCouponCode.totalWithTax).toBe(0); }); }); @@ -1755,6 +1755,81 @@ describe('Promotions applied to Orders', () => { expect(applyCouponCode.totalWithTax).toBe(96); }); + // https://github.com/vendure-ecommerce/vendure/issues/2385 + it('prevents negative line price', async () => { + await shopClient.asAnonymousUser(); + const item1000 = getVariantBySlug('item-1000')!; + const couponCode1 = '100%_off'; + const couponCode2 = '100%_off'; + await createPromotion({ + enabled: true, + name: '100% discount ', + couponCode: couponCode1, + conditions: [], + actions: [ + { + code: productsPercentageDiscount.code, + arguments: [ + { name: 'discount', value: '100' }, + { + name: 'productVariantIds', + value: `["${item1000.id}"]`, + }, + ], + }, + ], + }); + await createPromotion({ + enabled: true, + name: '20% discount ', + couponCode: couponCode2, + conditions: [], + actions: [ + { + code: productsPercentageDiscount.code, + arguments: [ + { name: 'discount', value: '20' }, + { + name: 'productVariantIds', + value: `["${item1000.id}"]`, + }, + ], + }, + ], + }); + + await shopClient.query< + CodegenShop.ApplyCouponCodeMutation, + CodegenShop.ApplyCouponCodeMutationVariables + >(APPLY_COUPON_CODE, { couponCode: couponCode1 }); + + await shopClient.query< + CodegenShop.AddItemToOrderMutation, + CodegenShop.AddItemToOrderMutationVariables + >(ADD_ITEM_TO_ORDER, { + productVariantId: item1000.id, + quantity: 1, + }); + + const { activeOrder: check1 } = await shopClient.query( + GET_ACTIVE_ORDER, + ); + + expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0); + expect(check1!.totalWithTax).toBe(0); + + await shopClient.query< + CodegenShop.ApplyCouponCodeMutation, + CodegenShop.ApplyCouponCodeMutationVariables + >(APPLY_COUPON_CODE, { couponCode: couponCode2 }); + + const { activeOrder: check2 } = await shopClient.query( + GET_ACTIVE_ORDER, + ); + expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0); + expect(check2!.totalWithTax).toBe(0); + }); + async function getProducts() { const result = await adminClient.query( GET_PRODUCTS_WITH_VARIANT_PRICES, diff --git a/packages/core/src/entity/order-line/order-line.entity.ts b/packages/core/src/entity/order-line/order-line.entity.ts index 3f424cce84..198d75df8e 100644 --- a/packages/core/src/entity/order-line/order-line.entity.ts +++ b/packages/core/src/entity/order-line/order-line.entity.ts @@ -330,7 +330,16 @@ export class OrderLine extends VendureEntity implements HasCustomFields { } addAdjustment(adjustment: Adjustment) { - this.adjustments = this.adjustments.concat(adjustment); + // We should not allow adding adjustments which would + // result in a negative unit price + const maxDiscount = this.proratedLinePrice * -1; + const limitedAdjustment: Adjustment = { + ...adjustment, + amount: Math.max(maxDiscount, adjustment.amount), + }; + if (limitedAdjustment.amount !== 0) { + this.adjustments = this.adjustments.concat(limitedAdjustment); + } } /** From 6561bb7b57c1c4f4969f843bd5d6180855236849 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 22 Sep 2023 11:45:11 +0200 Subject: [PATCH 04/22] feat(create): Allow selection of package manager Currently only Yarn and npm are supported, but this sets up future support for other package managers & runtimes such as pnpm & Bun --- packages/create/src/create-vendure-app.ts | 40 +++++++++++++------- packages/create/src/gather-user-responses.ts | 23 +++++++---- packages/create/src/helpers.ts | 14 ++++++- packages/create/src/types.ts | 2 + 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/packages/create/src/create-vendure-app.ts b/packages/create/src/create-vendure-app.ts index e002b6dd46..6279d6ed6d 100644 --- a/packages/create/src/create-vendure-app.ts +++ b/packages/create/src/create-vendure-app.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { intro, note, outro, spinner } from '@clack/prompts'; +import { intro, note, outro, select, spinner } from '@clack/prompts'; import { program } from 'commander'; import detectPort from 'detect-port'; import fs from 'fs-extra'; @@ -8,7 +8,7 @@ import path from 'path'; import pc from 'picocolors'; import { REQUIRED_NODE_VERSION, SERVER_PORT } from './constants'; -import { gatherCiUserResponses, gatherUserResponses } from './gather-user-responses'; +import { checkCancel, gatherCiUserResponses, gatherUserResponses } from './gather-user-responses'; import { checkDbConnection, checkNodeVersion, @@ -18,9 +18,9 @@ import { isSafeToCreateProjectIn, isServerPortInUse, scaffoldAlreadyExists, - shouldUseYarn, + yarnIsAvailable, } from './helpers'; -import { CliLogLevel } from './types'; +import { CliLogLevel, DbType, PackageManager } from './types'; // eslint-disable-next-line @typescript-eslint/no-var-requires const packageJson = require('../package.json'); @@ -75,7 +75,21 @@ export async function createVendureApp( const root = path.resolve(name); const appName = path.basename(root); const scaffoldExists = scaffoldAlreadyExists(root, name); - const useYarn = useNpm ? false : shouldUseYarn(); + + const yarnAvailable = yarnIsAvailable(); + let packageManager: PackageManager = 'npm'; + if (yarnAvailable && !useNpm) { + packageManager = (await select({ + message: 'Which package manager should be used?', + options: [ + { label: 'npm', value: 'npm' }, + { label: 'yarn', value: 'yarn' }, + ], + initialValue: 'yarn' as PackageManager, + })) as PackageManager; + checkCancel(packageManager); + } + if (scaffoldExists) { console.log( pc.yellow( @@ -97,11 +111,11 @@ export async function createVendureApp( dockerComposeSource, populateProducts, } = isCi - ? await gatherCiUserResponses(root, useYarn) - : await gatherUserResponses(root, scaffoldExists, useYarn); + ? await gatherCiUserResponses(root, packageManager) + : await gatherUserResponses(root, scaffoldExists, packageManager); const originalDirectory = process.cwd(); process.chdir(root); - if (!useYarn && !checkThatNpmCanReadCwd()) { + if (packageManager !== 'npm' && !checkThatNpmCanReadCwd()) { process.exit(1); } @@ -112,11 +126,11 @@ export async function createVendureApp( scripts: { 'dev:server': 'ts-node ./src/index.ts', 'dev:worker': 'ts-node ./src/index-worker.ts', - dev: useYarn ? 'concurrently yarn:dev:*' : 'concurrently npm:dev:*', + dev: packageManager === 'yarn' ? 'concurrently yarn:dev:*' : 'concurrently npm:dev:*', build: 'tsc', 'start:server': 'node ./dist/index.js', 'start:worker': 'node ./dist/index-worker.js', - start: useYarn ? 'concurrently yarn:start:*' : 'concurrently npm:start:*', + start: packageManager === 'yarn' ? 'concurrently yarn:start:*' : 'concurrently npm:start:*', 'migration:generate': 'ts-node migration generate', 'migration:run': 'ts-node migration run', 'migration:revert': 'ts-node migration revert', @@ -138,7 +152,7 @@ export async function createVendureApp( const installSpinner = spinner(); installSpinner.start(`Installing ${dependencies[0]} + ${dependencies.length - 1} more dependencies`); try { - await installPackages(root, useYarn, dependencies, false, logLevel, isCi); + await installPackages(root, packageManager === 'yarn', dependencies, false, logLevel, isCi); } catch (e) { outro(pc.red(`Failed to install dependencies. Please try again.`)); process.exit(1); @@ -151,7 +165,7 @@ export async function createVendureApp( `Installing ${devDependencies[0]} + ${devDependencies.length - 1} more dev dependencies`, ); try { - await installPackages(root, useYarn, devDependencies, true, logLevel, isCi); + await installPackages(root, packageManager === 'yarn', devDependencies, true, logLevel, isCi); } catch (e) { outro(pc.red(`Failed to install dev dependencies. Please try again.`)); process.exit(1); @@ -255,7 +269,7 @@ export async function createVendureApp( } populateSpinner.stop(`Server successfully initialized${populateProducts ? ' and populated' : ''}`); - const startCommand = useYarn ? 'yarn dev' : 'npm run dev'; + const startCommand = packageManager === 'yarn' ? 'yarn dev' : 'npm run dev'; const nextSteps = [ `${pc.green('Success!')} Created a new Vendure server at:`, `\n`, diff --git a/packages/create/src/gather-user-responses.ts b/packages/create/src/gather-user-responses.ts index 2afc6fa0c6..0e61b115e9 100644 --- a/packages/create/src/gather-user-responses.ts +++ b/packages/create/src/gather-user-responses.ts @@ -4,7 +4,7 @@ import fs from 'fs-extra'; import Handlebars from 'handlebars'; import path from 'path'; -import { DbType, FileSources, UserResponses } from './types'; +import { DbType, FileSources, PackageManager, UserResponses } from './types'; interface PromptAnswers { dbType: DbType; @@ -27,7 +27,7 @@ interface PromptAnswers { export async function gatherUserResponses( root: string, alreadyRanScaffold: boolean, - useYarn: boolean, + packageManager: PackageManager, ): Promise { const dbType = (await select({ message: 'Which database are you using?', @@ -119,7 +119,7 @@ export async function gatherUserResponses( }; return { - ...(await generateSources(root, answers, useYarn)), + ...(await generateSources(root, answers, packageManager)), dbType, populateProducts: answers.populateProducts as boolean, superadminIdentifier: answers.superadminIdentifier as string, @@ -130,7 +130,10 @@ export async function gatherUserResponses( /** * Returns mock "user response" without prompting, for use in CI */ -export async function gatherCiUserResponses(root: string, useYarn: boolean): Promise { +export async function gatherCiUserResponses( + root: string, + packageManager: PackageManager, +): Promise { const ciAnswers = { dbType: 'sqlite' as const, dbHost: '', @@ -144,7 +147,7 @@ export async function gatherCiUserResponses(root: string, useYarn: boolean): Pro }; return { - ...(await generateSources(root, ciAnswers, useYarn)), + ...(await generateSources(root, ciAnswers, packageManager)), dbType: ciAnswers.dbType, populateProducts: ciAnswers.populateProducts, superadminIdentifier: ciAnswers.superadminIdentifier, @@ -152,7 +155,7 @@ export async function gatherCiUserResponses(root: string, useYarn: boolean): Pro }; } -function checkCancel(value: T | symbol): value is T { +export function checkCancel(value: T | symbol): value is T { if (isCancel(value)) { cancel('Setup cancelled.'); process.exit(0); @@ -163,7 +166,11 @@ function checkCancel(value: T | symbol): value is T { /** * Create the server index, worker and config source code based on the options specified by the CLI prompts. */ -async function generateSources(root: string, answers: PromptAnswers, useYarn: boolean): Promise { +async function generateSources( + root: string, + answers: PromptAnswers, + packageManager: PackageManager, +): Promise { const assetPath = (fileName: string) => path.join(__dirname, '../assets', fileName); /** @@ -177,7 +184,7 @@ async function generateSources(root: string, answers: PromptAnswers, useYarn: bo const templateContext = { ...answers, - useYarn, + useYarn: packageManager === 'yarn', dbType: answers.dbType === 'sqlite' ? 'better-sqlite3' : answers.dbType, name: path.basename(root), isSQLite: answers.dbType === 'sqlite', diff --git a/packages/create/src/helpers.ts b/packages/create/src/helpers.ts index ad64db60e3..e40340c269 100644 --- a/packages/create/src/helpers.ts +++ b/packages/create/src/helpers.ts @@ -102,7 +102,7 @@ export function checkNodeVersion(requiredVersion: string) { } } -export function shouldUseYarn() { +export function yarnIsAvailable() { try { execSync('yarnpkg --version', { stdio: 'ignore' }); return true; @@ -111,6 +111,18 @@ export function shouldUseYarn() { } } +// Bun support should not be exposed yet, see +// https://github.com/oven-sh/bun/issues/4947 +// https://github.com/lovell/sharp/issues/3511 +export function bunIsAvailable() { + try { + execSync('bun --version', { stdio: 'ignore' }); + return true; + } catch (e: any) { + return false; + } +} + export function checkThatNpmCanReadCwd() { const cwd = process.cwd(); let childOutput = null; diff --git a/packages/create/src/types.ts b/packages/create/src/types.ts index 7095cf14a4..e9af5d91bd 100644 --- a/packages/create/src/types.ts +++ b/packages/create/src/types.ts @@ -19,4 +19,6 @@ export interface UserResponses extends FileSources { superadminPassword: string; } +export type PackageManager = 'npm' | 'yarn'; + export type CliLogLevel = 'silent' | 'info' | 'verbose'; From be37cacbc501cd544380ec143eaff38dcd71f197 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 22 Sep 2023 11:48:40 +0200 Subject: [PATCH 05/22] docs: Update installation guide with just a single install command Makes things simpler --- .../getting-started/installation/index.md | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/docs/docs/guides/getting-started/installation/index.md b/docs/docs/guides/getting-started/installation/index.md index b1b26d97d2..84fff9f498 100644 --- a/docs/docs/guides/getting-started/installation/index.md +++ b/docs/docs/guides/getting-started/installation/index.md @@ -18,34 +18,10 @@ The recommended way to get started with Vendure is by using the [@vendure/create ### 1. Run the command - - - ``` npx @vendure/create my-shop ``` - - - -``` -npm init @vendure my-shop -``` - - - - -``` -yarn create @vendure my-shop -``` - - - - -:::note -By default, the `@vendure/create` tool will use [Yarn](https://yarnpkg.com/) to manage your dependencies if you have it installed. If you want to force it to use npm, use the `--use-npm` flag. -::: - ### 2. Select a database Vendure supports a number of different databases. The `@vendure/create` tool will prompt you to select one. From 4cf1826f4a53814e1c5a55b7e6ead225bf718105 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 22 Sep 2023 15:03:38 +0200 Subject: [PATCH 06/22] feat(admin-ui): Implement values pagination for Facet detail view Closes #1257 --- .../facet-detail/facet-detail.component.html | 163 ++++++++++-------- .../facet-detail/facet-detail.component.scss | 10 ++ .../facet-detail/facet-detail.component.ts | 145 ++++++++++------ 3 files changed, 200 insertions(+), 118 deletions(-) diff --git a/packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.html b/packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.html index ffebe8aa56..e4e08f51e0 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.html +++ b/packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.html @@ -100,75 +100,102 @@ [title]="'catalog.facet-values' | translate" [paddingX]="false" > - - - - - - - - - - - - - - - - - - - + + +
{{ 'common.name' | translate }}{{ 'common.code' | translate }}{{ 'common.custom-fields' | translate }}
- - - - - - - - - - - - + + +
+
+ + +
+
@@ -117,7 +137,7 @@ - @@ -129,7 +149,7 @@