UNPKG

@grouparoo/core

Version:
1,316 lines (1,104 loc) 39.6 kB
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(); }); }); });