vendure-ecommerce / vendure Goto Github PK
View Code? Open in Web Editor NEWA headless GraphQL commerce platform for the modern web
Home Page: https://www.vendure.io
License: MIT License
A headless GraphQL commerce platform for the modern web
Home Page: https://www.vendure.io
License: MIT License
(Relates to #26)
The price of a ProductVariant is stored as an integer representing the price of the item in cents (or pence etc) without any taxes applied.
When putting together an order, the final price of the OrderItem is therefore subject to:
Furthermore, the overall Order price is the aggregate of each OrderItem as well as:
The price modifications listed above are known as Adjustments.
The basic idea is that each AdjustmentSource would have a function which if called for each potential target (e.g. each OrderItem in an Order) and this function should return true if an Adjustment should be applied or false if not.
The hard part will be figuring out how to allow the administrator to write this function. We don't want arbitrary JavaScript to be written and executed. A couple of alternatives are:
Research is needed to figure out the real costs & benefits of each approach, including:
Currently the Vendure server returns various error messages when encountering exceptions which have a i18n message but no specific status code or error code.
For a better developer experience for those building front-ends for the API, we should have a system of language-independent error codes and also be able to explicitly set the status code when throwing an I18nError.
For example, there are places in resolvers where we need to throw a "Forbidden" error, but there is currently no way to set the status of the response to 403.
Also, status codes allow the UI developer to implement conditional logic based on a well-defined set of possible error codes that may be returned from a given query/mutation.
These error codes would look like ERR_SHORT_DESCRIPTION
, e.g.:
ERR_FORBIDDEN
ERR_ENTITY_DOES_NOT_EXIST
ERR_INVALID_SORT_FIELD
ERR_CANNOT_TRANSITION
This would also be a good opportunity to think about how to make the I18n (which is currently only used for error messages) statically typed so we know when a string has no translation.
Shipping represents the price of sending the goods in an Order to the delivery address.
A typical ecommerce store may use several shipping providers (e.g. Royal Mail, DPD, FedEx, TNT). Each provider has particular properties which vary:
Therefore it makes sense to create a new entity, ShippingMethod
, which encode the above information. The ShippingMethod would fulfill 2 primary functions:
Different shipping providers use differing pricing structures. Some examples:
Here is a real example from a UK-based ecommerce store.
In this example, the Zone comprises the country "United Kindom (GB)" only. However, there are variations depending on postcode, since some addresses on islands and the Scottish highlands are subject to extra charges by Royal Mail. So a given UK address can be considered either "Mainland", "Highlands & Islands" or "Channel Islands" depending on the post code. Also, the package is "small" if its total weight is under 1500g AND its volumetric size is less than 15" x 22"
Postcode zone & size | Under Β£40 | Over Β£40 | Over Β£100 |
---|---|---|---|
Mainland small & large | 4.95 | 0.00 | 0.00 |
Highlands & Islands small | 3.95 | 0.00 | 0.00 |
Highlands & Islands large | 3.95 | 2.40 | 0.00 |
Channel Islands small | 3.95 | 0.00 | 0.00 |
Channel Islands large | 12.00 | 8.00 | 8.00 |
These are also actual shipping rules from a UK shop relating to shipping to Europe:
The main restriction here is that certain products contain solvents which cannot be sent via air, so this affects the available ShippingMethods. All the rules are quite complex so here is a non-exhaustive list of rules to illustrate the complexity of real-world shipping calculations:
As the 2 examples above illustrate, real-world shipping can be pretty complex and it would be nigh on impossible to model a general solution which could account for all of the above rules for most businesses.
Therefore, a useful and flexible design would be to use the idea of a ShippingCalculator
for each ShippingMethod. A ShippingCalculator would be a function (or method of a class implementing an interface) which takes the Order and returns a price.
This would allow an arbitrary level of complexity to be expressed according to the business rules of the store. For example, the "no solvents" rule can be implemented as a custom field on the ProductVariant. This would be more flexible than an idea of "shipping categories" since it can take in many dimensions at once.
For example: it would allow prices to be specified in an excel spreadsheet (a common way to enumerate a matrix of weight/price values) which would then be read by the ShippingCalculator and used to return the corresponding price.
Likewise, the "determine whether a ShippingMethod applies" function could also be a custom function, ShippingEligibilityChecker
I need to do some research on the best way to store and represent date types. Some questions:
How should the admin ui be deployed?
Initial thought is to bundle it with the server npm package, so that a single install can get everything up an running. However, in real production apps, it is possible that the admin ui is run on a separate server than the API, so it should also be available stand-alone somehow.
For the initial release, it would probably be sufficient to bundle it with the server and serve it up on a pre-defined endpoint such as http://localhost:3000/admin
(the path could be a configurable property of the VendureConfig).
Some points of note if we want to support digital goods:
Customers in the EU must pay VAT on digital goods at the rate applicable in their own country regardless of where the seller is located. Make sure that your tax settings are correct to sell digital products to customers in the EU. source
I read this article: https://refactoringui.com/previews/building-your-color-palette/ and now I want to use this system for the admin ui. We already have something not too far off, but we are missing the explicit shades of each colour.
Currently we are using JWT tokens (& refresh tokens - see #19) for client authentication.
After doing more research (see this thread, or Why JWTs Suck as Session Tokens for example), I am wondering whether we can really simplify the auth implementation.
So since we always perform a DB lookup to get user data for each request, we don't seem to get much benefit from JWT.
.findOne()
on the Session repository with the token. Check the expiry date and if not expired, set expiry date to "now + configured session duration". Get the User from the Session and attach to Req context and forward the request.logOut
mutation which takes a token and deletes (or sets to 'complete') the Session and deletes the cookie.The JWT generated on login is time-limited. Once it expires, the session is no longer valid and the user must login newly.
For a long, continuous user session, this is undesirable. The token should be refreshed during use. Needs research on how this is best achieved.
Secondly, if user is inactive for an expended period (e.g. puts laptop to sleep, opens next day and admin ui is still open in browser), then the next API call will fail with a 403 error. In this case, the app should automatically re-route to the login page, and upon successful login, redirect the user back to the route they were last requesting before the error.
Consider the following:
What do we do at step 5? We have now 2 active Orders - the current anonymous Order with 2 items, and the former Order with 3 items.
There are the following possibilities:
I believe there should be a Strategy approach to this, because in different circumstances, different strategies makes sense. Consider:
The choice of strategy should be configurable, as different retailers may have different ideas about the best way to handle this.
The common createdAt / updatedAt fields are missing from all entity types currently.
Current config allows setting the JWT secret only. We should expose other options as in: https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback to allow more fine-grained control.
Channels represent distinct sales outlets. Many web shops will just have a single, default channel, which is the website. However, there are cases when it would be desirable to be able to define distinct sales channels, such as:
Thus the following aspects could vary from one channel to the next:
Adding Channels would quite significantly increase the complexity of the models. Here is a rough idea for an implementation:
id
and name
. By default, there is a "default" channel which must exist. Any number of additional channels may be created.available
property relative to each Channel. This could be as simple as a one-to-many relationship from Product -> Channels.price
would now be associated with a particular Channel.role
would be relative to a given Channel. Thus you can have Administrators who can view and administrate only selected Channels. This will be tricky to get right.channelId
argument to every query / mutation. Perhaps the active channel id can be stored in the JWT info so that it can be figured out once and then just persists for the session.Currently the channel code is generated in a random way:
vendure/server/src/entity/channel/channel.entity.ts
Lines 9 to 12 in 9aa4aa7
This means that every time the "populate" script is run, a new code is generated and makes the population of data non-deterministic. This then adds complexity to e.g. the test client which needs to obtain the new code each time. Also the storefront app always needs to be updated.
Better would be to allow the code to be supplied in some way, probably in the VendureConfig object.
When creating new entities with the create... mutations, the newly-created entities are not added to the Apollo cache.
Looks like we need to use the update
or updateQueries
property of the mutate()
method: https://www.apollographql.com/docs/angular/features/cache-updates.html#directAccess
Create a basic administrator module for the admin ui, including:
Note - this module should not deal with Customers, even though Customers and Administrators are both a type of User. This is because, to the end-user, they are very different kinds of entities. Plus, only the SuperAdmin would typically be dealing with the Administrator CRUD operations, whereas viewing and editing Customer data would be routine for any kind of administrator.
There is currently no way to set which languages should be available to translate translatable entities into.
Where should the available languages be set?
The permissions handling in the RolesGuard decorator is buggy and needs a comprehensive set of e2e tests which:
First depends on building out the createRole
& assignRoleToUser
mutations, which we can then use in the e2e test script to set up the tests.
Currently we only have CRU but no D operations. I have intentionally left our "delete" operations because I think we need a "soft delete" (i.e. do not physically remove from the DB, but just mark as "deleted").
This is due in an upcoming version of TypeORM (typeorm/typeorm#534) so I will wait until this ships before implementing deletes.
Up until now all development and testing has been done against the source using ts-node.
Ultimately, Vendure will be an npm package and we will be delivering transpiled JavaScript with accompanying d.ts
files.
To build for distribution, the following needs to be solved:
Currently FacetValues can only be added to ProductVariants (see design in #2). With further development it seems apparent that in many cases it makes more sense to have FacetValues primarily set at the Product level:
This should be a relatively simple change:
[FacetValues!]!
property to Product
type schema + create / udapte Inputs.We need a way for developers to add arbitrary custom fields to any entity.
Adding a field to an entity involves the following files (example for Product):
It would be ideal to not require the developer to have to update multiple files and keep them in sync in order to add fields. Better would be to add the fields via config when bootstrapping the server, e.g.
bootstrap({
port: API_PORT,
apiPath: API_PATH,
dbConnectionOptions: {
//...
},
customFields: {
Product: [
{ name: 'infoUrl', type: 'string' } // this is a minimal example. In practice there will
// need to be more config options for each field
],
},
);
Then on bootstrap, we dynamically add those fields to the appropriate entities and also extend the GraphQL schema using the JS API.
Other types could be added (e.g. geo-spacial types, blob types) but the above would be the most common.
It would be quite likely that the developer would want to sort or filter by a custom field. This would then require dynamic extensions to the GraphQL resolvers for that entity. There needs to be some generic way of passing in custom sort and filter arguments to all list queries.
A form input will need to be generated in the admin ui app for the detail view of that entity (where such a detail view exists) and a "editable" boolean config option could be used to signify that the field value is read-only in the admin ui.
A Media
entity would represent all types of binary files. It would have a type
discriminator whose value would be a MediaType
(image, video, etc).
The Media entity would have a pointer to the actual binary file somewhere on a disk. The physical storage location should be configurable - it should not have to reside on the same machine as the Vendure API server. (look into existing Node libs which abstract away the file system).
There should be a built-in way to generate different sizes of image whilst preserving the original. This could be generalized into a "preview" (or better name) property which would be a collection of one or more MediaPreview
entities. This would also then handle the case for preview images of videos, pdfs and other file types.
Another way would be to have a single "preview" image which can be resized on-the-fly with query parameters. These generated previews would then be cached so that the re-sampling only need take place for the initial call. This is much simpler but has performance implications.
Maybe the way to go would be to automatically generate a default set of previews (according to some config), and then further permutations can be generated on the fly.
It would be good to abstract the actual image-manipulation lib away so that the image processing capabilities can be extended by the developer, so in essence we pass through the query string to the configured "image plugin" which can then pass off the work to some library. For example, a developer may want to use ImageMagick rather than a JavaScript-based lib for improved performance.
The Product
and ProductVariant
entities would have the existing image
string property replaced by a media
property which would be a collection of Media
entities.
Additionally, the custom fields API should be extended to allow Media fields with optional type
filter.
Where there is more than 1 Media entity, there must be a way to choose the "default" one, i.e. the image to show in a list view of Products, for example.
This could be done by:
defaultMedia
property which stores the id of one of the linked Media entities.I would tend to favour the first solution, since it is more explicit.
A media browser is a UI component which is used in the admin-ui to select an existing Media entity. As a first implementation this would be akin to the media browser in WordPress - essentially a flat (no folders), filterable list of all Media entities.
For large shops this might become unwieldy - our reference shop has over 6,000 individual images. Therefore a future extension will have to address the issue of organizing large media collections, with folders or tags or something like that.
Product Categories are a way of grouping Products. A category in essence a collection of Facets (#2). Any ProductVariant which to which all of those Facets are assigned would then be considered to be in that category.
Some shops may choose to organize their products in a tree-like taxonomy, starting with the most general grouping at the top and getting ever-more specific. E.g. "electrical goods" -> "phones" -> "iPhones"
Another use is for applying promotions to a particular category.
Product | Facet: Brand | Facet: Type |
---|---|---|
iPhone X | Apple | phone |
iPhone 8 | Apple | phone |
Pixel 3 | phone | |
Macbook Pro | Apple | laptop |
XPS 15 | Dell | laptop |
3210 | Nokia | phone |
ProductCategory | FacetValues | Products |
---|---|---|
Apple Products | Apple | iPhoneX, iPhone 8, Macbook Pro |
iPhones | Apple, phone | iPhone X, iPhone 8 |
Laptops | laptop | Macbook Pro, XPS 15 |
There should be a way of having a hierarchy of categories. E.g. The "electrical goods" category contains "phones" and "laptops". Another way to say it is that "phones" has "electrical goods" as a parent.
To implement this, the ProductCategory
would have a "parent" one-to-many relationship. TypeORM has built-in support for such tree structures: http://typeorm.io/#/tree-entities
This issue is a discussion of the high-level implementation of Orders
Orders represent a collection of ProductVariants which a Customer is in the process of purchasing or has purchased.
An Order can be thought of as a finite state machine with the following states:
OrderItem
entity is created for each ProductVariant which represents the variant in the given quantity. This state continues until the checkout stage.It would be useful to allow the developer to define additional states for the Order to match the particular business requirements of the shop. I'll look for existing FSM libs to see if this can be accommodated in a safe and intuitive manner.
When transitioning from one state into another, one or more functions can be called to execute some logic before the transition is made, and possibly also have the opportunity to stop the transition.
For example, when an Order transitions from "ArrangingPayment" to "Completed", a hook could generate and send a transactional email confirming the order.
The cost of an order must take into account the following factors:
Plugins are a way to customize the behaviour of Vendure. They would allow the developer to replace certain default behaviours with new ones, and to extend the capabilities of the default Vendure install.
For example, a plugin could:
We have a single VendureConfig
object which contains all configurable aspects of the server.
The config object has three broad kinds of configurable property:
port
or channelTokenKey
.The idea is that a "plugin" is just a way to set a bunch of related config rules all at once, as well as perform any other set-up or logic which might be needed when the app bootstraps.
The simplest implementation would be:
export interface VendurePlugin {
init(config: VendureConfig): VendureConfig | Promise<VendureConfig>;
}
A plugin implements this interface and when the app bootstraps, each plugin is run in series (they would be defined as an array in the config passed to the bootstrap()
function).
Each plugin can modify the config and then that modified config is passed on to the next plugin in the array.
Currently we have Jest unit tests for the server, but no tests for the API itself (i.e. tests which make GraphQL API calls and assert the response)
Now that the general API parts already implemented are (hopefully) somewhat stable, it makes sense to set up API testing infrastructure.
This would involve:
A ProductVariant should have assets just like a Product.
Use case:
Product = Winsor & Newton artists' watercolours.
ProductVariant = tube of Cerulean Blue. Asset is a color swatch image.
The following needs to be done:
assets
and featuredAsset
props to the TS entity and the GraphQL typeThe ProductVariantTranslation entity is not being updated.
See product.service.ts
line 102. Also this whole method should be moved to product-variant.service.ts
.
For queries which yield a PaginatedList
response (e.g. products()
, customers()
etc.) it will be a common requirement to filter and sort those lists.
There should be a generic format for requests which allow filtering and sorting to be expressed in the query arguments.
First of all we should put all list options into a single object. Currently we have individual take
and skip
args. These should be moved to a ListOptions
type:
interface ListQueryOptions {
take: number;
skip: number;
sort: SortParameter[];
filter: FilterParameter[];
}
This approach also has the advantage that we can more easily add options in the future.
interface SortParameter {
field: string;
order: 'asc' | 'desc';
}
interface FilterParameter {
field: string;
operator: FilterOperator;
value: string | number;
}
type FilterOperator =
'gt' |
'gte' |
'lt' |
'lte' |
'eq' |
'contains' |
'startsWith';
Reference: https://en.wikipedia.org/wiki/Payment_gateway#Typical_transaction_processes
Payment would typically be handled by a 3rd party payment provider such as PayPal, Stripe, WorldPay, etc.
The typical flow is to make a call to the payment API with the payment information (and possibly the order details). The payment is processed asynchronously and then on success a token is returned. The payment source (card) may be charged immediately or at some later point (e.g. upon shipping).
There are a number of possible payment workflows used by various payment gateways and methods:
In this workflow, the customer is redirected to the gateway. The customer then enters all data on the gateway website. Upon completion (approval, clearing or declined etc), the customer is redirected to some specified page where the completion payload is delivered e.g by a POST request, or by a JavaScript callback.
Example: WorldPay Business Gateway
In this workflow, the payment data is entered into a form on the storefront (typically provided as a ready-made JavaScript lib by the payment gateway), and then this data is securely posted via XHR to an API endpoint provided by the gateway. The card is authorized and the payment is settled in a single step. The response contains the token etc. which is then stored as a Payment in Vendure.
Example: PayPal Checkout
In this workflow, an initial call is made to the payment gateway from the client side to set up and authorize the payment. A token is returned but the charge has not been settled. This token is then used to create a Payment in Vendure with the "authorized" state. This token can then be used for more advanced workflows, such as only settling the payment upon shipment or storing s reference to the customer & card for easy repeat payments (e.g. Stripe Checkout.
Some websites store payment data for convenient reuse in repeat orders. This means that the payment data is sent to the server, and the authorization process is carried out between the server and gateway, rather than the client and gateway. This workflow implies PCI DSS compliance which we will not get into.
Vendure should support the first 2 workflows natively. The third can be supported with custom code by those businesses which have the resources and will.
addPaymentToOrder
mutation.Settled
state.PaymentsSettled
.addPaymentToOrder
mutation returns the Order, and since it is now settled, the customer is shown the "thank you for your order" page.addPaymentToOrder
mutation.Authorized
.Settled
state, and in the step a hook is used to settle the charge against the Stripe charges API using the authorization token. Upon success the hook returns true
so that the transition can complete.PaymentsSettled
.addPaymentToOrder
mutation returns the Order, and since it is now settled, the customer is shown the "thank you for your order" page.The PaymentMethod defines the states and transition hooks for a given payment gateway. When the addPaymentToOrder
mutation is called, Vendure would look up the matching PaymentMethod and then apply a method to create the Payment entity and transition its state as per the case studies above.
The PaymentMethod would be a TypeScript class passed into the VendureConfig, which would then auto-generate a corresponding entity in the database which would allow customization of variables (such as API key used in calls to the Stripe API) which would be stored and manipulated in the same way as Promotion conditions / actions.
A Payment represents an amount paid towards the Order total. Usually it will equal the total, but might not in the case that multiple payments are made for a single Order.
The Payment would store the state of the transaction (authorized, cleared, declined etc), the token returned on success, plus any other pertinent information which might be useful to keep such as IP address and specific info returned by the payment provider.
addPaymentToOrder
mutationThis mutation would take a token, the code of the PaymentMethod and possibly other pertinent data and create a Payment entity associated with the given order.
The default states should include:
It is currently in beta, so once it hits stable, we should upgrade: https://vmware.github.io/clarity/news/1.0.0-beta.2
Currently we are using the Apollo CLI codegen capabilities to generate interfaces based on the server schema & client operations. This works well but has the limitation that types are only generated if they are used in a client-side query.
This is not too much of a problem for admin-focused operations, since the admin ui will inevitably contain the queries/mutations required to produces the types for those operations. The problem comes for the shopfront-oriented operations (e.g. addItemToOrder()
), which might not ever be used in the admin ui app.
There is an open issue (apollographql/rover#351) on the Apollo CLI repo to be able to generate types for all server schema operations, but there is currently no resolution.
Look into alternatives for generating the server schemas, such as:
Facets are properties which can be assigned to a product / product variant and are used to encode properties of that product along arbitrary dimensions. Reference: https://www.sitepoint.com/cms-content-organization-structures-trees-vs-facets-vs-tags/
Facets are used to add structured metatdata to a product for the purpose of categorization and filtering.
A typical use-case is to have a filter-based product-discovery interface e.g. like Asos which allows filtering by various facets. Amazon also use this approach.
If a shop would prefer a tree-like category-based navigation, this can be built up via pre-defined combinations of facets which are grouped into a category. Further design on facet-based categories to follow.
Most countries have some kind of sales tax applied to goods being purchased.
In the UK for example, the current VAT rates are zero (0%), reduced (5%) and standard (20%). Here are rates in other EU countries
A tax is modeled as a type of Adjustment (#29) (AdjustmentType.TAX
) and should be applied before any other adjustments, since other adjustment conditions would typically query the tax-inclusive prices.
ProductVariant
entity should have a taxCategory
property which points to a tax AdjustmentSource.price
property should be tax-inclusive. There should be a new property, priceBeforeTax
which contains the price before taxes are applied.A Customer can be a guest (has no associated User) or registered (has an associated User).
An account can be created in one of two ways:
As the diagram shows, a customer does not need to create an account after placing an order. Indeed, the customer is free to only every do guest checkouts. For the checkout, however, the email address would be used to create a guest Customer, and then on subsequent checkouts with the same email address, the same guest Customer would be assumed.
At any time, the guest Customer can be converted into an authenticated Customer by registering in either of the two ways outlined above.
There are 2 questions to resolve regarding registration:
A user should have the ability to delete their account, as should an administrator. In this case, we want to keep the physical row in the DB for the purposes of data consistency and reporting, but we should completely remove any personally identifiable information (PII):
β[A]n identifiable natural person is one who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number, location data, an online identifier or to one or more factors specific to the physical, physiological, genetic, mental, economic, cultural or social identity of that natural person.β source
The new version 14 of graphql-js was recently released.
We have already been using the RC version for a while on the server but now should upgrade to the final release and also upgrade any dependencies which have it as a peerDependency etc.
Check out https://github.com/basecamp/trix
Then we can have the same version of TypeScript for both server and UI, which will make development a bit smoother.
Sometimes the loading indicator in the admin ui gets "stuck" in the loading state, which means that the loading counter never decrements back to zero.
When this occurs and yet there are no API requests in flight, then there is a logic error somewhere, which should be found and fixed.
Hiho, I am just followed the readme (populated via yarn and then started server and admin-ui). When I try to login I will get: "No valid channel was specified".
{"errors":[{"message":"No valid channel was specified","locations":[{"line":2,"column":3}],"path":["config"],"extensions":{"code":"NO_VALID_CHANNEL","exception":{"stacktrace":["Error: error.no-valid-channel-specified"," at RequestContextService.getChannelToken (/home/uter/src/vendure/server/src/api/common/request-context.service.ts:59:19)"," at RequestContextService.fromRequest (/home/uter/src/vendure/server/src/api/common/request-context.service.ts:33:35)"," at AuthGuard.canActivate (/home/uter/src/vendure/server/src/api/middleware/auth-guard.ts:40:65)"," at process._tickCallback (internal/process/next_tick.js:68:7)"]}}}],"data":null}
Cheers,
Christoph
A simple, easy-to-use search tool is critical for a successful ecommerce site. If people canβt find your products, they canβt buy them. A bad in-site search can frustrate users enough to make them abandon you for a competitor. source: Neilsen Norman
Search functionality is one of the most important factors of an ecommerce app. Vendure will ship with a built-in SQL-based search solution, and will be easily extended with more specialized search plugins.
There should be an interface which is common to all search plugins, so that plugins can be easily swapped out. This interface would include:
The existing products
endpoint can be used for a rudimentary search but is not ideal because it involves potentially expensive joins of many tables.
Instead, a separate search index would be created which denormalizes these entities into simple rows for easy search and filtering. For external tools such as Elastic, this is also the approach used.
Since the index is denomalized, it will become stale upon changes to any of the related entities. For this reason it must be updated when a Product, ProductVariant, Facet, FacetValue or Category changes. The naive approach would be to rebuild the entire index on any change, but this will likely be inefficient. A better way to do it would be to create a format which provides the changed entity, and the indexer can then determine which records are affected by that change and update only those rows.
Potentially the EventBus system can be used to trigger updates to the index.
Some sites may opt for a hosted 3rd party search solution such as Algolia. In this case, the indexing would still be handled by the plugin. In the case of Algolia, the plugin would use the Algolia API to push records into the index.
On the other hand, the actual search request by the customer may go direct to the search provider (as is recommended by Algolia), rather than going through the Vendure server which would act as a proxy. Both patterns should be supported.
The search endpoint is what would be used by the storefront client app to return search results. Since different search engines may have vastly differing query formats, it makes sense to have a generic JSON input type for the search input, and then have the search plugin transform and forward that data on to the search engine.
The following signals should be supported by the default search plugin:
Advanced search includes things like boolean operators in a search term. Research indicates that the vast majority of ecommerce users do not use such features.
In our recent search study, the mean query length was 2.0 words. Other studies also show a preponderance of simple searches. Most users cannot use advanced search or Boolean query syntax. source: Nielsen Norman
Therefore we will not support things like operators in the default search. Custom plugins are free to implement such features.
Prettier v1.15 supports pretty-printing of Angular templates: https://prettier.io/blog/2018/11/07/1.15.0.html
Currently a ProductVariant has a price
property with a single integer value.
It may be desirable to allow a ProductVariant to have multiple price values, one for each supported currency.
price
field is an array of currency-value objects.In practice, both of the above imply a one-to-many relationship of ProductVariant -> Price objects, in which case we may as well opt for a Channel-based approach, since Channels also unlock other desirable features.
It is missing from recently-implemented resolver methods.
There are a number of circumstances in which an ecommerce shop might want to generate and send an email to a customer, e.g:
There are 3 aspects to transaction emails: events, email creation, and sending.
An event would be some point in a process where listeners could be notified and then decide whether or not to create and send an email. Typical events would include: account created, order transitions to a certain state.
We already have hooks in those processes governed by the FiniteStateMachine, but this is not enough. For example, a cart abandonment email is not necessarily triggered by any kind of state transition, but perhaps an automated task that runs periodically.
Therefore it would make sense to have some kind of generic event bus service (#40), whereby various events are published and then the email handler can subscribe to any of these and thereby send an email at the right moment.
This step occurs in response to an event and is concerned with generating the body of the email. It would use some kind of templating system such as Handlebars which could be passed pertinent information from the event object (e.g. the Order and Customer entities in the case of a hypothetical OrderStateTransitionEvent) and would use this data to interpolate into the template. The result would be typically a string of HTML.
A "transport" is a method of actually sending the mail to the recipient.
SMTP is the most common way of sending an email. A business will either use their own SMTP server, or use a service such as Mandrill or SendGrid
Local - A local email server may be used such as sendmail may be used to directly send the email.
Other - For testing purposes a mock transport may be used which simply writes the email to a file on the local file system.
Nodemailer supports all of the above transport out of the box, and new transports can be written.
Currently, auth is still handled via a REST call. The AuthController should be replaced by a GraphQL resolver.
We are currently on 1.3.6. v2 is broadly backwards-compatible but since we are using the Nest GraphQLModule, we need to wait until v2 is properly supported.
There is an thread in which some people get it to work, but there currently seems to be an issue with the Passport.js integration.
Best to wait for a bit to see if these issues get resolved.
While designing the Emails system, it became apparent that a generic event bus would be beneficial.
Here is the problem statement that EventBus solves in a nutshell:
"I want an easy, centralized way to notify code that's interested in specific types of events when those events occur without any direct coupling between the code that publishes an event and the code that receives it." source
The rationale of why is makes sense for email handling is outlined in #39.
Other use-cases:
The basic design is a singleton service, EventBus
, which has a subscribe
method and a publish
method:
abstract class VendureEvent {}
class OrderStateTransitionEvent extends VendureEvent {
constructor(private order: Order, private customer: Customer) {}
}
class EventBus {
subscribe<T extends VendureEvent>(event: T, handler: (event: T) => void) {
// ...
}
publish(event: VendureEvent) {
// ...
}
}
// order-state-machine.ts
onTransitionEnd: (fromState, toState, data) => {
// ...
this.eventBus.publish(new OrderStateTransitionEvent(
data.order,
data.customer,
));
return this.onTransitionEnd(fromState, toState, data);
}
The build-in events should include:
There will probably be a few more that make sense, which can be added as needed.
Currently we are using shared types & utils from both server & admin-ui, and we end up with ugly import paths like:
import { CustomFieldConfig } from '../../../../../../shared/shared-types';
I think there is a way to configure TS to resolve the "shared" dir so we could instead use:
import { CustomFieldConfig } from 'shared/shared-types';
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.