@grouparoo/core
Version:
The Grouparoo Core
1,114 lines (945 loc) • 34.6 kB
text/typescript
import { helper } from "@grouparoo/spec-helper";
import { api, specHelper } from "actionhero";
import {
plugin,
App,
Schedule,
Source,
Option,
Run,
Filter,
GrouparooModel,
} from "../../src";
import { FilterHelper } from "../../src/modules/filterHelper";
describe("models/schedule", () => {
helper.grouparooTestServer({ truncate: true, enableTestPlugin: true });
let model: GrouparooModel;
beforeAll(async () => ({ model } = await helper.factories.properties()));
describe("with source", () => {
let app: App;
let source: Source;
beforeAll(async () => {
app = await helper.factories.app();
source = await Source.create({
name: "test source",
type: "test-plugin-import",
appId: app.id,
modelId: model.id,
});
await source.setOptions({ table: "users" });
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
});
test("a schedule can be created with a source, and it can find the related app", async () => {
const schedule = new Schedule({
name: "test schedule",
type: "test-plugin-import",
sourceId: source.id,
});
await schedule.save();
expect(schedule.id.length).toBe(40);
expect(schedule.createdAt).toBeTruthy();
expect(schedule.updatedAt).toBeTruthy();
const options = await schedule.getOptions();
expect(options).toEqual({});
const _source = await schedule.$get("source");
const _app = await _source.$get("app");
expect(_app.id).toBe(app.id);
await schedule.destroy();
});
test("a schedule name will be generated from the source if one is not provided", async () => {
const schedule = await Schedule.create({
type: "test-plugin-import",
sourceId: source.id,
});
expect(schedule.name).toMatch(/test source schedule/);
await schedule.destroy();
});
test("a schedule cannot be created if the source does not have all the required options set", async () => {
const app = await helper.factories.app();
await app.update({ state: "ready" });
const source = await helper.factories.source(app);
const sourceOptions = await source.getOptions();
await expect(source.validateOptions(sourceOptions)).rejects.toThrow(
/table is required/
);
await expect(
Schedule.create({
name: "test schedule",
type: "test-plugin-import",
sourceId: source.id,
})
).rejects.toThrow(/table is required/);
});
test("creating 2 schedules for the same source will throw an error", async () => {
const scheduleA = new Schedule({
name: "incoming schedule A",
type: "test-plugin-import",
sourceId: source.id,
});
await scheduleA.save();
const scheduleB = new Schedule({
name: "incoming schedule B",
type: "test-plugin-import",
sourceId: source.id,
});
await expect(scheduleB.save()).rejects.toThrow(/already has a schedule/);
await scheduleA.destroy();
});
test("options can be set and retrieved", async () => {
const schedule = await Schedule.create({
name: "incoming schedule A",
type: "test-plugin-import",
sourceId: source.id,
});
await schedule.setOptions({ maxColumn: "created_at" });
const options = await schedule.getOptions();
expect(options).toEqual({ maxColumn: "created_at" });
await schedule.destroy();
});
test("deleting a schedule deleted the options", async () => {
const schedule = await Schedule.create({
name: "incoming schedule A",
type: "test-plugin-import",
sourceId: source.id,
});
await schedule.setOptions({ maxColumn: "created_at" });
await schedule.destroy(); // doesn't throw
const optionsCount = await Option.count({
where: { ownerId: schedule.id },
});
expect(optionsCount).toBe(0);
});
test("deleting a schedule does not delete options for other models with the same id", async () => {
const schedule = await Schedule.create({
name: "incoming schedule A",
type: "test-plugin-import",
sourceId: source.id,
});
await schedule.setOptions({ maxColumn: "created_at" });
const foreignOption = await Option.create({
ownerId: schedule.id,
ownerType: "other",
key: "someKey",
value: "someValue",
type: "string",
});
let count = await Option.count({
where: { ownerId: schedule.id },
});
expect(count).toBe(2);
await schedule.destroy();
const options = await Option.findAll({
where: { ownerId: schedule.id },
});
expect(options.length).toBe(1);
expect(options[0].ownerType).toBe("other");
expect(options[0].key).toBe("someKey");
await foreignOption.destroy();
});
test("deleting a schedule deletes the previous runs", async () => {
const schedule = await Schedule.create({
name: "incoming schedule A",
type: "test-plugin-import",
sourceId: source.id,
});
await schedule.setOptions({ maxColumn: "updated_at" });
await schedule.update({ state: "ready" });
await Run.create({
creatorId: schedule.id,
creatorType: "schedule",
state: "running",
});
let runCount = await Run.scope(null).count({
where: { creatorId: schedule.id },
});
expect(runCount).toBe(1);
await schedule.destroy();
runCount = await Run.scope(null).count({
where: { creatorId: schedule.id },
});
expect(runCount).toBe(0);
});
test("providing invalid options will result in an error", async () => {
const schedule = await Schedule.create({
name: "incoming schedule A",
type: "test-plugin-import",
sourceId: source.id,
});
await expect(schedule.setOptions({ notThing: "abc" })).rejects.toThrow(
/maxColumn is required for a schedule of type test-plugin-import/
);
await expect(
schedule.setOptions({ maxColumn: "created_at", otherThing: "false" })
).rejects.toThrow(
/otherThing is not an option for a test-plugin-import schedule/
);
await expect(
schedule.setOptions({ maxColumn: "some_nonexistent_col" })
).rejects.toThrow(
/"some_nonexistent_col" is not a valid value for test-plugin-import schedule option "maxColumn"/
);
await schedule.destroy();
});
describe("running after save", () => {
let schedule: Schedule;
beforeEach(async () => {
schedule = await Schedule.create({
name: "schedule",
type: "test-plugin-import",
sourceId: source.id,
});
await schedule.setOptions({ maxColumn: "created_at" });
await schedule.update({ state: "ready" });
});
afterEach(async () => {
await schedule.destroy();
});
test("creating a new non-recurring schedule will not run now", async () => {
const runs = await schedule.$get("runs");
expect(runs.length).toBe(0);
});
test("creating a new recurring schedule will run now", async () => {
await schedule.update({
recurring: true,
recurringFrequency: 1000 * 60 * 60,
});
const runs = await schedule.$get("runs");
expect(runs.length).toBe(1);
expect(runs[0].state).toBe("running");
});
test("saving a recurring schedule will run not now if there is already a run in progress", async () => {
const firstRun = await schedule.enqueueRun();
await schedule.update({
recurring: true,
recurringFrequency: 1000 * 60 * 60,
});
const runs = await schedule.$get("runs");
expect(runs.length).toBe(1);
expect(runs[0].id).toBe(firstRun.id);
});
});
describe("with plugin that does not support schedules", () => {
beforeAll(() => {
plugin.registerPlugin({
name: "test-plugin-no-schedule",
apps: [
{
name: "test-plugin-app-no-schedule",
displayName: "test-plugin-app-no-schedule",
options: [],
methods: {
test: async () => {
return { success: true };
},
},
},
],
connections: [
{
name: "test-plugin-source-no-schedule",
displayName: "test-plugin-source-no-schedule",
description: "a test connection",
apps: ["test-plugin-app-no-schedule"],
direction: "import",
options: [],
methods: {
propertyOptions: async () => {
return null;
},
recordProperty: async () => {
return [];
},
},
},
],
});
});
test("a schedule cannot be created if the source does not support schedules", async () => {
const app = await App.create({
type: "test-plugin-app-no-schedule",
name: "test app",
});
await app.update({ state: "ready" });
const source = await Source.create({
type: "test-plugin-source-no-schedule",
appId: app.id,
modelId: model.id,
});
await source.update({ state: "ready" });
await expect(
Schedule.create({
name: "test schedule",
type: "test-plugin-source-no-schedule",
sourceId: source.id,
})
).rejects.toThrow(/cannot have a schedule/);
await source.destroy();
await app.destroy();
});
});
describe("with plugin that does not support incremental schedules", () => {
let app: App, source: Source;
beforeAll(async () => {
plugin.registerPlugin({
name: "test-plugin-no-incremental-schedule",
apps: [
{
name: "test-plugin-app-no-incremental-schedule",
displayName: "test-plugin-app-no-incremental-schedule",
options: [],
methods: {
test: async () => {
return { success: true };
},
},
},
],
connections: [
{
name: "test-plugin-source-no-incremental-schedule",
displayName: "test-plugin-source-no-incremental-schedule",
description: "a test connection",
apps: ["test-plugin-app-no-incremental-schedule"],
supportIncrementalSchedule: false,
direction: "import",
options: [],
methods: {
propertyOptions: async () => {
return null;
},
recordProperty: async () => {
return [];
},
importRecords: async () => ({
highWaterMark: {},
importsCount: 0,
sourceOffset: 0,
}),
},
},
],
});
app = await App.create({
type: "test-plugin-app-no-incremental-schedule",
name: "test app",
});
await app.update({ state: "ready" });
source = await Source.create({
type: "test-plugin-source-no-incremental-schedule",
appId: app.id,
modelId: model.id,
});
await source.update({ state: "ready" });
});
afterAll(async () => {
await source.destroy();
await app.destroy();
});
test("an incremental schedule cannot be created if the source does not support incremental schedules", async () => {
await expect(
Schedule.create({
name: "test schedule",
type: source.type,
sourceId: source.id,
incremental: true,
})
).rejects.toThrow(/does not support incremental schedules/);
});
test("`incremental` defaults to false for schedules on plugins that don't support it", async () => {
const schedule = await Schedule.create({
name: "test schedule",
type: source.type,
sourceId: source.id,
});
expect(schedule.incremental).toBe(false);
await schedule.destroy();
});
});
describe("validations", () => {
test("options must match the app options (extra options needed by app)", async () => {
const schedule = new Schedule({
name: "incoming schedule - too many options",
type: "test-plugin-import",
sourceId: source.id,
});
await schedule.save();
expect(schedule.id).toBeTruthy();
await expect(
schedule.setOptions({ maxColumn: "created_at", something: "abc123" })
).rejects.toThrow(
/something is not an option for a test-plugin-import schedule/
);
await schedule.destroy();
});
test("__options only includes options for schedules", async () => {
const schedule = await Schedule.create({
id: "myScheduleId",
type: "test-plugin-import",
name: "test schedule",
sourceId: source.id,
});
await Option.create({
ownerId: schedule.id,
ownerType: "schedule",
key: "maxColumn",
value: "abc",
type: "string",
});
await Option.create({
ownerId: schedule.id,
ownerType: "source",
key: "someOtherProperty",
value: "someValue",
type: "string",
});
const options = await schedule.$get("__options");
expect(options.length).toBe(1);
expect(options[0].ownerType).toBe("schedule");
expect(options[0].key).toBe("maxColumn");
await schedule.destroy();
});
test("recurring schedules require a recurring frequency > 1 minute", async () => {
const schedule = await helper.factories.schedule();
await expect(
schedule.update({ recurring: true, recurringFrequency: 0 })
).rejects.toThrow(
/recurring frequency is required to be one minute or greater/
);
await expect(
schedule.update({ recurring: true, recurringFrequency: null })
).rejects.toThrow(
/recurring frequency is required to be one minute or greater/
);
await schedule.update({
recurring: true,
recurringFrequency: 1000 * 60,
}); // OK
await schedule.destroy();
});
test("a schedule cannot be changed to to the ready state if there are missing required options", async () => {
const schedule = await Schedule.create({
sourceId: source.id,
name: "no opts",
});
await expect(schedule.update({ state: "ready" })).rejects.toThrow(
/maxColumn is required/
);
await schedule.destroy();
});
test("a schedule cannot be created in the ready state with missing required options", async () => {
const schedule = Schedule.build({
sourceId: source.id,
name: "no opts",
state: "ready",
});
await expect(schedule.save()).rejects.toThrow(/maxColumn is required/);
});
test("a schedule that is ready cannot move back to draft", async () => {
const schedule = await helper.factories.schedule();
await schedule.update({ state: "ready" });
await expect(schedule.update({ state: "draft" })).rejects.toThrow(
/cannot transition schedule state from ready to draft/
);
await schedule.destroy();
});
test("changing a schedule's options will reset previous highWaterMarks and start a run", async () => {
const schedule: Schedule = await helper.factories.schedule();
const opts = await schedule.getOptions();
expect(opts).toEqual({ maxColumn: "updated_at" });
const run = await Run.create({
creatorId: schedule.id,
creatorType: "schedule",
state: "complete",
highWaterMark: { updated_at: 12345 },
});
expect((await schedule.$get("runs")).length).toBe(1);
expect(run.highWaterMark).toEqual({ updated_at: 12345 });
await schedule.setOptions({ maxColumn: "created_at" });
await run.reload();
expect(run.highWaterMark).toEqual({}); // the getter formats to an empty array
expect((await schedule.$get("runs")).length).toBe(2);
await schedule.destroy();
});
test("changing a schedule's filters will reset previous highWaterMarks and start a run", async () => {
const schedule: Schedule = await helper.factories.schedule();
const filters = await schedule.getFilters();
expect(filters).toEqual([]);
const run = await Run.create({
creatorId: schedule.id,
creatorType: "schedule",
state: "complete",
highWaterMark: { updated_at: 12345 },
});
expect((await schedule.$get("runs")).length).toBe(1);
expect(run.highWaterMark).toEqual({ updated_at: 12345 });
await schedule.setFilters([{ key: "id", match: "0", op: "gt" }]);
await run.reload();
expect(run.highWaterMark).toEqual({}); // the getter formats to an empty array
expect((await schedule.$get("runs")).length).toBe(2);
await schedule.destroy();
});
});
describe("shouldRun", () => {
let schedule: Schedule;
beforeEach(async () => {
schedule = await Schedule.create({
name: "schedule",
type: "test-plugin-import",
sourceId: source.id,
});
await schedule.setOptions({ maxColumn: "created_at" });
await schedule.update({ state: "ready" });
});
afterEach(async () => {
await schedule.destroy();
});
describe("default behavior", () => {
test("it will not enqueue a run for a non-recurring schedule", async () => {
await schedule.update({
recurring: false,
recurringFrequency: 0,
});
await Run.truncate();
expect(await schedule.shouldRun()).toBe(false);
});
test("it will not enqueue a run for a recurring schedule that has never run but is a draft", async () => {
const source = await helper.factories.source();
await source.setOptions({ table: "users" });
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
const localSchedule = await Schedule.create({
sourceId: source.id,
name: "tmp",
recurring: true,
recurringFrequency: 60 * 1000,
});
expect(localSchedule.state).toBe("draft");
await Run.truncate();
expect(await localSchedule.shouldRun()).toBe(false);
await localSchedule.destroy();
await source.destroy();
});
test("it will enqueue a run for a recurring schedule that has never run and is ready", async () => {
await schedule.update({
recurring: true,
recurringFrequency: 60 * 1000,
state: "ready",
});
await Run.truncate();
expect(await schedule.shouldRun()).toBe(true);
});
test("it will enqueue a run for a recurring schedule that ran in the past", async () => {
await schedule.update({
recurring: true,
recurringFrequency: 60 * 1000,
});
await Run.truncate();
await Run.create({
creatorId: schedule.id,
creatorType: "schedule",
state: "complete",
completedAt: new Date(0),
});
expect(await schedule.shouldRun()).toBe(true);
});
test("it will not enqueue a run for a recurring schedule that ran too recently", async () => {
await schedule.update({
recurring: true,
recurringFrequency: 60 * 1000,
});
await Run.truncate();
await Run.create({
creatorId: schedule.id,
creatorType: "schedule",
state: "complete",
completedAt: new Date(),
});
expect(await schedule.shouldRun()).toBe(false);
});
test("it will not enqueue a run for a recurring schedule that is running", async () => {
await schedule.update({
recurring: true,
recurringFrequency: 60 * 1000,
});
await Run.truncate();
await Run.create({
creatorId: schedule.id,
creatorType: "schedule",
state: "running",
});
expect(await schedule.shouldRun()).toBe(false);
});
});
describe("custom inputs", () => {
test("it can enqueue a run for a non-recurring schedule (runIfNotRecurring)", async () => {
await schedule.update({
recurring: false,
recurringFrequency: 0,
});
await Run.truncate();
expect(await schedule.shouldRun({ runIfNotRecurring: true })).toBe(
true
);
});
test("it will not enqueue a run for a recurring schedule that ran too recently (ignoreDeltas)", async () => {
await schedule.update({
recurring: true,
recurringFrequency: 60 * 1000,
});
await Run.truncate();
const run = await Run.create({
creatorId: schedule.id,
creatorType: "schedule",
state: "complete",
completedAt: new Date(),
});
expect(await schedule.shouldRun({ ignoreDeltas: true })).toBe(true);
});
});
});
});
describe("with custom plugin", () => {
let app: App;
let source: Source;
beforeAll(async () => {
plugin.registerPlugin({
name: "test-plugin",
apps: [
{
name: "test-template-app",
displayName: "test-template-app",
options: [],
methods: {
test: async () => {
return { success: true };
},
},
},
],
connections: [
{
name: "import-from-test-template-app",
displayName: "import-from-test-template-app",
supportIncrementalSchedule: true,
description: "a test app connection",
apps: ["test-template-app"],
direction: "import" as "import",
options: [],
methods: {
scheduleOptions: async () => [
{
key: "maxColumn",
required: true,
description: "the column to choose",
type: "list",
options: async () => {
return [
{ key: "created_at", examples: [1, 2, 3] },
{ key: "updated_at", examples: [1, 2, 3] },
];
},
},
],
sourceRunPercentComplete: async () => {
return 33;
},
importRecords: async ({ highWaterMark }) => {
return {
highWaterMark: {
updated_at:
parseInt(
highWaterMark?.updated_at
? String(highWaterMark.updated_at)
: "0"
) + 100,
},
sourceOffset: 100,
importsCount: 100,
};
},
recordProperty: async () => {
return [];
},
sourceFilters: async () => {
return [
{
key: "id",
ops: ["gt", "lt"],
canHaveRelativeMatch: false,
},
];
},
},
},
],
});
app = await App.create({
name: "test app with real methods",
type: "test-template-app",
state: "ready",
});
source = await Source.create({
name: "test source from plugin",
type: "import-from-test-template-app",
appId: app.id,
modelId: model.id,
});
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
});
test("`incremental` defaults to true for schedules on plugins that support it", async () => {
const schedule = await Schedule.create({
name: "test schedule",
type: source.type,
sourceId: source.id,
});
expect(schedule.incremental).toBe(true);
await schedule.destroy();
});
test.each(["ready", "deleted"])(
"schedules can retrieve their options from a %p source",
async (state) => {
const schedule = await Schedule.create({
name: "test plugin schedule",
sourceId: source.id,
});
await app.update({ state });
await source.update({ state });
const pluginOptions = await schedule.pluginOptions();
expect(pluginOptions).toEqual([
{
description: "the column to choose",
key: "maxColumn",
options: [
{ examples: [1, 2, 3], key: "created_at" },
{ key: "updated_at", examples: [1, 2, 3] },
],
required: true,
type: "list",
},
]);
await schedule.destroy();
await source.update({ state: "ready" });
await app.update({ state: "ready" });
}
);
test("running a schedule that isn't ready will throw", async () => {
const schedule = await Schedule.create({
name: "test plugin schedule",
sourceId: source.id,
});
await expect(
Run.create({
creatorId: schedule.id,
creatorType: "schedule",
state: "running",
})
).rejects.toThrow(/creator schedule is not ready/);
await schedule.destroy();
});
test("runs from incremental schedules will use the previous highwatermark", async () => {
const schedule = await Schedule.create({
name: "test incremental schedule",
sourceId: source.id,
incremental: true,
});
await schedule.setOptions({ maxColumn: "updated_at" });
await schedule.update({ state: "ready" });
const firstRun = await schedule.enqueueRun();
await specHelper.runTask("schedule:run", { runId: firstRun.id });
await firstRun.reload();
expect(firstRun.highWaterMark).toEqual({ updated_at: 100 });
await firstRun.update({ state: "complete", importsCreated: 100 });
const secondRun = await schedule.enqueueRun();
await specHelper.runTask("schedule:run", { runId: secondRun.id });
await secondRun.reload();
expect(secondRun.highWaterMark).toEqual({ updated_at: 200 }); // starts at 100 increments one batch
await schedule.destroy();
});
test("runs from non-incremental schedules will start from the beginning", async () => {
const schedule = await Schedule.create({
name: "test incremental schedule",
sourceId: source.id,
incremental: false,
});
await schedule.setOptions({ maxColumn: "updated_at" });
await schedule.update({ state: "ready" });
const firstRun = await schedule.enqueueRun();
await specHelper.runTask("schedule:run", { runId: firstRun.id });
await firstRun.reload();
expect(firstRun.highWaterMark).toEqual({});
await firstRun.update({ state: "complete", importsCreated: 100 });
const secondRun = await schedule.enqueueRun();
await specHelper.runTask("schedule:run", { runId: secondRun.id });
await secondRun.reload();
expect(secondRun.highWaterMark).toEqual({});
await schedule.destroy();
});
test("the source can provide the percentComplete via sourceRunPercentComplete", async () => {
const schedule = await Schedule.create({
name: "test plugin schedule",
sourceId: source.id,
});
await schedule.setOptions({ maxColumn: "updated_at" });
await schedule.update({ state: "ready" });
const run = await Run.create({
creatorId: schedule.id,
creatorType: "schedule",
state: "running",
});
expect(run.percentComplete).toBe(0);
await run.determinePercentComplete();
expect(run.percentComplete).toBe(33);
await schedule.destroy();
});
test("a source using a plugin with no records method cannot have a schedule", async () => {
expect(await source.scheduleAvailable()).toBe(true);
// delete the records method
const plugin = api.plugins.plugins.filter(
(p) => p.name === "test-plugin"
)[0];
const originalMethod = plugin.connections[0].methods.importRecords;
plugin.connections[0].methods.importRecords = undefined;
expect(await source.scheduleAvailable()).toBe(false);
await expect(
Schedule.create({
name: "test plugin schedule",
sourceId: source.id,
})
).rejects.toThrow(/cannot have a schedule/);
// replace method
plugin.connections[0].methods.importRecords = originalMethod;
});
describe("filters", () => {
test("we can determine if schedule's filters have been changed", async () => {
const schedule = await Schedule.create({
key: "test",
type: "string",
sourceId: source.id,
});
await schedule.setFilters([{ key: "id", match: "0", op: "gt" }]);
const filters = await schedule.getFilters();
expect(FilterHelper.filtersAreEqual(filters, [])).toBe(false);
expect(
FilterHelper.filtersAreEqual(filters, [
{ key: "id", match: "0", op: "gt" },
])
).toBe(true);
expect(
FilterHelper.filtersAreEqual(filters, [
{ key: "id", match: "1", op: "gt" },
])
).toBe(false);
expect(
FilterHelper.filtersAreEqual(filters, [
{ key: "id", match: "0", op: "lt" },
])
).toBe(false);
await schedule.destroy();
});
test("it can get the filter options from the plugin", async () => {
const schedule = await Schedule.create({
key: "test",
type: "string",
sourceId: source.id,
});
const filterOptions = await FilterHelper.pluginFilterOptions(schedule);
expect(filterOptions).toEqual([
{
key: "id",
ops: ["gt", "lt"],
canHaveRelativeMatch: false,
},
]);
await schedule.destroy();
});
test("it will memoize filters as they are set", async () => {
const schedule = await Schedule.create({
key: "test",
type: "string",
sourceId: source.id,
});
await schedule.setFilters([{ op: "gt", match: 1, key: "id" }]);
expect(schedule.filters.length).toBe(1);
expect(schedule.filters[0].op).toBe("gt");
expect(schedule.filters[0].match).toBe("1");
expect(schedule.filters[0].key).toBe("id");
await schedule.destroy();
});
test("it will use memoized filters if they exist", async () => {
const schedule = await Schedule.create({
key: "test",
type: "string",
sourceId: source.id,
});
await schedule.setFilters([{ op: "gt", match: 999, key: "id" }]);
schedule.filters = [
Filter.build({
propertyId: schedule.id,
position: 1,
key: "foo",
match: "-1",
op: "less than",
}),
];
const filters = await schedule.getFilters();
expect(filters.length).toBe(1);
expect(filters[0].key).toEqual("foo");
expect(filters[0].match).toEqual("-1");
expect(filters[0].op).toEqual("less than");
await schedule.destroy();
});
test("filters that match the options can be set", async () => {
const schedule = await Schedule.create({
key: "test",
type: "string",
sourceId: source.id,
});
await schedule.setFilters([
{ op: "gt", match: 1, key: "id" },
{ op: "lt", match: 99, key: "id" },
]);
const filters = await schedule.getFilters();
expect(filters).toEqual([
{
op: "gt",
match: "1",
key: "id",
relativeMatchDirection: null,
relativeMatchNumber: null,
relativeMatchUnit: null,
},
{
op: "lt",
match: "99",
key: "id",
relativeMatchDirection: null,
relativeMatchNumber: null,
relativeMatchUnit: null,
},
]);
await schedule.destroy();
});
test("deleting a schedule also deleted the filters", async () => {
const count = await Filter.count({ where: { ownerType: "schedule" } });
expect(count).toBe(0);
});
test("filters that do not match the options cannot be set", async () => {
const schedule = await Schedule.create({
key: "test",
type: "string",
sourceId: source.id,
});
await expect(
schedule.setFilters([{ op: "gt", match: 1, key: "other-key" }])
).rejects.toThrow("other-key is not filterable");
await expect(
//-ignore
schedule.setFilters([{ op: "max it out", match: 1, key: "id" }])
).rejects.toThrow('"max it out" cannot be applied to id');
await schedule.destroy();
});
});
});
});