@rytass/wms-module-graphql 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -55,6 +55,65 @@ exports.LoaderService = class LoaderService {
55
55
  const fields = ___default.default(Object.keys(__from)).sort();
56
56
  return `${__hash}_${fields.map((field) => data[field]).join('_')}`;
57
57
  }
58
+ /**
59
+ * resolve a `__from` property name to the physical column it is stored in.
60
+ *
61
+ * TypeORM's `FindOptionsWhere` only accepts real column properties. A
62
+ * `@RelationId()` property — e.g. `LocationEntityClass.parentId`, which mirrors
63
+ * the `parent` tree relation — is virtual and is rejected with
64
+ * `EntityPropertyNotFoundError`, even though it is backed by the relation's
65
+ * join column. Map those back to their join column so they stay batchable.
66
+ * @param EntityMetadata
67
+ * @param string
68
+ * @returns string | null
69
+ */
70
+ resolveColumnName(metadata, propertyName) {
71
+ const column = metadata.findColumnWithPropertyName(propertyName);
72
+ if (column)
73
+ return column.databaseName;
74
+ const relationId = metadata.relationIds.find((candidate) => candidate.propertyName === propertyName);
75
+ const joinColumns = relationId?.relation.joinColumns ?? [];
76
+ return joinColumns.length === 1 ? joinColumns[0].databaseName : null;
77
+ }
78
+ /**
79
+ * build the where clause for `__from` keys that TypeORM's find options cannot
80
+ * express (see {@link resolveColumnName}), by addressing the columns directly.
81
+ * @param SelectQueryBuilder
82
+ * @param EntityMetadata
83
+ * @param LoaderOptions[]
84
+ * @param (keyof T)[]
85
+ */
86
+ applyColumnWhere(qb, metadata, options, primaryKeys) {
87
+ const columns = primaryKeys.map((primaryKey) => {
88
+ const columnName = this.resolveColumnName(metadata, String(primaryKey));
89
+ if (!columnName)
90
+ throw new Error(`Loader cannot resolve property "${String(primaryKey)}" to a column of "${metadata.name}".`);
91
+ return {
92
+ primaryKey,
93
+ column: `entity.${this.dataSource.driver.escape(columnName)}`,
94
+ };
95
+ });
96
+ if (columns.length === 1) {
97
+ const [{ primaryKey, column }] = columns;
98
+ qb.andWhere(`${column} IN (:...wmsLoaderValues)`, {
99
+ wmsLoaderValues: options.map(({ __from }) => __from[primaryKey]),
100
+ });
101
+ return;
102
+ }
103
+ qb.andWhere(new typeorm.Brackets((where) => {
104
+ options.forEach(({ __from }, optionIndex) => {
105
+ const parameters = {};
106
+ const predicate = columns
107
+ .map(({ primaryKey, column }, columnIndex) => {
108
+ const parameter = `wmsLoaderValue_${optionIndex}_${columnIndex}`;
109
+ parameters[parameter] = __from[primaryKey];
110
+ return `${column} = :${parameter}`;
111
+ })
112
+ .join(' AND ');
113
+ where.orWhere(`(${predicate})`, parameters);
114
+ });
115
+ }));
116
+ }
58
117
  /**
59
118
  * query data by collecting primary keys from options with same `__entity`
60
119
  * @param LoaderOptions[]
@@ -65,7 +124,10 @@ exports.LoaderService = class LoaderService {
65
124
  const repo = this.dataSource.getRepository(__entity);
66
125
  let qb = repo.createQueryBuilder('entity');
67
126
  const primaryKeys = Object.keys(__from);
68
- if (primaryKeys.length > 1)
127
+ const hasVirtualKey = primaryKeys.some((primaryKey) => !repo.metadata.findColumnWithPropertyName(String(primaryKey)));
128
+ if (hasVirtualKey)
129
+ this.applyColumnWhere(qb, repo.metadata, options, primaryKeys);
130
+ else if (primaryKeys.length > 1)
69
131
  qb.setFindOptions({ where: options.map(({ __from }) => __from) });
70
132
  else {
71
133
  const [primaryKey] = primaryKeys;
@@ -3611,8 +3673,21 @@ let LocationResolver = class LocationResolver {
3611
3673
  __from: { id: parentId },
3612
3674
  });
3613
3675
  }
3614
- children({ children }) {
3615
- return children;
3676
+ /**
3677
+ * `children` must never be read off the parent entity: only
3678
+ * `LocationQuery.location` and `LocationService.findMany` eager-load the tree
3679
+ * relation. Every other producer of a `LocationDto` (stock/loader/change-log/
3680
+ * transfer/ship/receive/warehouse-map resolvers, the `parent` field above and
3681
+ * the create/update mutations) hands back an entity whose `children` is
3682
+ * `undefined`, which breaks the non-nullable list. Always resolve through the
3683
+ * request-scoped loader instead.
3684
+ */
3685
+ children({ id }, { loaders }) {
3686
+ return loaders.batchMany.load({
3687
+ __entity: wmsModuleCore.LocationEntityClass,
3688
+ __from: { parentId: id },
3689
+ __conditions: (qb) => qb.orderBy('entity."key"', 'ASC'),
3690
+ });
3616
3691
  }
