From 628d91b003c4fd0e2277dcec21e8edfdcbbb3468 Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Sat, 29 Dec 2018 01:02:52 -0600 Subject: [PATCH] feat: tick entity for interval to consume, add intervals to schema --- docs/interval/command-interval.yml | 9 ++++ docs/isolex.yml | 2 + src/BaseService.ts | 2 +- src/Bot.ts | 9 ++++ src/entity/Context.ts | 5 ++- src/entity/Tick.ts | 51 +++++++++++++++++++++++ src/interval/BaseInterval.ts | 44 ++++++++++++++----- src/interval/CommandInterval.ts | 5 ++- src/interval/EventInterval.ts | 5 ++- src/interval/Interval.ts | 13 ++---- src/interval/MessageInterval.ts | 5 ++- src/migration/0001546063195-CreateTick.ts | 32 ++++++++++++++ src/module/EntityModule.ts | 2 + src/module/MigrationModule.ts | 2 + src/schema.yml | 44 +++++++++++++++---- src/utils/Clock.ts | 8 ++++ 16 files changed, 203 insertions(+), 35 deletions(-) create mode 100644 docs/interval/command-interval.yml create mode 100644 src/entity/Tick.ts create mode 100644 src/migration/0001546063195-CreateTick.ts diff --git a/docs/interval/command-interval.yml b/docs/interval/command-interval.yml new file mode 100644 index 000000000..2b0856630 --- /dev/null +++ b/docs/interval/command-interval.yml @@ -0,0 +1,9 @@ +metadata: + kind: command-interval + name: test-interval-cmd +data: + defaultCommand: + noun: time + verb: get + frequency: + zeit: 30 seconds \ No newline at end of file diff --git a/docs/isolex.yml b/docs/isolex.yml index 717710c0d..dd6223520 100644 --- a/docs/isolex.yml +++ b/docs/isolex.yml @@ -30,6 +30,8 @@ data: - !include ../docs/controller/token-controller.yml - !include ../docs/controller/user-controller.yml - !include ../docs/controller/weather-controller-owm.yml + intervals: + - !include ../docs/interval/command-interval.yml listeners: - !include ../docs/listener/discord-listener.yml - !include ../docs/listener/express-listener.yml diff --git a/src/BaseService.ts b/src/BaseService.ts index 8f753c14c..c3f48e23f 100644 --- a/src/BaseService.ts +++ b/src/BaseService.ts @@ -70,7 +70,7 @@ export abstract class BaseService implements Serv // validate the data const result = options.schema.match(options.data, schemaPath); if (!result.valid) { - this.logger.error({ errors: result.errors }, 'failed to validate config'); + this.logger.error({ data: options.data, errors: result.errors }, 'failed to validate config'); throw new SchemaError('failed to validate config'); } else { this.logger.debug('validated config data'); diff --git a/src/Bot.ts b/src/Bot.ts index b0dc51b69..c750b5832 100644 --- a/src/Bot.ts +++ b/src/Bot.ts @@ -9,6 +9,7 @@ import { BaseService, BaseServiceOptions } from 'src/BaseService'; import { Controller, ControllerData } from 'src/controller/Controller'; import { Command } from 'src/entity/Command'; import { Message } from 'src/entity/Message'; +import { Interval, IntervalData } from 'src/interval/Interval'; import { ContextFetchOptions, Listener, ListenerData } from 'src/listener/Listener'; import { ServiceModule } from 'src/module/ServiceModule'; import { Parser, ParserData } from 'src/parser/Parser'; @@ -20,6 +21,7 @@ import { StorageLogger, StorageLoggerOptions } from 'src/utils/StorageLogger'; export interface BotData { filters: Array; controllers: Array>; + intervals: Array; listeners: Array; logger: { level: LogLevel; @@ -48,6 +50,7 @@ export class Bot extends BaseService implements Service { // services protected controllers: Array; + protected intervals: Array; protected listeners: Array; protected parsers: Array; protected services: ServiceModule; @@ -68,6 +71,7 @@ export class Bot extends BaseService implements Service { // set up deps this.controllers = []; + this.intervals = []; this.listeners = []; this.parsers = []; @@ -296,6 +300,11 @@ export class Bot extends BaseService implements Service { this.controllers.push(await this.services.createService(data)); } + this.logger.info('setting up intervals'); + for (const data of this.data.intervals) { + this.intervals.push(await this.services.createService(data)); + } + this.logger.info('setting up listeners'); for (const data of this.data.listeners) { this.listeners.push(await this.services.createService(data)); diff --git a/src/entity/Context.ts b/src/entity/Context.ts index 92bda5f0f..9540f89a1 100644 --- a/src/entity/Context.ts +++ b/src/entity/Context.ts @@ -8,6 +8,7 @@ import { Token } from 'src/entity/auth/Token'; import { GRAPH_OUTPUT_USER, User } from 'src/entity/auth/User'; import { Listener } from 'src/listener/Listener'; import { Parser } from 'src/parser/Parser'; +import { BaseEntity } from './base/BaseEntity'; export interface ChannelData { id: string; @@ -44,7 +45,7 @@ export interface ContextData { export const TABLE_CONTEXT = 'context'; @Entity(TABLE_CONTEXT) -export class Context implements ContextData { +export class Context extends BaseEntity implements ContextData { @Column('simple-json') public channel: ChannelData; @@ -68,6 +69,8 @@ export class Context implements ContextData { public user?: User; constructor(options?: ContextData) { + super(); + if (options) { if (!options.name || !options.uid) { throw new MissingValueError('name and uid must be specified in context options'); diff --git a/src/entity/Tick.ts b/src/entity/Tick.ts new file mode 100644 index 000000000..ba1ec8f8d --- /dev/null +++ b/src/entity/Tick.ts @@ -0,0 +1,51 @@ +import { GraphQLObjectType, GraphQLString } from 'graphql'; +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; + +import { BaseEntity } from './base/BaseEntity'; + +export const TABLE_TICK = 'tick'; + +@Entity(TABLE_TICK) +export class Tick extends BaseEntity { + @CreateDateColumn() + public createdAt: number; + + @PrimaryGeneratedColumn('uuid') + public id: string; + + @Column() + public intervalId: string; + + @Column() + public status: number; + + @UpdateDateColumn() + public updatedAt: number; + + public toJSON(): object { + return { + createdAt: this.createdAt, + id: this.id, + intervalId: this.intervalId, + updatedAt: this.updatedAt, + }; + } +} + +export const GRAPH_OUTPUT_TICK = new GraphQLObjectType({ + fields: { + createdAt: { + type: GraphQLString, + }, + id: { + type: GraphQLString, + }, + intervalId: { + type: GraphQLString, + }, + updatedAt: { + type: GraphQLString, + }, + }, + name: 'Tick', +}); diff --git a/src/interval/BaseInterval.ts b/src/interval/BaseInterval.ts index 0e76de207..123d03a18 100644 --- a/src/interval/BaseInterval.ts +++ b/src/interval/BaseInterval.ts @@ -1,38 +1,60 @@ import { Inject } from 'noicejs'; -import { Repository } from 'typeorm'; +import { Equal, Repository } from 'typeorm'; import { BotService } from 'src/BotService'; import { Context } from 'src/entity/Context'; -import { Interval, IntervalData, IntervalJob, IntervalOptions } from 'src/interval/Interval'; +import { Tick } from 'src/entity/Tick'; +import { Interval, IntervalData, IntervalOptions } from 'src/interval/Interval'; import { Clock } from 'src/utils/Clock'; @Inject('clock', 'storage') export abstract class BaseInterval extends BotService implements Interval { protected readonly clock: Clock; - protected readonly tickRepository: Repository; + protected readonly tickRepository: Repository; + + protected interval: number; constructor(options: IntervalOptions, schemaPath: string) { super(options, schemaPath); this.clock = options.clock; - this.tickRepository = options.storage.getRepository(''); // @TODO interval tick/job entity + this.tickRepository = options.storage.getRepository(Tick); } - public abstract tick(context: Context, last: IntervalJob): Promise; + public async start() { + await super.start(); + + // set up the interval + this.interval = this.clock.setInterval(() => this.nextTick, 1000); // TODO: frequency from config + } + + public async stop() { + await super.stop(); + + this.clock.clearInterval(this.interval); + } + + public abstract tick(context: Context, last: Tick): Promise; protected async nextTick() { - const last = await this.tickRepository.findOneOrFail({ - // TODO: most recent - intervalId: this.id, + const last = await this.tickRepository.find({ + order: { + toString: undefined, // needs to be included, otherwise object's toString conflicts with typeorm interface + updatedAt: 'DESC', + }, + take: 1, + where: { + intervalId: Equal(this.id), + }, }); const context = await this.createTickContext(); - const status = await this.tick(context, last); - const next: IntervalJob = { + const status = await this.tick(context, last[0]); + const next = this.tickRepository.create({ createdAt: this.clock.getSeconds(), intervalId: this.id, status, updatedAt: this.clock.getSeconds(), - }; + }); await this.tickRepository.save(next); } diff --git a/src/interval/CommandInterval.ts b/src/interval/CommandInterval.ts index d65279fd8..76798c111 100644 --- a/src/interval/CommandInterval.ts +++ b/src/interval/CommandInterval.ts @@ -1,7 +1,8 @@ import { Command, CommandOptions } from 'src/entity/Command'; import { Context } from 'src/entity/Context'; +import { Tick } from 'src/entity/Tick'; import { BaseInterval } from 'src/interval/BaseInterval'; -import { IntervalData, IntervalJob, IntervalOptions } from 'src/interval/Interval'; +import { IntervalData, IntervalOptions } from 'src/interval/Interval'; export interface CommandIntervalData extends IntervalData { defaultCommand: CommandOptions; @@ -14,7 +15,7 @@ export class CommandInterval extends BaseInterval { super(options, 'isolex#/definitions/service-interval-command'); } - public async tick(context: Context, last: IntervalJob): Promise { + public async tick(context: Context, last: Tick): Promise { const cmd = new Command({ ...this.data.defaultCommand, context, diff --git a/src/interval/EventInterval.ts b/src/interval/EventInterval.ts index 4fadc4400..3f156a2f3 100644 --- a/src/interval/EventInterval.ts +++ b/src/interval/EventInterval.ts @@ -1,6 +1,7 @@ import { Context } from 'src/entity/Context'; +import { Tick } from 'src/entity/Tick'; import { BaseInterval } from 'src/interval/BaseInterval'; -import { IntervalData, IntervalJob, IntervalOptions } from 'src/interval/Interval'; +import { IntervalData, IntervalOptions } from 'src/interval/Interval'; import { ServiceLifecycle, ServiceMetadata } from 'src/Service'; export interface EventIntervalData extends IntervalData { @@ -15,7 +16,7 @@ export class EventInterval extends BaseInterval { super(options, 'isolex#/definitions/service-interval-event'); } - public async tick(context: Context, last: IntervalJob): Promise { + public async tick(context: Context, last: Tick): Promise { for (const def of this.data.services) { const svc = this.services.getService(def); await svc.notify(this.data.event); diff --git a/src/interval/Interval.ts b/src/interval/Interval.ts index 0a4b9248c..38ce7f38b 100644 --- a/src/interval/Interval.ts +++ b/src/interval/Interval.ts @@ -1,12 +1,7 @@ import { BotServiceData, BotServiceOptions } from 'src/BotService'; import { Context } from 'src/entity/Context'; - -export interface IntervalJob { - createdAt: number; - intervalId: string; - status: number; - updatedAt: number; -} +import { Tick } from 'src/entity/Tick'; +import { Service } from 'src/Service'; export interface IntervalData extends BotServiceData { frequency: { @@ -16,9 +11,9 @@ export interface IntervalData extends BotServiceData { } export type IntervalOptions = BotServiceOptions; -export interface Interval { +export interface Interval extends Service { /** * Based on the results of the last job, run a new one. */ - tick(context: Context, last: IntervalJob): Promise; + tick(context: Context, last: Tick): Promise; } diff --git a/src/interval/MessageInterval.ts b/src/interval/MessageInterval.ts index d81b462b1..10e79e8f2 100644 --- a/src/interval/MessageInterval.ts +++ b/src/interval/MessageInterval.ts @@ -1,7 +1,8 @@ import { Context } from 'src/entity/Context'; import { Message, MessageOptions } from 'src/entity/Message'; +import { Tick } from 'src/entity/Tick'; import { BaseInterval } from 'src/interval/BaseInterval'; -import { IntervalData, IntervalJob, IntervalOptions } from 'src/interval/Interval'; +import { IntervalData, IntervalOptions } from 'src/interval/Interval'; export interface MessageIntervalData extends IntervalData { defaultMessage: MessageOptions; @@ -14,7 +15,7 @@ export class MessageInterval extends BaseInterval { super(options, 'isolex#/definitions/service-interval-message'); } - public async tick(context: Context, last: IntervalJob): Promise { + public async tick(context: Context, last: Tick): Promise { const msg = new Message({ ...this.data.defaultMessage, body: `last fired: ${last.createdAt}`, diff --git a/src/migration/0001546063195-CreateTick.ts b/src/migration/0001546063195-CreateTick.ts new file mode 100644 index 000000000..5cd3d3e03 --- /dev/null +++ b/src/migration/0001546063195-CreateTick.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +import { TABLE_TICK } from 'src/entity/Tick'; + +export class CreateTick0001546063195 implements MigrationInterface { + public async up(query: QueryRunner): Promise { + await query.createTable(new Table({ + columns: [{ + isPrimary: true, + name: 'id', + type: 'varchar', + }, { + name: 'createdAt', + type: 'int', + }, { + name: 'intervalId', + type: 'varchar', + }, { + name: 'status', + type: 'int', + }, { + name: 'updatedAt', + type: 'int', + }], + name: TABLE_TICK, + })); + } + + public async down(query: QueryRunner): Promise { + await query.dropTable(TABLE_TICK); + } +} diff --git a/src/module/EntityModule.ts b/src/module/EntityModule.ts index 6410ffd02..72a34ad17 100644 --- a/src/module/EntityModule.ts +++ b/src/module/EntityModule.ts @@ -10,6 +10,7 @@ import { Fragment } from 'src/entity/Fragment'; import { Message } from 'src/entity/Message'; import { Counter } from 'src/entity/misc/Counter'; import { Keyword } from 'src/entity/misc/Keyword'; +import { Tick } from 'src/entity/Tick'; export class EntityModule extends Module { public async configure(options: ModuleOptions): Promise { @@ -23,6 +24,7 @@ export class EntityModule extends Module { Context, Fragment, Message, + Tick, /* auth */ Role, Token, diff --git a/src/module/MigrationModule.ts b/src/module/MigrationModule.ts index 1f65fc664..6088fbd91 100644 --- a/src/module/MigrationModule.ts +++ b/src/module/MigrationModule.ts @@ -11,6 +11,7 @@ import { CreateRole0001544312069 } from 'src/migration/0001544312069-CreateRole' import { CreateUser0001544312112 } from 'src/migration/0001544312112-CreateUser'; import { CreateToken0001544317462 } from 'src/migration/0001544317462-CreateToken'; import { KeywordCommand0001545509108 } from 'src/migration/0001545509108-KeywordCommand'; +import { CreateTick0001546063195 } from 'src/migration/0001546063195-CreateTick'; export class MigrationModule extends Module { public async configure(options: ModuleOptions): Promise { @@ -27,6 +28,7 @@ export class MigrationModule extends Module { CreateUser0001544312112, CreateToken0001544317462, KeywordCommand0001545509108, + CreateTick0001546063195, ]); } } diff --git a/src/schema.yml b/src/schema.yml index b0e4d15eb..866ecab4e 100644 --- a/src/schema.yml +++ b/src/schema.yml @@ -79,13 +79,14 @@ definitions: type: array items: type: object - anyOf: - - properties: - string: - type: string - - properties: - regexp: - type: string + additionalProperties: false + oneOf: + - required: [regexp] + - required: [string] + properties: + regexp: {} # TODO: instanceof RegExp + string: + type: string match-data: type: object @@ -379,6 +380,35 @@ definitions: users: $ref: "#/definitions/checklist" + service-interval: + allOf: + - $ref: "#/definitions/service-data" + - type: object + additionalProperties: true + required: [frequency] + properties: + frequency: + type: object + additionalProperties: false + oneOf: + - required: [cron] + - required: [zeit] + properties: + cron: + type: string + zeit: + type: string + + service-interval-command: + allOf: + - $ref: "#/definitions/service-interval" + - type: object + additionalProperties: true + required: [defaultCommand] + properties: + defaultCommand: + $ref: "#/definitions/entity-command" + service-listener: $ref: "#/definitions/service-data" diff --git a/src/utils/Clock.ts b/src/utils/Clock.ts index 635bea6f1..0c34f6feb 100644 --- a/src/utils/Clock.ts +++ b/src/utils/Clock.ts @@ -14,6 +14,10 @@ export class Clock { this.date = date; } + public clearInterval(id: number) { + clearInterval(id); + } + public getDate(): Date { return new Date(); } @@ -21,4 +25,8 @@ export class Clock { public getSeconds(): number { return Math.floor(this.date.now() / NOW_TO_SECONDS); } + + public setInterval(cb: Function, delay: number): number { + return setInterval(cb, delay); + } }