@rytass/wms-module-graphql 0.5.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.
@@ -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.5.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.2.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",