Browse Source

feat(server): Improved (type-safe) sort API

Relates to #4
Michael Bromley 7 years ago
parent
commit
3e7ed4aa9b

+ 15 - 1
server/src/api/customer/customer.api.graphql

@@ -1,5 +1,5 @@
 type Query {
-  customers(options: ListOptions): CustomerList!
+  customers(options: CustomerListOptions): CustomerList!
   customer(id: ID!): Customer
 }
 
@@ -14,3 +14,17 @@ type CustomerList implements PaginatedList {
     items: [Customer!]!
     totalItems: Int!
 }
+
+input CustomerListOptions {
+    take: Int
+    skip: Int
+    sort: CustomerSortParameter
+}
+
+input CustomerSortParameter {
+    id: SortOrder
+    firstName: SortOrder
+    lastName: SortOrder
+    phoneNumber: SortOrder
+    emailAddress: SortOrder
+}

+ 15 - 1
server/src/api/product/product.api.graphql

@@ -1,5 +1,5 @@
 type Query {
-    products(languageCode: LanguageCode, options: ListOptions): ProductList!
+    products(languageCode: LanguageCode, options: ProductListOptions): ProductList!
     product(id: ID!, languageCode: LanguageCode): Product
 }
 
@@ -22,3 +22,17 @@ type ProductList implements PaginatedList {
     items: [Product!]!
     totalItems: Int!
 }
+
+input ProductListOptions {
+    take: Int
+    skip: Int
+    sort: ProductSortParameter
+}
+
+input ProductSortParameter {
+    id: SortOrder
+    name: SortOrder
+    slug: SortOrder
+    description: SortOrder
+    image: SortOrder
+}

+ 1 - 1
server/src/common/build-list-query.ts

@@ -13,7 +13,7 @@ import { parseSortParams } from './parse-sort-params';
 export function buildListQuery<T extends VendureEntity>(
     connection: Connection,
     entity: Type<T>,
-    options: ListQueryOptions,
+    options: ListQueryOptions<T>,
     relations?: string[],
 ): SelectQueryBuilder<T> {
     const skip = options.skip;

+ 0 - 11
server/src/common/common-types.graphql

@@ -7,20 +7,9 @@ interface Node {
     id: ID!
 }
 
-input ListOptions {
-    take: Int
-    skip: Int
-    sort: [SortParameter!]
-}
-
 enum SortOrder {
     ASC
     DESC
 }
 
-input SortParameter {
-    field: String!
-    order: SortOrder
-}
-
 scalar JSON

+ 15 - 13
server/src/common/common-types.ts

@@ -1,3 +1,6 @@
+import { VendureEntity } from '../entity/base/base.entity';
+import { LocaleString } from '../locale/locale-types';
+
 /**
  * Creates a type based on T, but with all properties non-optional
  * and readonly.
@@ -12,22 +15,21 @@ export type UnwrappedArray<T extends any[]> = T[number];
 /**
  * Parameters for list queries
  */
-export interface ListQueryOptions {
+export interface ListQueryOptions<T extends VendureEntity> {
     take: number;
     skip: number;
-    sort: SortParameter[];
-    filter: FilterParameter[];
+    sort: SortParameter<T>;
 }
 
-export interface SortParameter {
-    field: string;
-    order: 'asc' | 'desc';
-}
+export type SortOrder = 'ASC' | 'DESC';
 
-export interface FilterParameter {
-    field: string;
-    operator: FilterOperator;
-    value: string | number;
-}
+export type PrimitiveFields<T extends VendureEntity> = {
+    [K in keyof T]: T[K] extends LocaleString | number | string ? K : never
+}[keyof T];
+
+export type SortParameter<T extends VendureEntity> = { [K in PrimitiveFields<T>]?: SortOrder } &
+    CustomFieldSortParameter;
 
-export type FilterOperator = 'gt' | 'gte' | 'lt' | 'lte' | 'eq' | 'contains' | 'startsWith';
+export type CustomFieldSortParameter = {
+    [customField: string]: SortOrder;
+};

+ 22 - 24
server/src/common/parse-sort-params.spec.ts

@@ -14,7 +14,7 @@ describe('parseSortParams()', () => {
         const connection = new MockConnection();
         connection.setColumns(Product, [{ propertyName: 'id' }, { propertyName: 'image' }]);
 
-        const result = parseSortParams(connection as any, Product, []);
+        const result = parseSortParams(connection as any, Product, {});
         expect(result).toEqual({});
     });
 
@@ -22,24 +22,13 @@ describe('parseSortParams()', () => {
         const connection = new MockConnection();
         connection.setColumns(Product, [{ propertyName: 'id' }, { propertyName: 'image' }]);
 
-        const sortParams: SortParameter[] = [{ field: 'id', order: 'asc' }];
-
-        const result = parseSortParams(connection as any, Product, sortParams);
-        expect(result).toEqual({
-            'product.id': 'ASC',
-        });
-    });
-
-    it('defaults the order to "ASC', () => {
-        const connection = new MockConnection();
-        connection.setColumns(Product, [{ propertyName: 'id' }, { propertyName: 'image' }]);
-
-        const sortParams: SortParameter[] = [{ field: 'id' } as any, { field: 'image', order: 'foo' } as any];
+        const sortParams: SortParameter<Product> = {
+            id: 'ASC',
+        };
 
         const result = parseSortParams(connection as any, Product, sortParams);
         expect(result).toEqual({
             'product.id': 'ASC',
-            'product.image': 'ASC',
         });
     });
 
@@ -51,11 +40,11 @@ describe('parseSortParams()', () => {
             { propertyName: 'createdAt' },
         ]);
 
-        const sortParams: SortParameter[] = [
-            { field: 'id', order: 'asc' },
-            { field: 'createdAt', order: 'desc' },
-            { field: 'image', order: 'asc' },
-        ];
+        const sortParams: SortParameter<Product> = {
+            id: 'ASC',
+            createdAt: 'DESC',
+            image: 'ASC',
+        };
 
         const result = parseSortParams(connection as any, Product, sortParams);
         expect(result).toEqual({
@@ -75,7 +64,10 @@ describe('parseSortParams()', () => {
             { propertyName: 'base', relationMetadata: {} as any },
         ]);
 
-        const sortParams: SortParameter[] = [{ field: 'id', order: 'asc' }, { field: 'name', order: 'desc' }];
+        const sortParams: SortParameter<Product> = {
+            id: 'ASC',
+            name: 'DESC',
+        };
 
         const result = parseSortParams(connection as any, Product, sortParams);
         expect(result).toEqual({
@@ -88,7 +80,9 @@ describe('parseSortParams()', () => {
         const connection = new MockConnection();
         connection.setColumns(Product, [{ propertyName: 'id' }, { propertyName: 'infoUrl' }]);
 
-        const sortParams: SortParameter[] = [{ field: 'infoUrl', order: 'asc' }];
+        const sortParams: SortParameter<Product> = {
+            infoUrl: 'ASC',
+        };
 
         const result = parseSortParams(connection as any, Product, sortParams);
         expect(result).toEqual({
@@ -102,7 +96,9 @@ describe('parseSortParams()', () => {
         connection.setRelations(Product, [{ propertyName: 'translations', type: ProductTranslation }]);
         connection.setColumns(ProductTranslation, [{ propertyName: 'id' }, { propertyName: 'shortName' }]);
 
-        const sortParams: SortParameter[] = [{ field: 'shortName', order: 'asc' }];
+        const sortParams: SortParameter<Product> = {
+            shortName: 'ASC',
+        };
 
         const result = parseSortParams(connection as any, Product, sortParams);
         expect(result).toEqual({
@@ -120,7 +116,9 @@ describe('parseSortParams()', () => {
             { propertyName: 'base', relationMetadata: {} as any },
         ]);
 
-        const sortParams: SortParameter[] = [{ field: 'invalid', order: 'asc' }];
+        const sortParams: SortParameter<Product> = {
+            invalid: 'ASC',
+        };
 
         try {
             parseSortParams(connection as any, Product, sortParams);

+ 5 - 7
server/src/common/parse-sort-params.ts

@@ -14,12 +14,12 @@ import { SortParameter } from './common-types';
  * @param entity
  * @param sortParams
  */
-export function parseSortParams(
+export function parseSortParams<T extends VendureEntity>(
     connection: Connection,
-    entity: Type<VendureEntity>,
-    sortParams: SortParameter[] | undefined,
+    entity: Type<T>,
+    sortParams: SortParameter<T> | undefined,
 ): OrderByCondition {
-    if (!sortParams || sortParams.length === 0) {
+    if (!sortParams || Object.keys(sortParams).length === 0) {
         return {};
     }
 
@@ -36,10 +36,8 @@ export function parseSortParams(
 
     const output = {};
 
-    for (const param of sortParams) {
-        const key = param.field;
+    for (const [key, order] of Object.entries(sortParams)) {
         const alias = metadata.name.toLowerCase();
-        const order = param.order && param.order.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
         if (columns.find(c => c.propertyName === key)) {
             output[`${alias}.${key}`] = order;
         } else if (translationColumns.find(c => c.propertyName === key)) {

+ 26 - 0
server/src/entity/__snapshots__/graphql-custom-fields.spec.ts.snap

@@ -14,6 +14,32 @@ type ProductCustomFields {
 "
 `;
 
+exports[`addGraphQLCustomFields() extends a type with SortParameters 1`] = `
+"scalar JSON
+
+type Product {
+  id: ID
+  customFields: ProductCustomFields
+}
+
+type ProductCustomFields {
+  available: Boolean
+  shortName: String
+}
+
+input ProductSortParameter {
+  id: SortOrder
+  available: SortOrder
+  shortName: SortOrder
+}
+
+enum SortOrder {
+  ASC
+  DESC
+}
+"
+`;
+
 exports[`addGraphQLCustomFields() extends a type with a Create input 1`] = `
 "input CreateProductCustomFieldsInput {
   available: Boolean

+ 22 - 0
server/src/entity/graphql-custom-fields.spec.ts

@@ -105,4 +105,26 @@ describe('addGraphQLCustomFields()', () => {
         const result = addGraphQLCustomFields(input, customFieldConfig);
         expect(result).toMatchSnapshot();
     });
+
+    it('extends a type with SortParameters', () => {
+        const input = `
+                    type Product {
+                        id: ID
+                    }
+
+                    input ProductSortParameter {
+                        id: SortOrder
+                    }
+
+                    enum SortOrder {
+                        ASC
+                        DESC
+                    }
+                `;
+        const customFieldConfig: CustomFields = {
+            Product: [{ name: 'available', type: 'boolean' }, { name: 'shortName', type: 'localeString' }],
+        };
+        const result = addGraphQLCustomFields(input, customFieldConfig);
+        expect(result).toMatchSnapshot();
+    });
 });

+ 14 - 2
server/src/entity/graphql-custom-fields.ts

@@ -97,6 +97,14 @@ export function addGraphQLCustomFields(typeDefs: string, customFieldConfig: Cust
             }
         }
 
+        if (customEntityFields.length && schema.getType(`${entityName}SortParameter`)) {
+            customFieldTypeDefs += `
+                    extend input ${entityName}SortParameter {
+                         ${mapToFields(customEntityFields, () => 'SortOrder')}
+                    }
+                `;
+        }
+
         if (localeStringFields && schema.getType(`${entityName}TranslationInput`)) {
             if (localeStringFields.length) {
                 customFieldTypeDefs += `
@@ -126,8 +134,12 @@ type GraphQLFieldType = 'String' | 'Int' | 'Float' | 'Boolean' | 'ID';
 /**
  * Maps an array of CustomFieldConfig objects into a string of SDL fields.
  */
-function mapToFields(fieldDefs: CustomFieldConfig[]): string {
-    return fieldDefs.map(field => `${field.name}: ${getGraphQlType(field.type)}`).join('\n');
+function mapToFields(
+    fieldDefs: CustomFieldConfig[],
+    typeFn?: (fieldType: CustomFieldType) => string,
+): string {
+    const getType = typeFn || getGraphQlType;
+    return fieldDefs.map(field => `${field.name}: ${getType(field.type)}`).join('\n');
 }
 
 function getGraphQlType(type: CustomFieldType): GraphQLFieldType {

+ 1 - 1
server/src/service/customer.service.ts

@@ -21,7 +21,7 @@ export class CustomerService {
         private passwordService: PasswordService,
     ) {}
 
-    findAll(options: ListQueryOptions): Promise<PaginatedList<Customer>> {
+    findAll(options: ListQueryOptions<Customer>): Promise<PaginatedList<Customer>> {
         return buildListQuery(this.connection, Customer, options)
             .getManyAndCount()
             .then(([items, totalItems]) => ({ items, totalItems }));

+ 4 - 1
server/src/service/product.service.ts

@@ -25,7 +25,10 @@ export class ProductService {
         private translationUpdaterService: TranslationUpdaterService,
     ) {}
 
-    findAll(lang: LanguageCode, options: ListQueryOptions): Promise<PaginatedList<Translated<Product>>> {
+    findAll(
+        lang: LanguageCode,
+        options: ListQueryOptions<Product>,
+    ): Promise<PaginatedList<Translated<Product>>> {
         const relations = ['variants', 'optionGroups', 'variants.options'];
 
         return buildListQuery(this.connection, Product, options, relations)