@grouparoo/core
Version:
The Grouparoo Core
1,316 lines (1,104 loc) • 39.6 kB
text/typescript
import { helper } from "@grouparoo/spec-helper";
import { api, specHelper } from "actionhero";
import {
App,
Filter,
GrouparooModel,
Option,
plugin,
PluginOptionType,
Property,
RecordProperty,
Run,
Source,
} from "../../../src";
import { FilterHelper } from "../../../src/modules/filterHelper";
describe("models/property", () => {
let model: GrouparooModel;
helper.grouparooTestServer({ truncate: true, enableTestPlugin: true });
beforeAll(async () => {
({ model } = await helper.factories.properties());
});
test("creating a property with options enqueued an internalRun", async () => {
const runningRuns = await Run.findAll({ where: { state: "running" } });
expect(runningRuns.length).toBe(1);
});
test("a property 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(
Property.create({
sourceId: source.id,
key: "thing",
type: "string",
unique: false,
})
).rejects.toThrow(/table is required/);
});
test("a property cannot be created if the source is not ready", async () => {
const app = await helper.factories.app();
await app.update({ state: "ready" });
const source = await helper.factories.source(app);
await source.setOptions({ table: "some table" });
await source.setMapping({ id: "userId" });
await expect(
Property.create({
sourceId: source.id,
key: "thing",
type: "string",
unique: false,
})
).rejects.toThrow(/source is not ready/);
await source.destroy();
});
describe("keys and types", () => {
let source: Source;
beforeAll(async () => {
source = await helper.factories.source();
await source.setOptions({ table: "some table" });
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
});
afterAll(async () => {
await source.destroy();
});
test("a new property will have a '' key", async () => {
const property = await Property.create({
sourceId: source.id,
type: "string",
});
expect(property.key).toBe("");
await property.destroy();
});
test("ready properties cannot share the same key regardless of key case", async () => {
const ruleOne = await Property.create({
sourceId: source.id,
key: "CASE",
type: "string",
});
const ruleTwo = await Property.create({
sourceId: source.id,
key: "case",
type: "string",
});
await ruleOne.setOptions({ column: "abc123" });
await ruleOne.update({ state: "ready" });
await ruleTwo.setOptions({ column: "abc123" });
await expect(ruleTwo.update({ state: "ready" })).rejects.toThrow(
/key "case" is already in use/
);
await ruleOne.destroy();
await ruleTwo.destroy();
});
test("draft property can share the same key, but not with ready rule", async () => {
const ruleOne = await Property.create({
sourceId: source.id,
type: "string",
});
const ruleTwo = await Property.create({
sourceId: source.id,
type: "string",
});
expect(ruleOne.key).toBe("");
expect(ruleTwo.key).toBe("");
await ruleOne.update({ key: "key" });
await ruleOne.setOptions({ column: "abc123" });
await ruleOne.update({ state: "ready" });
await expect(ruleTwo.update({ key: "key" })).rejects.toThrow(
/key "key" is already in use/
);
await ruleOne.destroy();
await ruleTwo.destroy();
});
test("deleted properties can share the same key, but not with ready rule", async () => {
const ruleOne = await Property.create({
sourceId: source.id,
type: "string",
});
const ruleTwo = await Property.create({
sourceId: source.id,
type: "string",
});
const ruleThree = await Property.create({
sourceId: source.id,
type: "string",
});
expect(ruleOne.key).toBe("");
expect(ruleTwo.key).toBe("");
expect(ruleThree.key).toBe("");
await ruleOne.setOptions({ column: "abc123" });
await ruleTwo.setOptions({ column: "abc123" });
await ruleThree.setOptions({ column: "abc123" });
await ruleOne.update({ state: "ready", key: "asdf" });
await ruleTwo.update({ state: "deleted", key: "asdf-deleted" });
await ruleThree.update({ state: "deleted", key: "asdf-deleted" });
await expect(ruleTwo.update({ key: "asdf" })).rejects.toThrow(
/key "asdf" is already in use/
);
await ruleOne.update({ key: "asdf-deleted" });
await ruleOne.destroy();
await ruleTwo.destroy();
await ruleThree.destroy();
});
test("types must be of a known type", async () => {
const property = await Property.create({
sourceId: source.id,
type: "string",
});
await expect(property.update({ type: "something" })).rejects.toThrow(
/something is not an allowed type/
);
await property.destroy();
});
test("keys cannot be from the reserved list of keys", async () => {
const reservedKeys = ["grouparooId", "grouparooCreatedAt", "_meta"];
for (const i in reservedKeys) {
const key = reservedKeys[i];
await expect(
Property.create({
sourceId: source.id,
type: "string",
key,
})
).rejects.toThrow(/is a reserved key and cannot be used/);
}
});
test("`id` is a valid property key", async () => {
const property = await Property.create({
sourceId: source.id,
type: "string",
key: "id",
}); // does not throw
await property.destroy();
});
test("a property can be isArray", async () => {
const property = await Property.create({
sourceId: source.id,
type: "string",
isArray: true,
});
await property.destroy();
});
test("a property cannot be isArray and unique", async () => {
await expect(
Property.create({
sourceId: source.id,
type: "string",
isArray: true,
unique: true,
})
).rejects.toThrow(/unique record properties cannot be arrays/);
});
test("a property cannot be made unique if there are non-unique values already", async () => {
const property = await Property.create({
sourceId: source.id,
key: "name",
type: "string",
});
await property.setOptions({ column: "name" });
await property.update({ state: "ready" });
const recordA = await helper.factories.record();
const recordB = await helper.factories.record();
const recordC = await helper.factories.record();
await recordA.addOrUpdateProperties({ name: ["mario"] });
await recordB.addOrUpdateProperties({ name: ["toad"] });
await recordC.addOrUpdateProperties({ name: ["toad"] });
await expect(property.update({ unique: true })).rejects.toThrow(
/cannot make this property unique as there are 2 records with the value 'toad'/
);
await recordC.addOrUpdateProperties({ name: ["peach"] });
await property.update({ unique: true }); // does not throw
await recordA.destroy();
await recordB.destroy();
await recordC.destroy();
await property.destroy();
});
});
test("updating a property with new options enqueued an internalRun and update groups relying on it", async () => {
await api.resque.queue.connection.redis.flushdb();
const property = await Property.findOne({ where: { key: "email" } });
const group = await helper.factories.group();
expect(group.state).toBe("ready");
await group.setRules([
{
key: property.key,
operation: { op: "eq" },
match: "abc",
},
]);
const runningRuns = await Run.findAll({
where: { state: "running", creatorType: "group" },
});
expect(runningRuns.length).toBe(1);
await property.setOptions({ column: "id" });
const runningRunsAgain = await Run.findAll({
where: { state: "running", creatorType: "group" },
});
expect(runningRunsAgain.length).toBe(1);
expect(runningRunsAgain[0].id).not.toEqual(runningRuns[0].id);
});
describe("#updateSampleRecords", () => {
let source: Source;
beforeAll(async () => {
source = await helper.factories.source();
await source.setOptions({ table: "some table" });
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
});
afterAll(async () => {
await source.destroy();
process.env.GROUPAROO_RUN_MODE = undefined;
});
test("in cli:config, after creating a property, null properties are built", async () => {
process.env.GROUPAROO_RUN_MODE = "cli:config";
const record = await helper.factories.record();
const property = await Property.create({
sourceId: source.id,
key: "newName",
type: "string",
});
await property.setOptions({ column: "newName" });
await property.update({ state: "ready" });
const properties = await record.getProperties();
expect(properties["newName"]).toBeTruthy();
expect(properties["newName"].state).toBe("pending");
await property.destroy();
await record.destroy();
});
test("in cli:run, after creating a property, null properties are not built in a model hook", async () => {
process.env.GROUPAROO_RUN_MODE = "cli:run";
const record = await helper.factories.record();
const property = await Property.create({
sourceId: source.id,
key: "newName",
type: "string",
});
await property.setOptions({ column: "newName" });
await property.update({ state: "ready" });
const properties = await record.getProperties();
expect(properties["newName"]).toBeFalsy();
await property.destroy();
await record.destroy();
});
});
test("when a property with no options or filters first becomes ready, a run will be started", async () => {
plugin.registerPlugin({
name: "test-plugin-no-options",
apps: [
{
name: "app-no-options",
displayName: "app-no-options",
options: [],
methods: {
test: async () => {
return { success: true };
},
},
},
],
connections: [
{
name: "source-no-options",
displayName: "source-no-options",
description: "a test source",
apps: ["app-no-options"],
direction: "import",
options: [],
methods: {
recordProperty: async () => {
return [];
},
propertyOptions: async () => {
return [];
},
},
},
],
});
const app = await App.create({ type: "app-no-options" });
await app.update({ state: "ready" });
const source = await Source.create({
appId: app.id,
type: "source-no-options",
modelId: model.id,
});
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
const property = await Property.create({
key: "property-no-options",
sourceId: source.id,
type: "boolean",
});
await property.update({ state: "ready" });
const firstRun = await Run.findOne({
where: { creatorId: property.id },
});
expect(firstRun).toBeTruthy();
await firstRun.destroy();
await property.update({ key: "new-key" });
const secondRun = await Run.findOne({
where: { creatorId: property.id },
});
expect(secondRun).toBeNull();
await property.destroy();
await source.destroy();
await app.destroy();
});
test("options can be set and retrieved", async () => {
const property = await Property.findOne({ where: { key: "email" } });
await property.setOptions({ column: "id" });
const options = await property.getOptions();
expect(options).toEqual({ column: "id" });
});
test("__options only includes options for properties", async () => {
const source = await helper.factories.source();
await source.setOptions({ table: "test table" });
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
const property = await Property.create({
id: "myPropertyId",
type: "string",
name: "test property",
sourceId: source.id,
});
await Option.create({
ownerId: property.id,
ownerType: "property",
key: "column",
value: "id",
type: "string",
});
await Option.create({
ownerId: property.id,
ownerType: "source",
key: "someOtherProperty",
value: "someValue",
type: "string",
});
const options = await property.$get("__options");
expect(options.length).toBe(1);
expect(options[0].ownerType).toBe("property");
expect(options[0].key).toBe("column");
await property.destroy();
await source.destroy();
});
test("providing invalid options will result in an error", async () => {
const property = await Property.findOne({
where: { key: "email" },
});
await expect(property.setOptions({ notThing: "abc" })).rejects.toThrow(
/column is required for a property of type test-plugin-import/
);
await expect(
property.setOptions({ column: "id", otherThing: "false" })
).rejects.toThrow(
/otherThing is not an option for a test-plugin-import property/
);
const source = await property.$get("source");
await source.setOptions({ table: "users", tableWithOptions: "users" });
await expect(
property.setOptions({ column: "some_nonexistent_col" })
).rejects.toThrow(
/"some_nonexistent_col" is not a valid value for test-plugin-import property option "column"/
);
});
test("options will have mustache keys converted to mustache ids", async () => {
const property = await Property.findOne({ where: { key: "email" } });
await property.setOptions({
column: "email",
arbitraryText: "{{{ email }}}@example.com",
});
let options = await property.getOptions();
expect(options).toEqual({
column: "email",
arbitraryText: "{{{ email }}}@example.com",
}); //appears normal (but formatted) to the user
const rawOption = await Option.findOne({
where: { key: "arbitraryText", ownerId: property.id },
});
expect(rawOption.value).toBe(`{{{ ${property.id} }}}@example.com`);
});
test("an array property cannot be used as an option", async () => {
const source = await helper.factories.source();
await source.setOptions({ table: "test table" });
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
const cartsProperty = await Property.create({
sourceId: source.id,
key: "carts",
type: "string",
isArray: true,
});
await cartsProperty.setOptions({ column: "carts" });
await cartsProperty.update({ state: "ready" });
const property = await Property.findOne({ where: { key: "email" } });
await expect(
property.setOptions({
column: "{{{carts}}}@example.com",
})
).rejects.toThrow('missing mustache key "carts"');
await cartsProperty.destroy();
await source.destroy();
});
test("a property cannot be created in the ready state with missing required options", async () => {
const source = await helper.factories.source();
const property = Property.build({
sourceId: source.id,
name: "no opts",
type: "string",
state: "ready",
});
await expect(property.save()).rejects.toThrow(
/table is required for a source of type test-plugin-import/
);
await source.destroy();
});
test("if there is no change to options, the internalRun will not be enqueued", async () => {
const property = await Property.findOne({ where: { key: "email" } });
await property.setOptions({ column: "id" });
await api.resque.queue.connection.redis.flushdb();
await property.setOptions({ column: "id" });
const foundInternalRunTasks = await specHelper.findEnqueuedTasks(
"run:internalRun"
);
expect(foundInternalRunTasks.length).toBe(0);
});
test("updating a property's unique property queues a task to update the record properties", async () => {
const source = await helper.factories.source();
await source.setOptions({ table: "test table" });
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
const property = await Property.create({
sourceId: source.id,
key: "thing",
type: "string",
unique: false,
});
// when unique changes
await api.resque.queue.connection.redis.flushdb();
await property.update({ unique: true });
let foundTasks = await specHelper.findEnqueuedTasks(
"property:updateRecordProperties"
);
expect(foundTasks.length).toBe(1);
expect(foundTasks[0].args[0].propertyId).toBe(property.id);
// when something else changes
await api.resque.queue.connection.redis.flushdb();
await property.update({ key: "new name" });
foundTasks = await specHelper.findEnqueuedTasks(
"property:updateRecordProperties"
);
expect(foundTasks.length).toBe(0);
await property.destroy();
await source.destroy();
});
describe("changing property type", () => {
let emailProperty: Property;
beforeAll(async () => {
await Run.truncate();
emailProperty = await Property.findOne({ where: { id: "email" } });
});
afterEach(async () => {
await emailProperty.update({ type: "email" });
process.env.GROUPAROO_RUN_MODE = undefined;
});
test("updating a property's type will enqueue an internal run in most run modes", async () => {
expect(await Run.count()).toBe(0);
await emailProperty.update({ type: "string" });
const run = await Run.findOne();
expect(run.creatorType).toBe("property");
expect(run.creatorId).toBe(emailProperty.id);
});
test("updating a property's type will mark the records and properties pending in cli:config", async () => {
process.env.GROUPAROO_RUN_MODE = "cli:config";
const record = await helper.factories.record();
await RecordProperty.update(
{ state: "ready" },
{ where: { recordId: record.id } }
);
await record.update({ state: "ready" });
const recordProperty = await RecordProperty.findOne({
where: { propertyId: "email", recordId: record.id },
});
expect(recordProperty.state).toBe("ready");
expect(record.state).toBe("ready");
await emailProperty.update({ type: "string" });
await record.reload();
await recordProperty.reload();
expect(recordProperty.state).toBe("pending");
expect(record.state).toBe("pending");
});
});
test("a property cannot be deleted if a group is using it", async () => {
const source = await helper.factories.source();
await source.setOptions({ table: "some table" });
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
const property = await Property.create({
sourceId: source.id,
key: "thing",
type: "string",
unique: false,
});
await property.setOptions({ column: "thing" });
await property.update({ state: "ready" });
const group = await helper.factories.group();
await group.setRules([
{ key: "thing", match: "%", operation: { op: "like" } },
]);
await expect(property.destroy()).rejects.toThrow(
/cannot delete property "thing", group .* is based on it/
);
await group.destroy();
await property.destroy(); // doesn't throw
await source.destroy();
});
test("deleting a property deleted the options", async () => {
const source = await helper.factories.source();
await source.setOptions({ table: "some table" });
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
const property = await Property.create({
sourceId: source.id,
key: "thing",
type: "string",
unique: false,
});
await property.setOptions({ column: "abc" });
await property.destroy(); // doesn't throw
await source.destroy();
const optionsCount = await Option.count({
where: { ownerId: property.id },
});
expect(optionsCount).toBe(0);
});
test("deleting a property does not delete options for other models with the same id", async () => {
const source = await helper.factories.source();
await source.setOptions({ table: "some table" });
await source.setMapping({ id: "userId" });
await source.update({ state: "ready" });
const property = await Property.create({
sourceId: source.id,
key: "thing",
type: "string",
unique: false,
});
await property.setOptions({ column: "abc" });
const foreignOption = await Option.create({
ownerId: property.id,
ownerType: "other",
key: "someKey",
value: "someValue",
type: "string",
});
let count = await Option.count({
where: { ownerId: property.id },
});
expect(count).toBe(2);
await property.destroy();
const options = await Option.findAll({
where: { ownerId: property.id },
});
expect(options.length).toBe(1);
expect(options[0].ownerType).toBe("other");
expect(options[0].key).toBe("someKey");
await foreignOption.destroy();
await source.destroy();
});
describe("with plugin", () => {
let app: App;
let source: Source;
let secondarySource: Source;
let queryCounter = 0;
const propertiesToMoveKeys = Object.freeze([
"userId",
"firstName",
"lastName",
]);
const prevSourceIds: string[] = [];
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-app",
displayName: "import-from-test-app",
description: "a test app",
apps: ["test-template-app"],
direction: "import",
options: [],
methods: {
propertyOptions: async ({ propertyOptions }) => {
const results = [];
results.push({
key: "column",
required: true,
description: "the column to choose",
type: "list" as PluginOptionType,
options: async () => {
const opts = [
{
key: "id",
examples: [1, 2, 3],
},
];
if (
propertyOptions?.column &&
propertyOptions?.column !== "id"
) {
opts.push({
key: propertyOptions.column.toString(),
examples: [1, 2, 3],
});
}
return opts;
},
});
if (propertyOptions?.column === "more") {
results.push({
key: "extra",
required: true,
description: "extra stuff",
type: "text" as PluginOptionType,
options: async () => [] as { key: string }[],
});
}
return results;
},
sourceFilters: async () => {
return [
{
key: "id",
ops: ["gt", "lt"],
canHaveRelativeMatch: false,
},
];
},
recordProperty: async ({ property, propertyOptions, record }) => {
const s = `the time is {{{now.sql}}} + ${JSON.stringify(
propertyOptions
)}`;
const q = await property.parameterizedQueryFromRecord(
s,
record
);
if (propertyOptions.column?.toString().match(/throw/)) {
throw new Error(`throw`);
}
queryCounter++;
return [q];
},
},
},
],
});
app = await App.create({
name: "test app",
type: "test-template-app",
state: "ready",
});
source = await Source.create({
name: "test source",
type: "import-from-test-app",
appId: app.id,
modelId: model.id,
});
await source.update({ state: "ready" });
for (const key of propertiesToMoveKeys) {
const propertyToMove = await Property.findOne({
where: { key },
});
prevSourceIds.push(propertyToMove.sourceId);
propertyToMove.sourceId = source.id;
await propertyToMove.save();
}
secondarySource = await Source.create({
name: "secondary source",
type: "import-from-test-app",
appId: app.id,
modelId: model.id,
});
await secondarySource.update({ state: "ready" });
});
beforeEach(() => {
queryCounter = 0;
});
afterAll(async () => {
for (const key of propertiesToMoveKeys) {
const prevSourceId = prevSourceIds.shift();
const propertyToMove = await Property.findOne({
where: { key },
});
propertyToMove.sourceId = prevSourceId;
await propertyToMove.save();
}
});
describe("primary key", () => {
let userIdProperty: Property;
let emailProperty: Property;
const loadUserIdProperty = async () => {
userIdProperty = await Property.findOne({
where: { key: "userId" },
});
};
const loadEmailProperty = async () => {
emailProperty = await Property.findOne({
where: { key: "myEmail" },
});
};
beforeAll(async () => {
await loadUserIdProperty();
await userIdProperty.update({ unique: true });
emailProperty = await helper.factories.property(
source,
{
key: "myEmail",
unique: true,
},
{ column: "email" }
);
});
afterEach(async () => {
await source.setMapping({});
});
afterAll(async () => {
await emailProperty.destroy();
});
test("primary key is set to true when primary source is mapped to property", async () => {
await source.setMapping({ id: "userId" });
expect(userIdProperty.isPrimaryKey).toBe(true);
});
test("primary key is updated when updating mapping", async () => {
await source.setMapping({ id: "userId" });
expect(userIdProperty.isPrimaryKey).toBe(true);
await source.setMapping({ email: "myEmail" });
await loadEmailProperty();
expect(emailProperty.isPrimaryKey).toBe(true);
await loadUserIdProperty();
expect(userIdProperty.isPrimaryKey).toBe(false);
});
test("property must be unique when primary key is true", async () => {
await source.setMapping({ id: "userId" });
await loadUserIdProperty();
await expect(userIdProperty.update({ unique: false })).rejects.toThrow(
/must be unique because it‘s the model‘s Primary Key/
);
});
});
describe("mapped though a non-unique property", () => {
let property: Property;
let secondaryProperty: Property;
beforeAll(async () => {
property = await helper.factories.property(
source,
{ key: "wordInSpanish" },
{ column: "spanishWord" }
);
secondaryProperty = await helper.factories.property(
secondarySource,
{ key: "company" },
{ column: "company_name" }
);
});
beforeEach(async () => {
await property.update({ unique: false, isArray: false });
await secondaryProperty.update({ unique: false, isArray: false });
});
afterEach(async () => {
await source.setMapping({});
await secondarySource.setMapping({});
});
afterAll(async () => {
await property.destroy();
await secondaryProperty.destroy();
});
test("properties mapped through unique properties can be unique", async () => {
await source.setMapping({ id: "userId" });
await property.update({ unique: true });
expect((await property.reload()).unique).toEqual(true);
});
test("properties mapped through unique properties can be arrays", async () => {
await source.setMapping({ id: "userId" });
await property.update({ isArray: true });
expect((await property.reload()).isArray).toEqual(true);
});
test("properties mapped through non-unique properties cannot be unique", async () => {
await secondarySource.setMapping({ last_name: "lastName" });
await expect(
secondaryProperty.update({ unique: true })
).rejects.toThrow(
/Unique Property .+ cannot be mapped through a non-unique Property/
);
});
});
describe("filters", () => {
test("we can determine if rule's filters have been changed", async () => {
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
await property.setFilters([{ key: "id", match: "0", op: "gt" }]);
const filters = await property.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 property.destroy();
});
test("it can get the filter options from the plugin", async () => {
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
const filterOptions = await FilterHelper.pluginFilterOptions(property);
expect(filterOptions).toEqual([
{
key: "id",
ops: ["gt", "lt"],
canHaveRelativeMatch: false,
},
]);
await property.destroy();
});
test("it will memoize filters as they are set", async () => {
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
await property.setFilters([{ op: "gt", match: 1, key: "id" }]);
expect(property.filters.length).toBe(1);
expect(property.filters[0].op).toBe("gt");
expect(property.filters[0].match).toBe("1");
expect(property.filters[0].key).toBe("id");
await property.destroy();
});
test("it will use memoized filters if they exist", async () => {
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
await property.setFilters([{ op: "gt", match: 999, key: "id" }]);
property.filters = [
Filter.build({
propertyId: property.id,
position: 1,
key: "foo",
match: "-1",
op: "lt",
}),
];
const filters = await property.getFilters();
expect(filters.length).toBe(1);
expect(filters[0].key).toEqual("foo");
expect(filters[0].match).toEqual("-1");
expect(filters[0].op).toEqual("lt");
await property.destroy();
});
test("filters that match the options can be set", async () => {
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
await property.setFilters([
{ op: "gt", match: 1, key: "id" },
{ op: "lt", match: 99, key: "id" },
]);
const filters = await property.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 property.destroy();
});
test("deleting a property also deleted the filters", async () => {
const count = await Filter.count({ where: { ownerType: "property" } });
expect(count).toBe(0);
});
test("filters that do not match the options cannot be set", async () => {
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
await expect(
property.setFilters([{ op: "gt", match: 1, key: "other-key" }])
).rejects.toThrow("other-key is not filterable");
await expect(
// @ts-ignore
property.setFilters([{ op: "max it out", match: 1, key: "id" }])
).rejects.toThrow('"max it out" cannot be applied to id');
await property.destroy();
});
});
describe("options", () => {
test.each(["deleted", "ready"])(
"properties can retrieve their options from the %p source",
async (state) => {
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
await source.update({ state });
await app.update({ state });
const pluginOptions = await property.pluginOptions();
expect(pluginOptions).toEqual([
{
description: "the column to choose",
key: "column",
options: [{ examples: [1, 2, 3], key: "id" }],
required: true,
type: "list",
},
]);
await property.destroy();
await source.update({ state: "ready" });
await app.update({ state: "ready" });
}
);
test("creating or editing a property options will test the query against a record", async () => {
expect(queryCounter).toBe(0);
const record = await helper.factories.record();
await record.addOrUpdateProperties({ userId: [1000] });
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
await property.setOptions({ column: "test" });
await property.update({ state: "ready" });
// not ready yet
await property.update({ state: "ready" });
// initial test
expect(queryCounter).toBeGreaterThanOrEqual(2);
await property.setOptions({ column: "id" });
// +2 checking the options
// +2 from the afterSave hook updating the rule
// +n for the mustache builder
expect(queryCounter).toBeGreaterThan(2);
await expect(property.setOptions({ column: "throw" })).rejects.toThrow(
/throw/
);
// no change
expect(queryCounter).toBeGreaterThan(2);
await property.destroy();
await record.destroy();
});
test("options cannot be saved if they fail testing import against a record", async () => {
const record = await helper.factories.record();
await record.addOrUpdateProperties({ userId: [1000] });
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
await expect(property.setOptions({ column: "throw" })).rejects.toThrow(
/throw/
);
expect(await property.getOptions()).toEqual({});
await property.destroy();
await record.destroy();
});
test("the property can be tested against the existing options or potential new options", async () => {
const record = await helper.factories.record();
await record.addOrUpdateProperties({ userId: [1000] });
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
await property.setOptions({ column: "~" });
await property.update({ state: "ready" });
await record.addOrUpdateProperties({ test: [true] });
// against saved query
const response = await property.test();
expect(response[0]).toMatch(`+ {"column":"~"}`);
// against new query
const responseAgain = await property.test({ column: "abc" });
expect(responseAgain[0]).toMatch('+ {"column":"abc"}');
await record.destroy();
await property.destroy();
});
test("options will be dynamically validated", async () => {
const property = await Property.create({
key: "test-with-extra",
type: "string",
sourceId: source.id,
});
await expect(property.setOptions({ column: "more" })).rejects.toThrow(
/extra is required for a property/
);
await expect(property.update({ state: "ready" })).rejects.toThrow();
});
test("apiData will include the options", async () => {
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
await property.setOptions({ column: "id" });
const apiData = await property.apiData();
expect(apiData.options).toEqual({ column: "id" });
await property.destroy();
});
});
test("apiData will include the source", async () => {
const property = await Property.create({
key: "test",
type: "string",
sourceId: source.id,
});
const apiData = await property.apiData();
expect(apiData.sourceId).toEqual(source.id);
await property.destroy();
});
});
});