3617
3692
  };
3618
3693
  tslib.__decorate([
@@ -3634,9 +3709,10 @@ tslib.__decorate([
3634
3709
  tslib.__decorate([
3635
3710
  graphql.ResolveField(() => [LocationDto]),
3636
3711
  tslib.__param(0, graphql.Parent()),
3712
+ tslib.__param(1, graphql.Context()),
3637
3713
  tslib.__metadata("design:type", Function),
3638
- tslib.__metadata("design:paramtypes", [wmsModuleCore.LocationEntityClass]),
3639
- tslib.__metadata("design:returntype", void 0)
3714
+ tslib.__metadata("design:paramtypes", [wmsModuleCore.LocationEntityClass, Object]),
3715
+ tslib.__metadata("design:returntype", Promise)
3640
3716
  ], LocationResolver.prototype, "children", null);
3641
3717
  LocationResolver = tslib.__decorate([
3642
3718
  graphql.Resolver(() => LocationDto)
@@ -33,6 +33,28 @@ export declare class LoaderService {
33
33
  * @returns Map<string, data>
34
34
  */
35
35
  private useIndex;
36
+ /**
37
+ * resolve a `__from` property name to the physical column it is stored in.
38
+ *
39
+ * TypeORM's `FindOptionsWhere` only accepts real column properties. A
40
+ * `@RelationId()` property — e.g. `LocationEntityClass.parentId`, which mirrors
41
+ * the `parent` tree relation — is virtual and is rejected with
42
+ * `EntityPropertyNotFoundError`, even though it is backed by the relation's
43
+ * join column. Map those back to their join column so they stay batchable.
44
+ * @param EntityMetadata
45
+ * @param string
46
+ * @returns string | null
47
+ */
48
+ private resolveColumnName;
49
+ /**
50
+ * build the where clause for `__from` keys that TypeORM's find options cannot
51
+ * express (see {@link resolveColumnName}), by addressing the columns directly.
52
+ * @param SelectQueryBuilder
53
+ * @param EntityMetadata
54
+ * @param LoaderOptions[]
55
+ * @param (keyof T)[]
56
+ */
57
+ private applyColumnWhere;
36
58
  /**
37
59
  * query data by collecting primary keys from options with same `__entity`
38
60
  * @param LoaderOptions[]
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
3
3
  import { createHash } from 'crypto';
4
4
  import _ from 'lodash';
5
5
  import { LRUCache } from 'lru-cache';
6
- import { DataSource, In } from 'typeorm';
6
+ import { DataSource, Brackets, In } from 'typeorm';
7
7
  import DataLoader from 'dataloader';
8
8
 
9
9
  let LoaderService = class LoaderService {
@@ -42,6 +42,65 @@ let LoaderService = class LoaderService {
42
42
  const fields = _(Object.keys(__from)).sort();
43
43
  return `${__hash}_${fields.map((field) => data[field]).join('_')}`;
44
44
  }
45
+ /**
46
+ * resolve a `__from` property name to the physical column it is stored in.
47
+ *
48
+ * TypeORM's `FindOptionsWhere` only accepts real column properties. A
49
+ * `@RelationId()` property — e.g. `LocationEntityClass.parentId`, which mirrors
50
+ * the `parent` tree relation — is virtual and is rejected with
51
+ * `EntityPropertyNotFoundError`, even though it is backed by the relation's
52
+ * join column. Map those back to their join column so they stay batchable.
53
+ * @param EntityMetadata
54
+ * @param string
55
+ * @returns string | null
56
+ */
57
+ resolveColumnName(metadata, propertyName) {
58
+ const column = metadata.findColumnWithPropertyName(propertyName);
59
+ if (column)
60
+ return column.databaseName;
61
+ const relationId = metadata.relationIds.find((candidate) => candidate.propertyName === propertyName);
62
+ const joinColumns = relationId?.relation.joinColumns ?? [];
63
+ return joinColumns.length === 1 ? joinColumns[0].databaseName : null;
64
+ }
65
+ /**
66
+ * build the where clause for `__from` keys that TypeORM's find options cannot
67
+ * express (see {@link resolveColumnName}), by addressing the columns directly.
68
+ * @param SelectQueryBuilder
69
+ * @param EntityMetadata
70
+ * @param LoaderOptions[]
71
+ * @param (keyof T)[]
72
+ */
73
+ applyColumnWhere(qb, metadata, options, primaryKeys) {
74
+ const columns = primaryKeys.map((primaryKey) => {
75
+ const columnName = this.resolveColumnName(metadata, String(primaryKey));
76
+ if (!columnName)
77
+ throw new Error(`Loader cannot resolve property "${String(primaryKey)}" to a column of "${metadata.name}".`);
78
+ return {
79
+ primaryKey,
80
+ column: `entity.${this.dataSource.driver.escape(columnName)}`,
81
+ };
82
+ });
83
+ if (columns.length === 1) {
84
+ const [{ primaryKey, column }] = columns;
85
+ qb.andWhere(`${column} IN (:...wmsLoaderValues)`, {
86
+ wmsLoaderValues: options.map(({ __from }) => __from[primaryKey]),
87
+ });
88
+ return;
89
+ }
90
+ qb.andWhere(new Brackets((where) => {
91
+ options.forEach(({ __from }, optionIndex) => {
92
+ const parameters = {};
93
+ const predicate = columns
94
+ .map(({ primaryKey, column }, columnIndex) => {
95
+ const parameter = `wmsLoaderValue_${optionIndex}_${columnIndex}`;
96
+ parameters[parameter] = __from[primaryKey];
97
+ return `${column} = :${parameter}`;
98
+ })
99
+ .join(' AND ');
100
+ where.orWhere(`(${predicate})`, parameters);
101
+ });
102
+ }));
103
+ }
45
104
  /**
46
105
  * query data by collecting primary keys from options with same `__entity`
47
106
  * @param LoaderOptions[]
@@ -52,7 +111,10 @@ let LoaderService = class LoaderService {
52
111
  const repo = this.dataSource.getRepository(__entity);
53
112
  let qb = repo.createQueryBuilder('entity');
54
113
  const primaryKeys = Object.keys(__from);
55
- if (primaryKeys.length > 1)
114
+ const hasVirtualKey = primaryKeys.some((primaryKey) => !repo.metadata.findColumnWithPropertyName(String(primaryKey)));
115
+ if (hasVirtualKey)
116
+ this.applyColumnWhere(qb, repo.metadata, options, primaryKeys);
117
+ else if (primaryKeys.length > 1)
56
118
  qb.setFindOptions({ where: options.map(({ __from }) => __from) });
57
119
  else {
58
120
  const [primaryKey] = primaryKeys;
@@ -6,5 +6,14 @@ import { LocationEntityClass } from '@rytass/wms-module-core';
6
6
  export declare class LocationResolver {
7
7
  mapData({ id }: LocationDto, { loaders }: LoaderType<WarehouseMapEntity>): Promise<MapDataDto | null>;
8
8
  parent({ parentId }: LocationEntityClass, { loaders }: LoaderType<LocationEntityClass>): Promise<LocationEntityClass> | null;
9
- children({ children }: LocationEntityClass): import("@rytass/wms-base-nestjs-module").LocationEntity[];
9
+ /**
10
+ * `children` must never be read off the parent entity: only
11
+ * `LocationQuery.location` and `LocationService.findMany` eager-load the tree
12
+ * relation. Every other producer of a `LocationDto` (stock/loader/change-log/
13
+ * transfer/ship/receive/warehouse-map resolvers, the `parent` field above and
14
+ * the create/update mutations) hands back an entity whose `children` is
15
+ * `undefined`, which breaks the non-nullable list. Always resolve through the
16
+ * request-scoped loader instead.
17
+ */
18
+ children({ id }: LocationEntityClass, { loaders }: LoaderType<LocationEntityClass>): Promise<LocationDto[]>;
10
19
  }
@@ -21,8 +21,21 @@ let LocationResolver = class LocationResolver {
21
21
  __from: { id: parentId },
22
22
  });
23
23
  }
24
- children({ children }) {
25
- return children;
24
+ /**
25
+ * `children` must never be read off the parent entity: only
26
+ * `LocationQuery.location` and `LocationService.findMany` eager-load the tree
27
+ * relation. Every other producer of a `LocationDto` (stock/loader/change-log/
28
+ * transfer/ship/receive/warehouse-map resolvers, the `parent` field above and
29
+ * the create/update mutations) hands back an entity whose `children` is
30
+ * `undefined`, which breaks the non-nullable list. Always resolve through the
31
+ * request-scoped loader instead.
32
+ */
33
+ children({ id }, { loaders }) {
34
+ return loaders.batchMany.load({
35
+ __entity: LocationEntityClass,
36
+ __from: { parentId: id },
37
+ __conditions: (qb) => qb.orderBy('entity."key"', 'ASC'),
38
+ });
26
39
  }
27
40
  };
28
41
  __decorate([
@@ -44,9 +57,10 @@ __decorate([
44
57
  __decorate([
45
58
  ResolveField(() => [LocationDto]),
46
59
  __param(0, Parent()),
60
+ __param(1, Context()),
47
61
  __metadata("design:type", Function),
48
- __metadata("design:paramtypes", [LocationEntityClass]),
49
- __metadata("design:returntype", void 0)
62
+ __metadata("design:paramtypes", [LocationEntityClass, Object]),
63
+ __metadata("design:returntype", Promise)
50
64
  ], LocationResolver.prototype, "children", null);
51
65
  LocationResolver = __decorate([
52
66
  Resolver(() => LocationDto)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rytass/wms-module-graphql",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Rytass WMS Module — GraphQL schema, resolvers and DTOs for warehouse management",
5
5
  "keywords": [
6
6
  "rytass",
@@ -57,7 +57,7 @@
57
57
  "@nestjs/apollo": ">=13.0.0 <13.2.0",
58
58
  "@nestjs/common": "^11.0.0",
59
59
  "@nestjs/graphql": "^13.0.0",
60
- "@rytass/wms-module-core": "^0.3.0",
60
+ "@rytass/wms-module-core": "^0.3.1",
61
61
  "graphql": "^16.0.0",
62
62
  "reflect-metadata": "^0.1.13 || ^0.2.0",
63
63
  "rxjs": "^7.8.0",