-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): Implement EntityHydrator to simplify working with entities
Relates to #1103
- Loading branch information
1 parent
469e3f7
commit 28e6a3a
Showing
13 changed files
with
582 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
/* tslint:disable:no-non-null-assertion */ | ||
import { mergeConfig, Product } from '@vendure/core'; | ||
import { createTestEnvironment } from '@vendure/testing'; | ||
import gql from 'graphql-tag'; | ||
import path from 'path'; | ||
|
||
import { initialData } from '../../../e2e-common/e2e-initial-data'; | ||
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config'; | ||
|
||
import { HydrationTestPlugin } from './fixtures/test-plugins/hydration-test-plugin'; | ||
|
||
describe('Entity hydration', () => { | ||
const { server, adminClient } = createTestEnvironment( | ||
mergeConfig(testConfig, { | ||
plugins: [HydrationTestPlugin], | ||
}), | ||
); | ||
|
||
beforeAll(async () => { | ||
await server.init({ | ||
initialData, | ||
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'), | ||
customerCount: 1, | ||
}); | ||
await adminClient.asSuperAdmin(); | ||
}, TEST_SETUP_TIMEOUT_MS); | ||
|
||
afterAll(async () => { | ||
await server.destroy(); | ||
}); | ||
|
||
it('includes existing relations', async () => { | ||
const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, { | ||
id: 'T_1', | ||
}); | ||
|
||
expect(hydrateProduct.facetValues).toBeDefined(); | ||
expect(hydrateProduct.facetValues.length).toBe(2); | ||
}); | ||
|
||
it('hydrates top-level single relation', async () => { | ||
const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, { | ||
id: 'T_1', | ||
}); | ||
|
||
expect(hydrateProduct.featuredAsset.name).toBe('derick-david-409858-unsplash.jpg'); | ||
}); | ||
|
||
it('hydrates top-level array relation', async () => { | ||
const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, { | ||
id: 'T_1', | ||
}); | ||
|
||
expect(hydrateProduct.assets.length).toBe(1); | ||
expect(hydrateProduct.assets[0].asset.name).toBe('derick-david-409858-unsplash.jpg'); | ||
}); | ||
|
||
it('hydrates nested single relation', async () => { | ||
const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, { | ||
id: 'T_1', | ||
}); | ||
|
||
expect(hydrateProduct.variants[0].product.id).toBe('T_1'); | ||
}); | ||
|
||
it('hydrates nested array relation', async () => { | ||
const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, { | ||
id: 'T_1', | ||
}); | ||
|
||
expect(hydrateProduct.variants[0].options.length).toBe(2); | ||
}); | ||
|
||
it('translates top-level translatable', async () => { | ||
const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, { | ||
id: 'T_1', | ||
}); | ||
|
||
expect(hydrateProduct.variants.map(v => v.name).sort()).toEqual([ | ||
'Laptop 13 inch 16GB', | ||
'Laptop 13 inch 8GB', | ||
'Laptop 15 inch 16GB', | ||
'Laptop 15 inch 8GB', | ||
]); | ||
}); | ||
|
||
it('translates nested translatable', async () => { | ||
const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, { | ||
id: 'T_1', | ||
}); | ||
|
||
expect( | ||
getVariantWithName(hydrateProduct, 'Laptop 13 inch 8GB') | ||
.options.map(o => o.name) | ||
.sort(), | ||
).toEqual(['13 inch', '8GB']); | ||
}); | ||
|
||
it('translates nested translatable 2', async () => { | ||
const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, { | ||
id: 'T_1', | ||
}); | ||
|
||
expect(hydrateProduct.assets[0].product.name).toBe('Laptop'); | ||
}); | ||
|
||
it('populates ProductVariant price data', async () => { | ||
const { hydrateProduct } = await adminClient.query<HydrateProductQuery>(GET_HYDRATED_PRODUCT, { | ||
id: 'T_1', | ||
}); | ||
|
||
expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 8GB').price).toBe(129900); | ||
expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 8GB').priceWithTax).toBe(155880); | ||
expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 16GB').price).toBe(219900); | ||
expect(getVariantWithName(hydrateProduct, 'Laptop 13 inch 16GB').priceWithTax).toBe(263880); | ||
expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 8GB').price).toBe(139900); | ||
expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 8GB').priceWithTax).toBe(167880); | ||
expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 16GB').price).toBe(229900); | ||
expect(getVariantWithName(hydrateProduct, 'Laptop 15 inch 16GB').priceWithTax).toBe(275880); | ||
}); | ||
}); | ||
|
||
function getVariantWithName(product: Product, name: string) { | ||
return product.variants.find(v => v.name === name)!; | ||
} | ||
|
||
type HydrateProductQuery = { hydrateProduct: Product }; | ||
|
||
const GET_HYDRATED_PRODUCT = gql` | ||
query GetHydratedProduct($id: ID!) { | ||
hydrateProduct(id: $id) | ||
} | ||
`; |
50 changes: 50 additions & 0 deletions
50
packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { Args, Query, Resolver } from '@nestjs/graphql'; | ||
import { | ||
Ctx, | ||
EntityHydrator, | ||
ID, | ||
PluginCommonModule, | ||
Product, | ||
RequestContext, | ||
TransactionalConnection, | ||
VendurePlugin, | ||
} from '@vendure/core'; | ||
import gql from 'graphql-tag'; | ||
|
||
@Resolver() | ||
export class TestAdminPluginResolver { | ||
constructor(private connection: TransactionalConnection, private entityHydrator: EntityHydrator) {} | ||
|
||
@Query() | ||
async hydrateProduct(@Ctx() ctx: RequestContext, @Args() args: { id: ID }) { | ||
const product = await this.connection.getRepository(ctx, Product).findOne(args.id, { | ||
relations: ['facetValues'], | ||
}); | ||
// tslint:disable-next-line:no-non-null-assertion | ||
await this.entityHydrator.hydrate(ctx, product!, { | ||
relations: [ | ||
'variants.options', | ||
'variants.product', | ||
'assets.product', | ||
'facetValues.facet', | ||
'featuredAsset', | ||
'variants.stockMovements', | ||
], | ||
applyProductVariantPrices: true, | ||
}); | ||
return product; | ||
} | ||
} | ||
|
||
@VendurePlugin({ | ||
imports: [PluginCommonModule], | ||
adminApiExtensions: { | ||
resolvers: [TestAdminPluginResolver], | ||
schema: gql` | ||
extend type Query { | ||
hydrateProduct(id: ID!): JSON | ||
} | ||
`, | ||
}, | ||
}) | ||
export class HydrationTestPlugin {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './transactional-connection'; | ||
export * from './transaction-subscriber'; | ||
export * from './connection.module'; | ||
export * from './types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { ID } from '@vendure/common/lib/shared-types'; | ||
import { FindOneOptions } from 'typeorm'; | ||
|
||
/** | ||
* @description | ||
* Options used by the {@link TransactionalConnection} `getEntityOrThrow` method. | ||
* | ||
* @docsCategory data-access | ||
*/ | ||
export interface GetEntityOrThrowOptions<T = any> extends FindOneOptions<T> { | ||
/** | ||
* @description | ||
* An optional channelId to limit results to entities assigned to the given Channel. Should | ||
* only be used when getting entities that implement the {@link ChannelAware} interface. | ||
*/ | ||
channelId?: ID; | ||
/** | ||
* @description | ||
* If set to a positive integer, it will retry getting the entity in case it is initially not | ||
* found. | ||
* | ||
* @since 1.1.0 | ||
* @default 0 | ||
*/ | ||
retries?: number; | ||
/** | ||
* @description | ||
* Specifies the delay in ms to wait between retries. | ||
* | ||
* @since 1.1.0 | ||
* @default 25 | ||
*/ | ||
retryDelay?: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
packages/core/src/service/helpers/entity-hydrator/entity-hydrator-types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { VendureEntity } from '../../../entity/base/base.entity'; | ||
|
||
/** | ||
* @description | ||
* Options used to control which relations of the entity get hydrated | ||
* when using the {@link EntityHydrator} helper. | ||
* | ||
* @since 1.3.0 | ||
*/ | ||
export interface HydrateOptions<Entity extends VendureEntity> { | ||
/** | ||
* @description | ||
* Defines the relations to hydrate, using strings with dot notation to indicate | ||
* nested joins. If the entity already has a particular relation available, that relation | ||
* will be skipped (no extra DB join will be added). | ||
*/ | ||
relations: Array<EntityRelationPaths<Entity>>; | ||
/** | ||
* @description | ||
* If set to `true`, any ProductVariants will also have their `price` and `priceWithTax` fields | ||
* applied based on the current context. If prices are not required, this can be left `false` which | ||
* will be slightly more efficient. | ||
* | ||
* @default false | ||
*/ | ||
applyProductVariantPrices?: boolean; | ||
} | ||
|
||
// The following types are all related to allowing dot-notation access to relation properties | ||
export type EntityRelationKeys<T extends VendureEntity> = { | ||
[K in Extract<keyof T, string>]: T[K] extends VendureEntity | ||
? K | ||
: T[K] extends VendureEntity[] | ||
? K | ||
: never; | ||
}[Extract<keyof T, string>]; | ||
|
||
export type EntityRelations<T extends VendureEntity> = { | ||
[K in EntityRelationKeys<T>]: T[K]; | ||
}; | ||
|
||
export type PathsToStringProps1<T extends VendureEntity> = T extends string | ||
? [] | ||
: { | ||
[K in EntityRelationKeys<T>]: K; | ||
}[Extract<EntityRelationKeys<T>, string>]; | ||
|
||
export type PathsToStringProps2<T extends VendureEntity> = T extends string | ||
? never | ||
: { | ||
[K in EntityRelationKeys<T>]: T[K] extends VendureEntity[] | ||
? [K, PathsToStringProps1<T[K][number]>] | ||
: [K, PathsToStringProps1<T[K]>]; | ||
}[Extract<EntityRelationKeys<T>, string>]; | ||
|
||
export type TripleDotPath = `${string}.${string}.${string}`; | ||
|
||
export type EntityRelationPaths<T extends VendureEntity> = | ||
| PathsToStringProps1<T> | ||
| Join<PathsToStringProps2<T>, '.'> | ||
| TripleDotPath; | ||
|
||
// Based on https://stackoverflow.com/a/47058976/772859 | ||
export type Join<T extends Array<string | any>, D extends string> = T extends [] | ||
? never | ||
: T extends [infer F] | ||
? F | ||
: // tslint:disable-next-line:no-shadowed-variable | ||
T extends [infer F, ...infer R] | ||
? F extends string | ||
? `${F}${D}${Join<Extract<R, string[]>, D>}` | ||
: never | ||
: string; |
Oops, something went wrong.