Просмотр исходного кода

feat(core): Implement query for scheduled tasks

Relates to #1425
Michael Bromley 9 месяцев назад
Родитель
Сommit
3c657fbb5d
23 измененных файлов с 2867 добавлено и 2600 удалено
  1. 12 0
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 11 0
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  3. 674 653
      packages/common/src/generated-shop-types.ts
  4. 12 0
      packages/common/src/generated-types.ts
  5. 11 0
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  6. 645 624
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  7. 32 0
      packages/core/e2e/graphql/shop-definitions.ts
  8. 6 17
      packages/core/e2e/payment-method.e2e-spec.ts
  9. 1 16
      packages/core/e2e/shipping-method.e2e-spec.ts
  10. 1 0
      packages/core/package.json
  11. 4 0
      packages/core/src/api/api-internal-modules.ts
  12. 16 0
      packages/core/src/api/resolvers/admin/scheduled-task.resolver.ts
  13. 1 1
      packages/core/src/api/schema/admin-api/province.api.graphql
  14. 13 0
      packages/core/src/api/schema/admin-api/scheduled-task.api.graphql
  15. 31 2
      packages/core/src/plugin/default-scheduler-plugin/default-scheduler-strategy.ts
  16. 1 0
      packages/core/src/plugin/default-scheduler-plugin/default-scheduler.plugin.ts
  17. 10 0
      packages/core/src/scheduler/scheduler-strategy.ts
  18. 1 0
      packages/core/src/scheduler/scheduler.module.ts
  19. 36 2
      packages/core/src/scheduler/scheduler.service.ts
  20. 11 0
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  21. 11 0
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  22. 649 628
      packages/payments-plugin/e2e/graphql/generated-shop-types.ts
  23. 678 657
      packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts

