ソースを参照

feat(server): Implement global settings API

Relates to #59
Michael Bromley 7 年 前
コミット
a362adc212

ファイルの差分が大きいため隠しています
+ 0 - 0
schema.json


+ 3 - 1
server/dev-config.ts

@@ -34,7 +34,9 @@ export const devConfig: VendureConfig = {
     paymentOptions: {
         paymentMethodHandlers: [examplePaymentHandler],
     },
-    customFields: {},
+    customFields: {
+        GlobalSettings: [{ name: 'royalMailId', type: 'string' }],
+    },
     emailOptions: {
         emailTemplatePath: path.join(__dirname, 'src/email/templates'),
         emailTypes: defaultEmailTypes,

+ 2 - 0
server/src/api/api.module.ts

@@ -23,6 +23,7 @@ import { CountryResolver } from './resolvers/country.resolver';
 import { CustomerGroupResolver } from './resolvers/customer-group.resolver';
 import { CustomerResolver } from './resolvers/customer.resolver';
 import { FacetResolver } from './resolvers/facet.resolver';
+import { GlobalSettingsResolver } from './resolvers/global-settings.resolver';
 import { ImportResolver } from './resolvers/import.resolver';
 import { OrderResolver } from './resolvers/order.resolver';
 import { PaymentMethodResolver } from './resolvers/payment-method.resolver';
@@ -60,6 +61,7 @@ const resolvers = [
     TaxCategoryResolver,
     TaxRateResolver,
     ZoneResolver,
+    GlobalSettingsResolver,
 ];
 
 /**

+ 22 - 0
server/src/api/resolvers/global-settings.resolver.ts

@@ -0,0 +1,22 @@
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+
+import { Permission, UpdateGlobalSettingsMutationArgs } from '../../../../shared/generated-types';
+import { GlobalSettingsService } from '../../service/services/global-settings.service';
+import { Allow } from '../decorators/allow.decorator';
+
+@Resolver('GlobalSettings')
+export class GlobalSettingsResolver {
+    constructor(private globalSettingsService: GlobalSettingsService) {}
+
+    @Query()
+    @Allow(Permission.Authenticated)
+    async globalSettings() {
+        return this.globalSettingsService.getSettings();
+    }
+
+    @Mutation()
+    @Allow(Permission.UpdateSettings)
+    async updateGlobalSettings(@Args() args: UpdateGlobalSettingsMutationArgs) {
+        return this.globalSettingsService.updateSettings(args.input);
+    }
+}

+ 7 - 0
server/src/api/types/global-settings.api.graphql

@@ -0,0 +1,7 @@
+type Query {
+    globalSettings: GlobalSettings!
+}
+
+type Mutation {
+    updateGlobalSettings(input: UpdateGlobalSettingsInput!): GlobalSettings!
+}

+ 3 - 0
server/src/entity/custom-entity-fields.ts

@@ -40,6 +40,8 @@ export class CustomProductVariantFields {}
 export class CustomProductVariantFieldsTranslation {}
 @Entity()
 export class CustomUserFields {}
+@Entity()
+export class CustomGlobalSettingsFields {}
 
 /**
  * Dynamically add columns to the custom field entity based on the CustomFields config.
@@ -168,6 +170,7 @@ export function registerCustomEntityFields(config: VendureConfig) {
     registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFields);
     registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFieldsTranslation, true);
     registerCustomFieldsForEntity(config, 'User', CustomUserFields);
+    registerCustomFieldsForEntity(config, 'GlobalSettings', CustomGlobalSettingsFields);
 }
 
 /**

+ 2 - 0
server/src/entity/entities.ts

@@ -10,6 +10,7 @@ import { FacetValueTranslation } from './facet-value/facet-value-translation.ent
 import { FacetValue } from './facet-value/facet-value.entity';
 import { FacetTranslation } from './facet/facet-translation.entity';
 import { Facet } from './facet/facet.entity';
+import { GlobalSettings } from './global-settings/global-settings.entity';
 import { OrderItem } from './order-item/order-item.entity';
 import { OrderLine } from './order-line/order-line.entity';
 import { Order } from './order/order.entity';
@@ -55,6 +56,7 @@ export const coreEntitiesMap = {
     FacetTranslation,
     FacetValue,
     FacetValueTranslation,
+    GlobalSettings,
     Order,
     OrderLine,
     OrderItem,

+ 19 - 0
server/src/entity/global-settings/global-settings.entity.ts

@@ -0,0 +1,19 @@
+import { Column, Entity } from 'typeorm';
+
+import { VendureEntity } from '..';
+import { LanguageCode } from '../../../../shared/generated-types';
+import { DeepPartial, HasCustomFields } from '../../../../shared/shared-types';
+import { CustomGlobalSettingsFields } from '../custom-entity-fields';
+
+@Entity()
+export class GlobalSettings extends VendureEntity implements HasCustomFields {
+    constructor(input?: DeepPartial<GlobalSettings>) {
+        super(input);
+    }
+
+    @Column('simple-array')
+    availableLanguages: LanguageCode[];
+
+    @Column(type => CustomGlobalSettingsFields)
+    customFields: CustomGlobalSettingsFields;
+}

+ 10 - 0
server/src/entity/global-settings/global-settings.graphql

@@ -0,0 +1,10 @@
+type GlobalSettings {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    availableLanguages: [LanguageCode!]!
+}
+
+input UpdateGlobalSettingsInput {
+    availableLanguages: [LanguageCode!]
+}

+ 1 - 0
server/src/entity/index.ts

@@ -11,6 +11,7 @@ export * from './facet/facet.entity';
 export * from './facet/facet-translation.entity';
 export * from './facet-value/facet-value.entity';
 export * from './facet-value/facet-value-translation.entity';
+export * from './global-settings/global-settings.entity';
 export * from './order/order.entity';
 export * from './order-item/order-item.entity';
 export * from './order-line/order-line.entity';

+ 48 - 0
server/src/service/helpers/utils/patch-entity.spec.ts

@@ -0,0 +1,48 @@
+import { patchEntity } from './patch-entity';
+
+describe('patchEntity()', () => {
+    it('updates non-null values', () => {
+        const entity: any = {
+            foo: 'foo',
+            bar: 'bar',
+            baz: 'baz',
+        };
+
+        const result = patchEntity(entity, { bar: 'bar2', baz: null });
+        expect(result).toEqual({
+            foo: 'foo',
+            bar: 'bar2',
+            baz: 'baz',
+        });
+    });
+
+    it('does not update id field', () => {
+        const entity: any = {
+            id: 123,
+            bar: 'bar',
+        };
+
+        const result = patchEntity(entity, { id: 456 });
+        expect(result).toEqual({
+            id: 123,
+            bar: 'bar',
+        });
+    });
+
+    it('updates individual customFields', () => {
+        const entity: any = {
+            customFields: {
+                cf1: 'cf1',
+                cf2: 'cf2',
+            },
+        };
+
+        const result = patchEntity(entity, { customFields: { cf2: 'foo' } });
+        expect(result).toEqual({
+            customFields: {
+                cf1: 'cf1',
+                cf2: 'foo',
+            },
+        });
+    });
+});

+ 3 - 1
server/src/service/helpers/utils/patch-entity.ts

@@ -9,7 +9,9 @@ export type InputPatch<T> = { [K in keyof T]?: T[K] | null };
 export function patchEntity<T extends VendureEntity, I extends InputPatch<T>>(entity: T, input: I): T {
     for (const key of Object.keys(entity)) {
         const value = input[key];
-        if (value != null && key !== 'id') {
+        if (key === 'customFields') {
+            patchEntity(entity[key], value);
+        } else if (value != null && key !== 'id') {
             entity[key] = value;
         }
     }

+ 4 - 0
server/src/service/service.module.ts

@@ -24,6 +24,7 @@ import { CustomerGroupService } from './services/customer-group.service';
 import { CustomerService } from './services/customer.service';
 import { FacetValueService } from './services/facet-value.service';
 import { FacetService } from './services/facet.service';
+import { GlobalSettingsService } from './services/global-settings.service';
 import { OrderService } from './services/order.service';
 import { PaymentMethodService } from './services/payment-method.service';
 import { ProductCategoryService } from './services/product-category.service';
@@ -50,6 +51,7 @@ const exportedProviders = [
     CustomerService,
     FacetService,
     FacetValueService,
+    GlobalSettingsService,
     OrderService,
     PaymentMethodService,
     ProductCategoryService,
@@ -97,6 +99,7 @@ export class ServiceModule implements OnModuleInit {
         private taxRateService: TaxRateService,
         private shippingMethodService: ShippingMethodService,
         private paymentMethodService: PaymentMethodService,
+        private globalSettingsService: GlobalSettingsService,
     ) {}
 
     async onModuleInit() {
@@ -106,6 +109,7 @@ export class ServiceModule implements OnModuleInit {
         // methods below, we can e.g. guarantee that the default channel exists
         // (channelService.initChannels()) before we try to create any roles (which assume that
         // there is a default Channel to work with.
+        await this.globalSettingsService.initGlobalSettings();
         await this.channelService.initChannels();
         await this.roleService.initRoles();
         await this.administratorService.initAdministrators();

+ 42 - 0
server/src/service/services/global-settings.service.ts

@@ -0,0 +1,42 @@
+import { Injectable } from '@nestjs/common';
+import { InjectConnection } from '@nestjs/typeorm';
+import { Connection } from 'typeorm';
+
+import { UpdateGlobalSettingsInput } from '../../../../shared/generated-types';
+import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { InternalServerError } from '../../common/error/errors';
+import { GlobalSettings } from '../../entity';
+import { patchEntity } from '../helpers/utils/patch-entity';
+
+@Injectable()
+export class GlobalSettingsService {
+    constructor(@InjectConnection() private connection: Connection) {}
+
+    /**
+     * Ensure there is a global settings row in the database.
+     */
+    async initGlobalSettings() {
+        try {
+            await this.getSettings();
+        } catch (err) {
+            const settings = new GlobalSettings({
+                availableLanguages: [DEFAULT_LANGUAGE_CODE],
+            });
+            await this.connection.getRepository(GlobalSettings).save(settings);
+        }
+    }
+
+    async getSettings(): Promise<GlobalSettings> {
+        const settings = await this.connection.getRepository(GlobalSettings).findOne();
+        if (!settings) {
+            throw new InternalServerError(`error.global-settings-not-found`);
+        }
+        return settings;
+    }
+
+    async updateSettings(input: UpdateGlobalSettingsInput): Promise<GlobalSettings> {
+        const settings = await this.getSettings();
+        patchEntity(settings, input);
+        return this.connection.getRepository(GlobalSettings).save(settings);
+    }
+}

+ 79 - 0
shared/generated-types.ts

@@ -58,6 +58,7 @@ export interface Query {
     activeCustomer?: Customer | null;
     facets: FacetList;
     facet?: Facet | null;
+    globalSettings: GlobalSettings;
     order?: Order | null;
     activeOrder?: Order | null;
     orderByCode?: Order | null;
@@ -288,6 +289,18 @@ export interface FacetTranslation {
     name: string;
 }
 
+export interface GlobalSettings {
+    id: string;
+    createdAt: DateTime;
+    updatedAt: DateTime;
+    availableLanguages: LanguageCode[];
+    customFields?: GlobalSettingsCustomFields | null;
+}
+
+export interface GlobalSettingsCustomFields {
+    royalMailId?: string | null;
+}
+
 export interface Order extends Node {
     id: string;
     createdAt: DateTime;
@@ -671,6 +684,7 @@ export interface Mutation {
     updateFacet: Facet;
     createFacetValues: FacetValue[];
     updateFacetValues: FacetValue[];
+    updateGlobalSettings: GlobalSettings;
     importProducts?: ImportInfo | null;
     addItemToOrder?: Order | null;
     removeItemFromOrder?: Order | null;
@@ -1223,6 +1237,15 @@ export interface UpdateFacetValueInput {
     customFields?: Json | null;
 }
 
+export interface UpdateGlobalSettingsInput {
+    availableLanguages?: LanguageCode[] | null;
+    customFields?: UpdateGlobalSettingsCustomFieldsInput | null;
+}
+
+export interface UpdateGlobalSettingsCustomFieldsInput {
+    royalMailId?: string | null;
+}
+
 export interface PaymentInput {
     method: string;
     metadata: Json;
@@ -1653,6 +1676,9 @@ export interface CreateFacetValuesMutationArgs {
 export interface UpdateFacetValuesMutationArgs {
     input: UpdateFacetValueInput[];
 }
+export interface UpdateGlobalSettingsMutationArgs {
+    input: UpdateGlobalSettingsInput;
+}
 export interface ImportProductsMutationArgs {
     csvFile: Upload;
 }
@@ -2191,6 +2217,7 @@ export namespace QueryResolvers {
         activeCustomer?: ActiveCustomerResolver<Customer | null, any, Context>;
         facets?: FacetsResolver<FacetList, any, Context>;
         facet?: FacetResolver<Facet | null, any, Context>;
+        globalSettings?: GlobalSettingsResolver<GlobalSettings, any, Context>;
         order?: OrderResolver<Order | null, any, Context>;
         activeOrder?: ActiveOrderResolver<Order | null, any, Context>;
         orderByCode?: OrderByCodeResolver<Order | null, any, Context>;
@@ -2380,6 +2407,11 @@ export namespace QueryResolvers {
         languageCode?: LanguageCode | null;
     }
 
+    export type GlobalSettingsResolver<R = GlobalSettings, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
     export type OrderResolver<R = Order | null, Parent = any, Context = any> = Resolver<
         R,
         Parent,
@@ -3187,6 +3219,42 @@ export namespace FacetTranslationResolvers {
     export type NameResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
 }
 
+export namespace GlobalSettingsResolvers {
+    export interface Resolvers<Context = any> {
+        id?: IdResolver<string, any, Context>;
+        createdAt?: CreatedAtResolver<DateTime, any, Context>;
+        updatedAt?: UpdatedAtResolver<DateTime, any, Context>;
+        availableLanguages?: AvailableLanguagesResolver<LanguageCode[], any, Context>;
+        customFields?: CustomFieldsResolver<GlobalSettingsCustomFields | null, any, Context>;
+    }
+
+    export type IdResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type CreatedAtResolver<R = DateTime, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type UpdatedAtResolver<R = DateTime, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type AvailableLanguagesResolver<R = LanguageCode[], Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
+    export type CustomFieldsResolver<
+        R = GlobalSettingsCustomFields | null,
+        Parent = any,
+        Context = any
+    > = Resolver<R, Parent, Context>;
+}
+
+export namespace GlobalSettingsCustomFieldsResolvers {
+    export interface Resolvers<Context = any> {
+        royalMailId?: RoyalMailIdResolver<string | null, any, Context>;
+    }
+
+    export type RoyalMailIdResolver<R = string | null, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
+}
+
 export namespace OrderResolvers {
     export interface Resolvers<Context = any> {
         id?: IdResolver<string, any, Context>;
@@ -4288,6 +4356,7 @@ export namespace MutationResolvers {
         updateFacet?: UpdateFacetResolver<Facet, any, Context>;
         createFacetValues?: CreateFacetValuesResolver<FacetValue[], any, Context>;
         updateFacetValues?: UpdateFacetValuesResolver<FacetValue[], any, Context>;
+        updateGlobalSettings?: UpdateGlobalSettingsResolver<GlobalSettings, any, Context>;
         importProducts?: ImportProductsResolver<ImportInfo | null, any, Context>;
         addItemToOrder?: AddItemToOrderResolver<Order | null, any, Context>;
         removeItemFromOrder?: RemoveItemFromOrderResolver<Order | null, any, Context>;
@@ -4580,6 +4649,16 @@ export namespace MutationResolvers {
         input: UpdateFacetValueInput[];
     }
 
+    export type UpdateGlobalSettingsResolver<R = GlobalSettings, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context,
+        UpdateGlobalSettingsArgs
+    >;
+    export interface UpdateGlobalSettingsArgs {
+        input: UpdateGlobalSettingsInput;
+    }
+
     export type ImportProductsResolver<R = ImportInfo | null, Parent = any, Context = any> = Resolver<
         R,
         Parent,

+ 1 - 0
shared/shared-types.ts

@@ -45,6 +45,7 @@ export interface CustomFields {
     Customer?: CustomFieldConfig[];
     Facet?: CustomFieldConfig[];
     FacetValue?: CustomFieldConfig[];
+    GlobalSettings?: CustomFieldConfig[];
     Product?: CustomFieldConfig[];
     ProductCategory?: CustomFieldConfig[];
     ProductOption?: CustomFieldConfig[];

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません