-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: Upgrade to V2 #6
Changes from 7 commits
80c3cc6
cad2a0a
855dfd4
a217fa8
45b779e
961e253
3e9cd0d
0d77cf0
a4539c9
839d6ab
eca6683
0245e2f
f9c9946
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
__data__ | ||
dist | ||
node_modules | ||
node_modules | ||
__admin-ui |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,25 +19,25 @@ | |
"build": "rimraf dist && tsc && copyfiles -u 1 'src/ui/**/*' dist/src/ && copyfiles -u 1 'generated/*' dist/src/generated", | ||
"generate": "graphql-codegen", | ||
"start": "ts-node test/dev-server.ts", | ||
"test": "jest --preset=\"ts-jest\" --forceExit" | ||
"test": "vitest run" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can run |
||
}, | ||
"devDependencies": { | ||
"@graphql-codegen/cli": "^2.4.0", | ||
"@graphql-codegen/typescript": "^2.4.8", | ||
"@graphql-codegen/typescript-operations": "^2.3.5", | ||
"@types/jest": "^29.5.1", | ||
"@vendure/admin-ui-plugin": "1.9.3", | ||
"@vendure/asset-server-plugin": "1.9.3", | ||
"@vendure/core": "1.9.3", | ||
"@vendure/email-plugin": "^1.9.5", | ||
"@vendure/testing": "1.9.3", | ||
"@vendure/ui-devkit": "1.9.3", | ||
"@swc/core": "^1.3.58", | ||
"@vendure/admin-ui-plugin": "2.0.0-beta.2", | ||
"@vendure/asset-server-plugin": "2.0.0-beta.2", | ||
"@vendure/core": "2.0.0-beta.2", | ||
"@vendure/email-plugin": "2.0.0-beta.2", | ||
"@vendure/testing": "2.0.0-beta.2", | ||
"@vendure/ui-devkit": "2.0.0-beta.2", | ||
"copyfiles": "^2.4.1", | ||
"jest": "^29.5.0", | ||
"rimraf": "^4.1.2", | ||
"ts-jest": "^29.1.0", | ||
"ts-node": "^10.9.1", | ||
"typescript": "4.3.5" | ||
"typescript": "4.9.5", | ||
"unplugin-swc": "^1.3.2", | ||
"vitest": "^0.31.0" | ||
}, | ||
"dependencies": {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,19 @@ | ||
import { | ||
EntityHydrator, | ||
PluginCommonModule, | ||
ProductVariant, | ||
ProductVariantService, | ||
RequestContextService, | ||
TransactionalConnection, | ||
VendurePlugin, | ||
VendurePlugin | ||
} from '@vendure/core'; | ||
import { PLUGIN_INIT_OPTIONS } from './constants'; | ||
import { EmailEventListener } from '@vendure/email-plugin'; | ||
import { AdminUiExtension } from '@vendure/ui-devkit/compiler'; | ||
import path from 'path'; | ||
import { adminApiExtensions, shopApiExtensions } from './api/api-extensions'; | ||
import { BackInStockAdminResolver } from './api/back-in-stock-admin.resolver'; | ||
import { BackInStockResolver } from './api/back-in-stock.resolver'; | ||
import { UnionResolver } from './api/union.resolver'; | ||
import { PLUGIN_INIT_OPTIONS } from './constants'; | ||
import { BackInStock } from './entity/back-in-stock.entity'; | ||
import { BackInStockService } from './service/back-in-stock.service'; | ||
import { EmailEventListener } from '@vendure/email-plugin'; | ||
import { BackInStockEvent } from './events/back-in-stock.event'; | ||
import { BackInStockSubscriptionStatus } from './types'; | ||
import { UnionResolver } from './api/union.resolver'; | ||
import { BackInStockAdminResolver } from './api/back-in-stock-admin.resolver'; | ||
import { AdminUiExtension } from '@vendure/ui-devkit/compiler'; | ||
import path from 'path'; | ||
import { EntitySubscriberInterface, EventSubscriber, UpdateEvent } from 'typeorm'; | ||
import { Injectable } from '@nestjs/common'; | ||
import { getApiType } from '@vendure/core/dist/api/common/get-api-type'; | ||
import { SortOrder } from './generated/graphql-shop-api-types'; | ||
import { BackInStockService } from './service/back-in-stock.service'; | ||
|
||
export interface BackInStockOptions { | ||
enableEmail: boolean; | ||
|
@@ -40,66 +31,6 @@ export interface BackInStockOptions { | |
allowSubscriptionWithoutSession?: boolean; | ||
} | ||
|
||
/** | ||
* @description | ||
* Subscribes to {@link ProductVariant} inventory changes | ||
* and FIFO updates BackInStock {@link BackInStock} to be notified | ||
* to the amount of saleable stock with plugin init option | ||
* limitEmailToStock = true or false to notify all subscribers | ||
* | ||
*/ | ||
@Injectable() | ||
@EventSubscriber() | ||
export class ProductVariantSubscriber implements EntitySubscriberInterface<ProductVariant> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
constructor( | ||
private connection: TransactionalConnection, | ||
private backInStockService: BackInStockService, | ||
private productVariantService: ProductVariantService, | ||
private requestContextService: RequestContextService, | ||
) { | ||
this.connection.rawConnection.subscribers.push(this); | ||
} | ||
listenTo() { | ||
return ProductVariant; | ||
} | ||
|
||
// set subscriptions to be notified only on replenishment event | ||
async afterUpdate(event: UpdateEvent<ProductVariant>) { | ||
if ( | ||
event.entity?.stockOnHand > event.databaseEntity?.stockOnHand && | ||
BackInStockPlugin.options.enableEmail | ||
) { | ||
const ctx = await this.requestContextService.create({ apiType: getApiType() }); | ||
const productVariant = await this.productVariantService.findOne(ctx, event.entity?.id); | ||
//! calculate saleable manually as this context is not aware of the current db transaction | ||
const saleableStock = | ||
event.entity?.stockOnHand - | ||
productVariant!.stockAllocated - | ||
productVariant!.outOfStockThreshold; | ||
|
||
const backInStockSubscriptions = await this.backInStockService.findActiveForProductVariant( | ||
ctx, | ||
productVariant!.id, | ||
{ | ||
take: BackInStockPlugin.options.limitEmailToStock ? saleableStock : undefined, | ||
sort: { | ||
createdAt: SortOrder.Asc, | ||
}, | ||
}, | ||
); | ||
|
||
if (saleableStock >= 1 && backInStockSubscriptions.totalItems >= 1) { | ||
for (const subscription of backInStockSubscriptions.items) { | ||
this.backInStockService.update(ctx, { | ||
id: subscription.id, | ||
status: BackInStockSubscriptionStatus.Notified, | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
@VendurePlugin({ | ||
imports: [PluginCommonModule], | ||
entities: [BackInStock], | ||
|
@@ -109,7 +40,6 @@ export class ProductVariantSubscriber implements EntitySubscriberInterface<Produ | |
useFactory: () => BackInStockPlugin.options, | ||
}, | ||
BackInStockService, | ||
ProductVariantSubscriber, | ||
], | ||
shopApiExtensions: { | ||
schema: shopApiExtensions, | ||
|
@@ -124,13 +54,14 @@ export class BackInStockPlugin { | |
static options: BackInStockOptions = { | ||
enableEmail: true, | ||
limitEmailToStock: true, | ||
allowSubscriptionWithoutSession: true, | ||
}; | ||
|
||
static init(options: BackInStockOptions): typeof BackInStockPlugin { | ||
if (options.allowSubscriptionWithoutSession === undefined) { | ||
options.allowSubscriptionWithoutSession = true; | ||
} | ||
this.options = options; | ||
this.options = { | ||
...this.options, | ||
...options | ||
}; // Only override whats passed in, leave the other defaults | ||
return BackInStockPlugin; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,25 +18,32 @@ import { | |
InternalServerError, | ||
EventBus, | ||
TranslatorService, | ||
StockMovementEvent, | ||
StockLevelService, | ||
Logger, | ||
EntityHydrator, | ||
} from '@vendure/core'; | ||
import { BackInStock } from '../entity/back-in-stock.entity'; | ||
import { BackInStockSubscriptionStatus } from '../types'; | ||
import { | ||
CreateBackInStockInput, | ||
CreateBackInStockSubscriptionResult, | ||
ErrorCode, | ||
SortOrder, | ||
UpdateBackInStockInput, | ||
} from '../generated/graphql-shop-api-types'; | ||
import { BackInStockPlugin } from '../back-in-stock.plugin'; | ||
} from '../ui/generated/graphql-shop-api-types'; | ||
import { BackInStockOptions } from '../back-in-stock.plugin'; | ||
import { BackInStockEvent } from '../events/back-in-stock.event'; | ||
import { OnApplicationBootstrap, Inject } from '@nestjs/common'; | ||
import { PLUGIN_INIT_OPTIONS, loggerCtx } from '../constants'; | ||
|
||
/** | ||
* @description | ||
* Contains methods relating to {@link BackInStock} entities. | ||
* | ||
*/ | ||
@Injectable() | ||
export class BackInStockService { | ||
export class BackInStockService implements OnApplicationBootstrap { | ||
private readonly relations = ['productVariant', 'channel', 'customer']; | ||
|
||
constructor( | ||
|
@@ -47,11 +54,54 @@ export class BackInStockService { | |
private productVariantService: ProductVariantService, | ||
private translatorService: TranslatorService, | ||
private eventBus: EventBus, | ||
) {} | ||
@Inject(PLUGIN_INIT_OPTIONS) private options: BackInStockOptions | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. - BackInStockPlugin.options
+ @Inject(PLUGIN_INIT_OPTIONS) private options: BackInStockOptions
|
||
) { } | ||
|
||
async findOne(ctx: RequestContext, id: ID): Promise<BackInStock | undefined> { | ||
return this.connection.getRepository(ctx, BackInStock).findOne(id, { | ||
relations: this.relations, | ||
|
||
onApplicationBootstrap() { | ||
// Listen for stock movements and update subscriptions | ||
this.eventBus.ofType(StockMovementEvent).subscribe(async event => { | ||
// Refetch variants, because variants in event does not have all properties fetched from DB | ||
const variants = await this.productVariantService.findByIds(event.ctx, event.stockMovements.map(sm => sm.productVariant.id)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
// Check new stockLevel of each variant in the event | ||
Promise.all(variants.map(async (productVariant) => { | ||
const saleableStock = await this.productVariantService.getSaleableStockLevel(event.ctx, productVariant); | ||
if (isNaN(saleableStock)) { | ||
// This can happen when an event is fired during bootstrap, | ||
// so Vendure can't yet resolve saleable stock for some reason | ||
return; | ||
} | ||
if (saleableStock < 1) { | ||
return; // Still not in stock | ||
} | ||
const backInStockSubscriptions = await this.findActiveForProductVariant( | ||
event.ctx, | ||
productVariant!.id, | ||
{ | ||
take: this.options.limitEmailToStock ? saleableStock : undefined, | ||
sort: { | ||
createdAt: SortOrder.Asc, | ||
}, | ||
}, | ||
); | ||
if (backInStockSubscriptions.totalItems < 1) { | ||
return; // No subscriptions to notify | ||
} | ||
await Promise.all(backInStockSubscriptions.items.map(async subscription => | ||
this.update(event.ctx, { | ||
id: subscription.id, | ||
status: BackInStockSubscriptionStatus.Notified, | ||
}))); | ||
})); | ||
}); | ||
} | ||
|
||
async findOne(ctx: RequestContext, id: ID): Promise<BackInStock | null> { | ||
return this.connection.getRepository(ctx, BackInStock).findOne({ | ||
where: { | ||
id | ||
}, | ||
relations: { productVariant: true, channel: true, customer: true }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New way of fetching relations in TypeOrm 0.3.0.
|
||
}); | ||
} | ||
|
||
|
@@ -80,16 +130,14 @@ export class BackInStockService { | |
options?: ListQueryOptions<BackInStock>, | ||
relations?: RelationPaths<Channel> | RelationPaths<Customer>, | ||
): Promise<PaginatedList<BackInStock>> { | ||
const productVariant = await this.productVariantService.findOne(ctx, productVariantId); | ||
|
||
return this.listQueryBuilder | ||
.build(BackInStock, options, { | ||
relations: relations || this.relations, | ||
ctx, | ||
where: { | ||
productVariant, | ||
status: BackInStockSubscriptionStatus.Created, | ||
}, | ||
// where: { | ||
// productVariant: { id: productVariantId }, | ||
// status: BackInStockSubscriptionStatus.Created, | ||
// }, | ||
}) | ||
.getManyAndCount() | ||
.then(async ([items, totalItems]) => { | ||
|
@@ -104,7 +152,7 @@ export class BackInStockService { | |
ctx: RequestContext, | ||
productVariantId: ID, | ||
email: string, | ||
): Promise<BackInStock | undefined> { | ||
): Promise<BackInStock | null> { | ||
const { channelId } = ctx; | ||
const status = BackInStockSubscriptionStatus.Created; | ||
const queryBuilder = this.connection | ||
|
@@ -140,6 +188,13 @@ export class BackInStockService { | |
}; | ||
} | ||
|
||
if (!productVariant) { | ||
return { | ||
errorCode: ErrorCode.UnknownError, | ||
message: `No variant found with ID ${input.productVariantId}`, | ||
}; | ||
} | ||
|
||
const existingSubscription = await this.findActiveForProductVariantWithCustomer( | ||
ctx, | ||
productVariantId, | ||
|
@@ -169,14 +224,14 @@ export class BackInStockService { | |
if (!subscription) { | ||
throw new InternalServerError('Subscription not found'); | ||
} | ||
|
||
const updatedSubscription = patchEntity(subscription, input); | ||
if (input.status === 'Notified') { | ||
if (BackInStockPlugin.options.enableEmail) { | ||
if (this.options.enableEmail) { | ||
const translatedVariant = this.translatorService.translate(subscription.productVariant, ctx); | ||
this.eventBus.publish( | ||
new BackInStockEvent(ctx, subscription, translatedVariant, 'updated', subscription.email), | ||
); | ||
Logger.info(`Publish "BackInStockEvent" for variant ${translatedVariant.sku} to notify "${subscription.email}"`, loggerCtx); | ||
} else { | ||
throw new InternalServerError('Email notification disabled'); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Place generated inside
ui
dir, because Admin UI Angular compilation can't access anything outside theui
directory