+ 12 - 0
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -5223,6 +5223,7 @@ export type Query = {
   provinces: ProvinceList;
   role?: Maybe<Role>;
   roles: RoleList;
+  scheduledTasks: Array<ScheduledTask>;
   search: SearchResponse;
   seller?: Maybe<Seller>;
   sellers: SellerList;
@@ -5784,6 +5785,17 @@ export type Sale = Node & StockMovement & {
   updatedAt: Scalars['DateTime']['output'];
 };
 
+export type ScheduledTask = {
+  __typename?: 'ScheduledTask';
+  id: Scalars['String']['output'];
+  isRunning: Scalars['Boolean']['output'];
+  lastExecutedAt?: Maybe<Scalars['DateTime']['output']>;
+  lastResult?: Maybe<Scalars['JSON']['output']>;
+  nextExecutionAt?: Maybe<Scalars['DateTime']['output']>;
+  schedule: Scalars['String']['output'];
+  scheduleDescription: Scalars['String']['output'];
+};
+
 export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']['input']>;
   collectionSlug?: InputMaybe<Scalars['String']['input']>;

+ 11 - 0
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -4886,6 +4886,7 @@ export type Query = {
     provinces: ProvinceList;
     role?: Maybe<Role>;
     roles: RoleList;
+    scheduledTasks: Array<ScheduledTask>;
     search: SearchResponse;
     seller?: Maybe<Seller>;
     sellers: SellerList;
@@ -5388,6 +5389,16 @@ export type Sale = Node &
         updatedAt: Scalars['DateTime']['output'];
     };
 
+export type ScheduledTask = {
+    id: Scalars['String']['output'];
+    isRunning: Scalars['Boolean']['output'];
+    lastExecutedAt?: Maybe<Scalars['DateTime']['output']>;
+    lastResult?: Maybe<Scalars['JSON']['output']>;
+    nextExecutionAt?: Maybe<Scalars['DateTime']['output']>;
+    schedule: Scalars['String']['output'];
+    scheduleDescription: Scalars['String']['output'];
+};
+
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;

Разница между файлами не показана из-за своего большого размера
+ 674 - 653
packages/common/src/generated-shop-types.ts


+ 12 - 0
packages/common/src/generated-types.ts

@@ -5147,6 +5147,7 @@ export type Query = {
   provinces: ProvinceList;
   role?: Maybe<Role>;
   roles: RoleList;
+  scheduledTasks: Array<ScheduledTask>;
   search: SearchResponse;
   seller?: Maybe<Seller>;
   sellers: SellerList;
@@ -5706,6 +5707,17 @@ export type Sale = Node & StockMovement & {
   updatedAt: Scalars['DateTime']['output'];
 };
 
+export type ScheduledTask = {
+  __typename?: 'ScheduledTask';
+  id: Scalars['String']['output'];
+  isRunning: Scalars['Boolean']['output'];
+  lastExecutedAt?: Maybe<Scalars['DateTime']['output']>;
+  lastResult?: Maybe<Scalars['JSON']['output']>;
+  nextExecutionAt?: Maybe<Scalars['DateTime']['output']>;
+  schedule: Scalars['String']['output'];
+  scheduleDescription: Scalars['String']['output'];
+};
+
 export type SearchInput = {
   collectionId?: InputMaybe<Scalars['ID']['input']>;
   collectionSlug?: InputMaybe<Scalars['String']['input']>;

+ 11 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -4886,6 +4886,7 @@ export type Query = {
     provinces: ProvinceList;
     role?: Maybe<Role>;
     roles: RoleList;
+    scheduledTasks: Array<ScheduledTask>;
     search: SearchResponse;
     seller?: Maybe<Seller>;
     sellers: SellerList;
@@ -5388,6 +5389,16 @@ export type Sale = Node &
         updatedAt: Scalars['DateTime']['output'];
     };
 
+export type ScheduledTask = {
+    id: Scalars['String']['output'];
+    isRunning: Scalars['Boolean']['output'];
+    lastExecutedAt?: Maybe<Scalars['DateTime']['output']>;
+    lastResult?: Maybe<Scalars['JSON']['output']>;
+    nextExecutionAt?: Maybe<Scalars['DateTime']['output']>;
+    schedule: Scalars['String']['output'];
+    scheduleDescription: Scalars['String']['output'];
+};
+
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;

Разница между файлами не показана из-за своего большого размера
+ 645 - 624
packages/core/e2e/graphql/generated-e2e-shop-types.ts


+ 32 - 0
packages/core/e2e/graphql/shop-definitions.ts

@@ -834,3 +834,35 @@ export const GET_ACTIVE_CUSTOMER_WITH_ORDERS_PRODUCT_PRICE = gql`
         }
     }
 `;
+
+export const ACTIVE_PAYMENT_METHODS_QUERY = gql`
+    query ActivePaymentMethods {
+        activePaymentMethods {
+            id
+            code
+            name
+            description
+            translations {
+                languageCode
+                name
+                description
+            }
+        }
+    }
+`;
+
+export const GET_ACTIVE_SHIPPING_METHODS = gql`
+    query GetActiveShippingMethods {
+        activeShippingMethods {
+            id
+            code
+            name
+            description
+            translations {
+                languageCode
+                name
+                description
+            }
+        }
+    }
+`;

+ 6 - 17
packages/core/e2e/payment-method.e2e-spec.ts

@@ -24,7 +24,12 @@ import * as Codegen from './graphql/generated-e2e-admin-types';
 import { ErrorCode } from './graphql/generated-e2e-shop-types';
 import * as CodegenShop from './graphql/generated-e2e-shop-types';
 import { CREATE_CHANNEL } from './graphql/shared-definitions';
-import { ADD_ITEM_TO_ORDER, ADD_PAYMENT, GET_ELIGIBLE_PAYMENT_METHODS } from './graphql/shop-definitions';
+import {
+    ACTIVE_PAYMENT_METHODS_QUERY,
+    ADD_ITEM_TO_ORDER,
+    ADD_PAYMENT,
+    GET_ELIGIBLE_PAYMENT_METHODS,
+} from './graphql/shop-definitions';
 import { proceedToArrangingPayment } from './utils/test-order-utils';
 
 const checkerSpy = vi.fn();
@@ -697,19 +702,3 @@ export const DELETE_PAYMENT_METHOD = gql`
         }
     }
 `;
-
-const ACTIVE_PAYMENT_METHODS_QUERY = gql`
-    query ActivePaymentMethods {
-        activePaymentMethods {
-            id
-            code
-            name
-            description
-            translations {
-                languageCode
-                name
-                description
-            }
-        }
-    }
-`;

+ 1 - 16
packages/core/e2e/shipping-method.e2e-spec.ts

@@ -22,6 +22,7 @@ import {
     GET_SHIPPING_METHOD_LIST,
     UPDATE_SHIPPING_METHOD,
 } from './graphql/shared-definitions';
+import { GET_ACTIVE_SHIPPING_METHODS } from './graphql/shop-definitions';
 
 const TEST_METADATA = {
     foo: 'bar',
@@ -581,19 +582,3 @@ export const TEST_ELIGIBLE_SHIPPING_METHODS = gql`
         }
     }
 `;
-
-const GET_ACTIVE_SHIPPING_METHODS = gql`
-    query GetActiveShippingMethods {
-        activeShippingMethods {
-            id
-            code
-            name
-            description
-            translations {
-                languageCode
-                name
-                description
-            }
-        }
-    }
-`;

+ 1 - 0
packages/core/package.json

@@ -55,6 +55,7 @@
         "cookie-session": "^2.1.0",
         "cron-time-generator": "^2.0.3",
         "croner": "^9.0.0",
+        "cronstrue": "^2.57.0",
         "csv-parse": "^5.6.0",
         "express": "^5.0.1",
         "fs-extra": "^11.2.0",

+ 4 - 0
packages/core/src/api/api-internal-modules.ts

@@ -6,6 +6,7 @@ import { ConnectionModule } from '../connection/connection.module';
 import { DataImportModule } from '../data-import/data-import.module';
 import { JobQueueModule } from '../job-queue/job-queue.module';
 import { createDynamicGraphQlModulesForPlugins } from '../plugin/dynamic-plugin-api.module';
+import { SchedulerModule } from '../scheduler/scheduler.module';
 import { ServiceModule } from '../service/service.module';
 
 import { ConfigurableOperationCodec } from './common/configurable-operation-codec';
@@ -31,6 +32,7 @@ import { ProductOptionResolver } from './resolvers/admin/product-option.resolver
 import { ProductResolver } from './resolvers/admin/product.resolver';
 import { PromotionResolver } from './resolvers/admin/promotion.resolver';
 import { RoleResolver } from './resolvers/admin/role.resolver';
+import { ScheduledTaskResolver } from './resolvers/admin/scheduled-task.resolver';
 import { SearchResolver } from './resolvers/admin/search.resolver';
 import { SellerResolver } from './resolvers/admin/seller.resolver';
 import { ShippingMethodResolver } from './resolvers/admin/shipping-method.resolver';
@@ -112,6 +114,7 @@ const adminResolvers = [
     PromotionResolver,
     RoleResolver,
     SearchResolver,
+    ScheduledTaskResolver,
     ShippingMethodResolver,
     StockLocationResolver,
     TagResolver,
@@ -197,6 +200,7 @@ export class ApiSharedModule {}
     imports: [
         ApiSharedModule,
         JobQueueModule,
+        SchedulerModule,
         DataImportModule,
         ...createDynamicGraphQlModulesForPlugins('admin'),
     ],

+ 16 - 0
packages/core/src/api/resolvers/admin/scheduled-task.resolver.ts

@@ -0,0 +1,16 @@
+import { Query, Resolver } from '@nestjs/graphql';
+import { Permission } from '@vendure/common/lib/generated-types';
+
+import { SchedulerService } from '../../../scheduler/scheduler.service';
+import { Allow } from '../../decorators/allow.decorator';
+
+@Resolver()
+export class ScheduledTaskResolver {
+    constructor(private readonly schedulerService: SchedulerService) {}
+
+    @Query()
+    @Allow(Permission.ReadSettings, Permission.ReadSystem)
+    scheduledTasks() {
+        return this.schedulerService.getTaskList();
+    }
+}

+ 1 - 1
packages/core/src/api/schema/admin-api/province.api.graphql

@@ -19,7 +19,7 @@ input ProvinceTranslationInput {
     languageCode: LanguageCode!
     name: String
 }
-
+ 
 input CreateProvinceInput {
     code: String!
     translations: [ProvinceTranslationInput!]!

+ 13 - 0
packages/core/src/api/schema/admin-api/scheduled-task.api.graphql

@@ -0,0 +1,13 @@
+type Query {
+    scheduledTasks: [ScheduledTask!]!
+}
+
+type ScheduledTask {
+    id: String!
+    schedule: String!
+    scheduleDescription: String!
+    lastExecutedAt: DateTime
+    nextExecutionAt: DateTime
+    isRunning: Boolean!
+    lastResult: JSON
+}

+ 31 - 2
packages/core/src/plugin/default-scheduler-plugin/default-scheduler-strategy.ts

@@ -6,7 +6,7 @@ import { Logger } from '../../config/logger/vendure-logger';
 import { TransactionalConnection } from '../../connection';
 import { ProcessContext } from '../../process-context';
 import { ScheduledTask } from '../../scheduler/scheduled-task';
-import { SchedulerStrategy } from '../../scheduler/scheduler-strategy';
+import { SchedulerStrategy, TaskReport } from '../../scheduler/scheduler-strategy';
 
 import { DEFAULT_SCHEDULER_PLUGIN_OPTIONS } from './constants';
 import { ScheduledTaskRecord } from './scheduled-task-record.entity';
@@ -68,7 +68,7 @@ export class DefaultSchedulerStrategy implements SchedulerStrategy {
                     {
                         lastExecutedAt: new Date(),
                         lockedAt: null,
-                        lastResult: result,
+                        lastResult: result ?? '',
                     },
                 );
             } catch (error) {
@@ -90,6 +90,35 @@ export class DefaultSchedulerStrategy implements SchedulerStrategy {
         };
     }
 
+    getTasks(): Promise<TaskReport[]> {
+        return this.connection.rawConnection
+            .getRepository(ScheduledTaskRecord)
+            .createQueryBuilder('task')
+            .getMany()
+            .then(tasks => {
+                return tasks.map(task => this.entityToReport(task));
+            });
+    }
+
+    getTask(id: string): Promise<TaskReport | undefined> {
+        return this.connection.rawConnection
+            .getRepository(ScheduledTaskRecord)
+            .createQueryBuilder('task')
+            .where('task.taskId = :id', { id })
+            .getOne()
+            .then(task => (task ? this.entityToReport(task) : undefined));
+    }
+
+    private entityToReport(task: ScheduledTaskRecord): TaskReport {
+        return {
+            id: task.taskId,
+            lastExecutedAt: task.lastExecutedAt,
+            isRunning: task.lockedAt !== null,
+            lastResult: task.lastResult,
+            enabled: task.enabled,
+        };
+    }
+
     private async ensureTaskIsRegistered(task: ScheduledTask) {
         if (!this.tasks.get(task.id)?.isRegistered) {
             await this.connection.rawConnection

+ 1 - 0
packages/core/src/plugin/default-scheduler-plugin/default-scheduler.plugin.ts

@@ -19,6 +19,7 @@ import { DefaultSchedulerPluginOptions } from './types';
             useValue: DefaultSchedulerPlugin.options,
         },
     ],
+    compatibility: '>0.0.0',
 })
 export class DefaultSchedulerPlugin {
     static options: DefaultSchedulerPluginOptions = {

+ 10 - 0
packages/core/src/scheduler/scheduler-strategy.ts

@@ -4,6 +4,16 @@ import { InjectableStrategy } from '../common';
 
 import { ScheduledTask } from './scheduled-task';
 
+export interface TaskReport {
+    id: string;
+    lastExecutedAt: Date | null;
+    isRunning: boolean;
+    lastResult: any;
+    enabled: boolean;
+}
+
 export interface SchedulerStrategy extends InjectableStrategy {
     executeTask(task: ScheduledTask): (job: Cron) => Promise<any> | any;
+    getTasks(): Promise<TaskReport[]>;
+    getTask(id: string): Promise<TaskReport | undefined>;
 }

+ 1 - 0
packages/core/src/scheduler/scheduler.module.ts

@@ -7,5 +7,6 @@ import { SchedulerService } from './scheduler.service';
 @Module({
     imports: [ConfigModule],
     providers: [SchedulerService],
+    exports: [SchedulerService],
 })
 export class SchedulerModule {}

+ 36 - 2
packages/core/src/scheduler/scheduler.service.ts

@@ -1,6 +1,7 @@
 import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
 import CronTime from 'cron-time-generator';
 import { Cron } from 'croner';
+import cronstrue from 'cronstrue';
 
 import { ConfigService } from '../config/config.service';
 import { Logger } from '../config/logger/vendure-logger';
@@ -8,9 +9,19 @@ import { Logger } from '../config/logger/vendure-logger';
 import { NoopSchedulerStrategy } from './noop-scheduler-strategy';
 import { ScheduledTask } from './scheduled-task';
 
+export interface TaskInfo {
+    id: string;
+    schedule: string;
+    scheduleDescription: string;
+    lastExecutedAt: Date | null;
+    nextExecutionAt: Date | null;
+    isRunning: boolean;
+    lastResult: any;
+}
+
 @Injectable()
 export class SchedulerService implements OnApplicationBootstrap {
-    private jobs: Cron[] = [];
+    private jobs: Map<string, { task: ScheduledTask; job: Cron }> = new Map();
     constructor(private configService: ConfigService) {}
 
     onApplicationBootstrap() {
@@ -26,10 +37,33 @@ export class SchedulerService implements OnApplicationBootstrap {
 
         for (const task of scheduledTasks) {
             const job = this.createCronJob(task);
-            this.jobs.push(job);
+            this.jobs.set(task.id, { task, job });
         }
     }
 
+    getTaskList(): Promise<TaskInfo[]> {
+        return this.configService.schedulerOptions.schedulerStrategy.getTasks().then(tasks =>
+            tasks
+                .map(task => {
+                    const job = this.jobs.get(task.id)?.job;
+                    if (!job) {
+                        return;
+                    }
+                    const pattern = job.getPattern();
+                    return {
+                        id: task.id,
+                        schedule: pattern ?? 'unknown',
+                        scheduleDescription: pattern ? cronstrue.toString(pattern) : 'unknown',
+                        lastExecutedAt: task.lastExecutedAt,
+                        nextExecutionAt: job.nextRun(),
+                        isRunning: task.isRunning,
+                        lastResult: task.lastResult,
+                    };
+                })
+                .filter(x => x !== undefined),
+        );
+    }
+
     private createCronJob(task: ScheduledTask) {
         const schedulerStrategy = this.configService.schedulerOptions.schedulerStrategy;
         const protectCallback = (_job: Cron) => {

+ 11 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -4886,6 +4886,7 @@ export type Query = {
     provinces: ProvinceList;
     role?: Maybe<Role>;
     roles: RoleList;
+    scheduledTasks: Array<ScheduledTask>;
     search: SearchResponse;
     seller?: Maybe<Seller>;
     sellers: SellerList;
@@ -5388,6 +5389,16 @@ export type Sale = Node &
         updatedAt: Scalars['DateTime']['output'];
     };
 
+export type ScheduledTask = {
+    id: Scalars['String']['output'];
+    isRunning: Scalars['Boolean']['output'];
+    lastExecutedAt?: Maybe<Scalars['DateTime']['output']>;
+    lastResult?: Maybe<Scalars['JSON']['output']>;
+    nextExecutionAt?: Maybe<Scalars['DateTime']['output']>;
+    schedule: Scalars['String']['output'];
+    scheduleDescription: Scalars['String']['output'];
+};
+
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;

+ 11 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -4957,6 +4957,7 @@ export type Query = {
     provinces: ProvinceList;
     role?: Maybe<Role>;
     roles: RoleList;
+    scheduledTasks: Array<ScheduledTask>;
     search: SearchResponse;
     seller?: Maybe<Seller>;
     sellers: SellerList;
@@ -5463,6 +5464,16 @@ export type Sale = Node &
         updatedAt: Scalars['DateTime']['output'];
     };
 
+export type ScheduledTask = {
+    id: Scalars['String']['output'];
+    isRunning: Scalars['Boolean']['output'];
+    lastExecutedAt?: Maybe<Scalars['DateTime']['output']>;
+    lastResult?: Maybe<Scalars['JSON']['output']>;
+    nextExecutionAt?: Maybe<Scalars['DateTime']['output']>;
+    schedule: Scalars['String']['output'];
+    scheduleDescription: Scalars['String']['output'];
+};
+
 export type SearchInput = {
     collectionId?: InputMaybe<Scalars['ID']['input']>;
     collectionSlug?: InputMaybe<Scalars['String']['input']>;

Разница между файлами не показана из-за своего большого размера
+ 649 - 628
packages/payments-plugin/e2e/graphql/generated-shop-types.ts


Разница между файлами не показана из-за своего большого размера
+ 678 - 657
packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts


Некоторые файлы не были показаны из-за большого количества измененных файлов