@grouparoo/core
Version:
The Grouparoo Core
1,207 lines (1,016 loc) • 39.9 kB
text/typescript
import { helper } from "@grouparoo/spec-helper";
import { cache } from "actionhero";
import { DestinationOps } from "../../../src/modules/ops/destination";
import {
App,
Destination,
DestinationGroupMembership,
Export,
Group,
GrouparooModel,
GroupMember,
Mapping,
Option,
plugin,
Run,
} from "../../../src";
import { ExportOps } from "../../../src/modules/ops/export";
describe("models/destination", () => {
helper.grouparooTestServer({ truncate: true, enableTestPlugin: true });
describe("with apps", () => {
let model: GrouparooModel;
let app: App;
let destination: Destination;
beforeAll(async () => {
({ model } = await helper.factories.properties());
app = await helper.factories.app();
});
afterEach(async () => {
if (destination) {
await destination.destroy();
}
});
test("a destination can be created, and it can find the related app", async () => {
destination = await Destination.create({
name: "test destination",
type: "test-plugin-export",
syncMode: "sync",
appId: app.id,
modelId: model.id,
});
expect(destination.id.length).toBe(40);
expect(destination.createdAt).toBeTruthy();
expect(destination.updatedAt).toBeTruthy();
const options = await destination.getOptions();
expect(options).toEqual({});
const _app = await destination.$get("app");
expect(_app.id).toBe(app.id);
});
test("a new destination will have a '' name", async () => {
destination = await Destination.create({
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
});
expect(destination.name).toBe("");
await destination.destroy();
});
test("draft destination can share the same name, but not with ready destination", async () => {
const destinationOne = await Destination.create({
type: "test-plugin-export",
syncMode: "sync",
appId: app.id,
modelId: model.id,
});
const otherApp = await helper.factories.app();
const destinationTwo = await Destination.create({
type: "test-plugin-export",
syncMode: "sync",
appId: otherApp.id,
modelId: model.id,
});
expect(destinationOne.name).toBe("");
expect(destinationTwo.name).toBe("");
await destinationOne.update({ name: "name" });
await destinationOne.setOptions({ table: "users_out" });
await destinationOne.update({ state: "ready" });
await expect(destinationTwo.update({ name: "name" })).rejects.toThrow(
/name "name" is already in use/
);
await destinationOne.destroy();
await destinationTwo.destroy();
await otherApp.destroy();
});
test("deleted destination can share the same name, but not with ready destination", async () => {
const destinationOne = await Destination.create({
type: "test-plugin-export",
syncMode: "sync",
appId: app.id,
modelId: model.id,
});
const otherApp = await helper.factories.app();
const destinationTwo = await Destination.create({
type: "test-plugin-export",
syncMode: "sync",
appId: otherApp.id,
modelId: model.id,
});
const otherOtherApp = await helper.factories.app();
const destinationThree = await Destination.create({
type: "test-plugin-export",
syncMode: "sync",
appId: otherOtherApp.id,
modelId: model.id,
});
expect(destinationOne.name).toBe("");
expect(destinationTwo.name).toBe("");
expect(destinationThree.name).toBe("");
await destinationOne.setOptions({ table: "users_out" });
await destinationOne.update({ state: "ready" });
await destinationTwo.update({ state: "deleted", name: "asdf" });
await destinationThree.update({ state: "deleted", name: "asdf" });
await destinationOne.update({ name: "asdf" });
await expect(destinationTwo.update({ name: "asdf" })).rejects.toThrow(
/name "asdf" is already in use/
);
await destinationOne.destroy();
await destinationTwo.destroy();
await otherApp.destroy();
await destinationThree.destroy();
await otherOtherApp.destroy();
});
test("a destination cannot be created with a deleted state model", async () => {
await model.update({ state: "deleted" });
const destination = new Destination({
type: "test-plugin-export",
syncMode: "sync",
appId: app.id,
modelId: model.id,
});
await expect(destination.save()).rejects.toThrow(
/cannot find ready model with id/
);
await model.update({ state: "ready" });
});
test("a deleted destination can be saved with a deleted state model", async () => {
const destination = new Destination({
type: "test-plugin-export",
syncMode: "sync",
appId: app.id,
modelId: model.id,
});
await destination.save();
await model.update({ state: "deleted" });
await destination.update({ name: "abc", state: "deleted" });
expect(destination.name).toBe("abc");
await model.update({ state: "ready" });
});
test("deleting a destination deletes related models", async () => {
const group = await helper.factories.group();
destination = await Destination.create({
name: "bye destination",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
});
await destination.setOptions({ table: "users_out" });
await destination.setMapping({ "primary-id": "userId" });
await destination.updateTracking("group", group.id);
const destinationGroupMemberships: Record<string, string> = {};
destinationGroupMemberships[group.id] = "remote-tag";
await destination.setDestinationGroupMemberships(
destinationGroupMemberships
);
await destination.updateTracking("none");
await destination.destroy();
let count = await DestinationGroupMembership.count({
where: { destinationId: destination.id },
});
expect(count).toBe(0);
count = await Option.count({
where: { ownerId: destination.id },
});
expect(count).toBe(0);
count = await Mapping.count({
where: { ownerId: destination.id },
});
expect(count).toBe(0);
await group.destroy();
});
test("deleting a destination sets related export's destinationId to null", async () => {
destination = await Destination.create({
name: "bye destination",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
});
const _export = await helper.factories.export(null, destination);
await ExportOps.completeBatch([_export]);
expect(_export.destinationId).toBe(destination.id);
await destination.destroy();
await _export.reload();
expect(_export.destinationId).toBeNull();
});
test("destinations require a valid modelId", async () => {
expect(
Destination.create({
name: "bye destination",
type: "test-plugin-export",
appId: app.id,
modelId: "foo",
})
).rejects.toThrow(/cannot find model with id "foo"/);
});
test("destinations cannot change models", async () => {
destination = await Destination.create({
name: "bye destination",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
});
await expect(destination.update({ modelId: "foo" })).rejects.toThrow(
/cannot change models/
);
await destination.destroy();
});
test("deleting a destination does not delete options for other models with the same id", async () => {
destination = await Destination.create({
name: "some destination",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
});
await destination.setOptions({ table: "users_out" });
const foreignOption = await Option.create({
ownerId: destination.id,
ownerType: "other",
key: "someKey",
value: "someValue",
type: "string",
});
let count = await Option.count({
where: { ownerId: destination.id },
});
expect(count).toBe(2);
await destination.destroy();
const options = await Option.findAll({
where: { ownerId: destination.id },
});
expect(options.length).toBe(1);
expect(options[0].ownerType).toBe("other");
expect(options[0].key).toBe("someKey");
await foreignOption.destroy();
});
test("destinations can retrieve related export totals", async () => {
destination = await Destination.create({
name: "bye destination",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
});
const record = await helper.factories.record();
const _export = await Export.create({
destinationId: destination.id,
recordId: record.id,
oldRecordProperties: {},
newRecordProperties: {},
oldGroups: [],
newGroups: [],
});
const totals = await destination.getExportTotals();
expect(totals).toEqual({
canceled: 0,
complete: 0,
draft: 0,
failed: 0,
pending: 1,
processing: 0,
});
await _export.destroy();
await destination.destroy();
await record.destroy();
});
test.each(["deleted", "ready"])(
"a destination can get options from a connection with a %p app",
async (appState) => {
await app.update({ state: appState });
const connectionOptions =
await destination.destinationConnectionOptions();
expect(connectionOptions).toEqual({
tableWithOptions: {
type: "list",
options: ["users_out", "users", "groups"],
},
});
}
);
test("partial options will be passed to destinationConnectionOptions", async () => {
const connectionOptions = await destination.destinationConnectionOptions({
options: "true",
});
expect(connectionOptions).toEqual({
tableWithOptions: {
type: "list",
options: ["users_out", "users", "groups"],
},
receivedOptions: { type: "text", options: ["true"] },
});
});
describe("options trigger runs", () => {
const newTable = "users_out";
let destination: Destination;
let group: Group;
let run: Run;
beforeAll(async () => {
destination = await Destination.create({
name: "incoming destination",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
});
group = await helper.factories.group();
await destination.updateTracking("group", group.id);
});
afterAll(async () => {
await destination.updateTracking("none");
await destination.destroy();
await group.destroy();
});
test("setting a destination's options triggers a group run", async () => {
await destination.setOptions({ table: newTable });
const runningRuns = await Run.findAll({
where: { state: "running", creatorType: "group" },
});
expect(runningRuns.length).toBe(1);
expect(runningRuns[0].destinationId).toBe(destination.id);
run = runningRuns[0];
});
test("re-setting options with the same value will not trigger a group run ", async () => {
await destination.setOptions({ table: newTable });
await destination.setOptions({ table: newTable });
await destination.setOptions({ table: newTable });
const runningRuns = await Run.findAll({
where: { state: "running", creatorType: "group" },
});
expect(runningRuns.length).toEqual(1);
expect(runningRuns[0].id).toEqual(run.id);
});
});
describe("options from environment variables", () => {
beforeAll(() => {
process.env.GROUPAROO_OPTION__DESTINATION__TEST_OPTION = "users_out";
});
test("options can be set from an environment variable but not stored in the database", async () => {
const destination = await Destination.create({
name: "incoming destination",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
});
await destination.setOptions({ table: "TEST_OPTION" });
const options = await destination.getOptions();
expect(options.table).toBe("users_out");
const option = await Option.findOne({
where: { ownerId: destination.id, key: "table" },
});
expect(option.value).toBe("TEST_OPTION");
await destination.destroy();
});
afterAll(() => {
process.env.GROUPAROO_OPTION__DESTINATION__TEST_OPTION = undefined;
});
});
describe("validations", () => {
describe("with additional plugin", () => {
beforeAll(async () => {
plugin.registerPlugin({
name: "test-other-plugin",
apps: [
{
name: "test-other-plugin-app",
displayName: "test-other-plugin-app",
options: [],
methods: {
test: async () => {
return { success: true };
},
},
},
],
});
});
test("the app must be of compatible type", async () => {
const otherApp = await App.create({
name: "my other app",
type: "test-other-plugin-app",
});
await otherApp.update({ state: "ready" });
await expect(
Destination.create({
type: "test-plugin-import",
name: "test destination",
appId: otherApp.id,
modelId: model.id,
})
).rejects.toThrow(
/Destination of type \"test-plugin-import\" does not support the App .* of type \"test-other-plugin-app\". Supported App types: test-plugin-app./
);
});
});
test("a destination requires a plugin connection", async () => {
await expect(
Destination.create({
type: "missing-destination",
name: "test destination",
appId: app.id,
modelId: model.id,
})
).rejects.toThrow(
/Cannot find a \"missing-destination\" connection available within the installed plugins. Current connections installed are:/
);
});
test("__options only includes options for destinations", async () => {
const destination = await Destination.create({
id: "myDestinationId",
type: "test-plugin-export",
name: "test property",
appId: app.id,
modelId: model.id,
});
await Option.create({
ownerId: destination.id,
ownerType: "destination",
key: "table",
value: "users",
type: "string",
});
await Option.create({
ownerId: destination.id,
ownerType: "app",
key: "someOtherProperty",
value: "someValue",
type: "string",
});
const options = await destination.$get("__options");
expect(options.length).toBe(1);
expect(options[0].ownerType).toBe("destination");
expect(options[0].key).toBe("table");
await destination.destroy();
});
test("options must match the app options (extra options needed by connection)", async () => {
destination = new Destination({
name: "incoming destination - too many options",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
});
await destination.save();
expect(destination.id).toBeTruthy();
await expect(
destination.setOptions({
something: "abc123",
table: "users_out",
primaryKey: "userID",
groupsTable: "groups",
groupForeignKey: "userId",
groupColumnName: "groupName",
})
).rejects.toThrow(
/something is not an option for a test-plugin-export destination/
);
});
test("a valid value for options with multiple choices must be provided ", async () => {
const destination = await helper.factories.destination();
expect(destination.id).toBeTruthy();
await expect(
destination.setOptions({
table: "users_out",
tableWithOptions: "something_else",
})
).rejects.toThrow(
/"something_else" is not a valid value for test-plugin-export destination option "tableWithOptions"/
);
});
test("required mappings must be included in the mappings", async () => {
destination = await helper.factories.destination();
await expect(
destination.setMapping({
local_first_name: "firstName",
})
).rejects.toThrow(/primary-id is a required/);
});
test("required mappings with a type can only use record properties that match the type", async () => {
destination = await helper.factories.destination();
await expect(
destination.setMapping({
"primary-id": "email",
})
).rejects.toThrow(/primary-id requires a property of type integer/);
});
test("mappings must map to properties", async () => {
destination = await helper.factories.destination();
await destination.setMapping({
"primary-id": "userId",
local_first_name: "firstName",
});
const mapping = await destination.getMapping();
expect(mapping).toEqual({
"primary-id": "userId",
local_first_name: "firstName",
});
});
test("mapping data is cached", async () => {
destination = await helper.factories.destination();
const cacheKey = `destination:${destination.id}:mappingOptions`;
await expect(cache.load(cacheKey)).rejects.toThrow();
await destination.setMapping({
"primary-id": "userId",
local_first_name: "firstName",
});
await destination.getMapping();
const response = await cache.load(cacheKey);
expect(response.value).not.toBeFalsy();
});
test("it throws an error if the mapping does not include the key of a recordPropertyRyle", async () => {
destination = await helper.factories.destination();
await expect(
destination.setMapping({
"primary-id": "TheUserID",
})
).rejects.toThrow(/cannot find property TheUserID/);
});
test("a destination cannot be created in the ready state with missing required options", async () => {
const destination = Destination.build({
appId: app.id,
type: "test-plugin-export",
syncMode: "sync",
state: "ready",
modelId: model.id,
});
await expect(destination.save()).rejects.toThrow(/table is required/);
});
test("a destination cannot be changed to to the ready state if there are missing required options", async () => {
destination = await Destination.build({
appId: app.id,
name: "missing options",
type: "test-plugin-export",
});
await expect(destination.update({ state: "ready" })).rejects.toThrow();
});
test("a destination that is ready cannot move back to draft", async () => {
destination = await helper.factories.destination();
await destination.setOptions({ table: "users_out" });
await destination.update({ state: "ready" });
await expect(destination.update({ state: "draft" })).rejects.toThrow(
/cannot transition destination state from ready to draft/
);
});
test("an app can only have one destination per connection with the same options and group tracking", async () => {
destination = await Destination.create({
name: "first destination",
appId: app.id,
modelId: model.id,
type: "test-plugin-export",
groupId: "abc123",
collection: "group",
});
await expect(
Destination.create({
name: "second destination",
appId: app.id,
modelId: model.id,
type: "test-plugin-export",
groupId: "abc123",
collection: "group",
})
).rejects.toThrow(
/destination "first destination" .* is already using this app with the same options/
);
// but it's ok to have the same app track a different group
const differentGroupDestination = await Destination.create({
name: "different group destination",
appId: app.id,
modelId: model.id,
type: "test-plugin-export", // same
groupId: "def456", // different
collection: "group",
});
expect(differentGroupDestination.state).toEqual("draft");
// but it's ok to have one in another type of connection
const ok = await Destination.create({
name: "ok destination",
appId: app.id,
modelId: model.id,
type: "test-plugin-export-batch", // different
groupId: "abc123", // same group
collection: "group",
});
expect(ok.state).toEqual("draft");
await destination.setOptions({ table: "users" });
const otherDestination = await Destination.create({
name: "second destination",
appId: app.id,
modelId: model.id,
type: "test-plugin-export", // same
groupId: "abc123", // same
collection: "group",
}); // does not throw, as first destination now has new options
await otherDestination.setOptions({ table: "groups" }); // does not throw
await expect(
otherDestination.setOptions({ table: "users" })
).rejects.toThrow(
/destination "first destination" .* is already using this app with the same options/
);
await differentGroupDestination.destroy();
await otherDestination.destroy();
// and it's ok to have the options here
await ok.setOptions({ table: "users" });
await ok.destroy();
});
test("an app can only have one destination per connection with the same options and model tracking", async () => {
destination = await Destination.create({
name: "first destination",
appId: app.id,
modelId: model.id,
type: "test-plugin-export",
collection: "model",
});
await expect(
Destination.create({
name: "second destination",
appId: app.id,
modelId: model.id,
type: "test-plugin-export",
collection: "model",
})
).rejects.toThrow(
/destination "first destination" .* is already using this app with the same options/
);
// but it's ok to have one in another type of connection
const ok = await Destination.create({
name: "ok destination",
appId: app.id,
modelId: model.id,
type: "test-plugin-export-batch", // different
collection: "model",
});
expect(ok.state).toEqual("draft");
await destination.setOptions({ table: "users" });
const otherDestination = await Destination.create({
name: "second destination",
appId: app.id,
modelId: model.id,
type: "test-plugin-export", // same
collection: "model",
}); // does not throw, as first destination now has new options
await otherDestination.setOptions({ table: "groups" }); // does not throw
await expect(
otherDestination.setOptions({ table: "users" })
).rejects.toThrow(
/destination "first destination" .* is already using this app with the same options/
);
await otherDestination.destroy();
// and it's ok to have the options here
await ok.setOptions({ table: "users" });
await ok.destroy();
});
test("a destination cannot be created in the ready state with missing syncMode", async () => {
const destination = Destination.build({
appId: app.id,
modelId: model.id,
type: "test-plugin-export",
state: "ready",
});
await expect(destination.save()).rejects.toThrow(
/Sync mode is required/
);
});
test("a destination can be ready without syncMode if a default has been defined", async () => {
const destination = await Destination.create({
appId: app.id,
modelId: model.id,
type: "test-plugin-export-batch", //has default mode "additive"
});
await destination.setOptions({ table: "users" });
await destination.update({ state: "ready" });
const syncMode = await destination.getSyncMode();
expect(syncMode).toBe("additive");
await destination.destroy();
});
test("destination syncMode must be set on ready", async () => {
const destination = await Destination.create({
appId: app.id,
modelId: model.id,
type: "test-plugin-export",
});
await destination.setOptions({ table: "users" });
await expect(destination.update({ state: "ready" })).rejects.toThrow(
/Sync mode is required/
);
await destination.destroy();
});
test("a destination cannot set an unsupported sync mode", async () => {
const destination = await Destination.create({
appId: app.id,
modelId: model.id,
type: "test-plugin-export",
syncMode: "RandomSyncMode",
});
await destination.setOptions({ table: "users" });
await expect(destination.update({ state: "ready" })).rejects.toThrow(
/does not support sync mode/
);
await destination.destroy();
});
});
describe("tracking a model", () => {
let destination: Destination;
let model: GrouparooModel;
beforeEach(async () => {
model = await GrouparooModel.findOne();
destination = await Destination.create({
name: "test destination",
appId: app.id,
modelId: "mod_profiles",
syncMode: "sync",
type: "test-plugin-export",
});
});
afterEach(async () => {
await destination.destroy();
});
test("a model can be tracked", async () => {
const { newRun } = await destination.updateTracking("model");
expect(newRun.destinationId).toEqual(destination.id);
expect(destination.collection).toBe("model");
expect(destination.modelId).toBe(model.id);
});
test("tracking a model will enqueue runs", async () => {
const { newRun, oldRun } = await destination.updateTracking("model");
expect(oldRun).toBeFalsy();
expect(newRun).toBeTruthy();
});
test("a destination cannot track an unrelated model", async () => {
const otherModel = await helper.factories.model({
id: "otherModel",
name: "Other Model",
type: "profile",
});
const otherGroup = await helper.factories.group({
modelId: otherModel.id,
});
await expect(
destination.updateTracking("group", otherGroup.id)
).rejects.toThrow(/do not share the same modelId/);
await otherGroup.destroy();
await otherModel.destroy();
});
test("a model can be unTracked", async () => {
await destination.updateTracking("model");
const { oldRun, newRun } = await destination.updateTracking("none");
expect(oldRun.destinationId).toEqual(destination.id);
expect(newRun).toBeFalsy();
expect(destination.collection).toBe("none");
expect(destination.groupId).toBe(null);
});
test("recordPreview - without updates - with model", async () => {
const record = await helper.factories.record();
await record.addOrUpdateProperties({
userId: [1],
email: ["yoshi@example.com"],
});
await destination.updateTracking("model");
const mapping = {
"primary-id": "userId",
email: "email",
};
const destinationGroupMemberships: Record<string, string> = {};
const _record = await destination.recordPreview(
record,
mapping,
destinationGroupMemberships
);
expect(_record.properties["primary-id"].values[0]).toBe(1);
expect(_record.properties["email"].values[0]).toBe("yoshi@example.com");
await record.destroy();
});
test("destination record previews will convert the type of the property to match the destination", async () => {
const record = await helper.factories.record();
await record.addOrUpdateProperties({
userId: [1],
email: ["yoshi@example.com"],
ltv: [123],
});
await destination.updateTracking("model");
const mapping = {
"primary-id": "userId",
"string-property": "ltv",
};
const _record = await destination.recordPreview(record, mapping, {});
expect(_record.properties["primary-id"].values[0]).toBe(1);
expect(_record.properties["primary-id"].type).toBe("integer");
expect(_record.properties["string-property"].values[0]).toBe("123");
expect(_record.properties["string-property"].type).toBe("string");
await record.destroy();
});
});
describe("with groups", () => {
let destination: Destination;
let group: Group;
beforeEach(async () => {
destination = await Destination.create({
name: "test destination",
appId: app.id,
modelId: model.id,
syncMode: "sync",
type: "test-plugin-export",
});
group = await helper.factories.group();
});
afterEach(async () => {
await group.destroy();
await destination.destroy();
});
test("a group can be tracked", async () => {
await destination.updateTracking("group", group.id);
const _group = await destination.$get("group");
expect(_group.id).toBe(group.id);
});
test("a group cannot be tracked in any other collection", async () => {
await expect(
destination.updateTracking("none", group.id)
).rejects.toThrow(/cannot track/);
await expect(
destination.updateTracking("model", group.id)
).rejects.toThrow(/cannot track/);
});
test("tracking a group will enqueue runs", async () => {
const { oldRun, newRun } = await destination.updateTracking(
"group",
group.id
);
expect(oldRun).toBeFalsy();
expect(newRun).toBeTruthy();
});
test("a destination can only track a single group", async () => {
const newGroup = await helper.factories.group();
await destination.updateTracking("group", group.id);
await destination.updateTracking("group", newGroup.id);
const _group = await destination.$get("group");
expect(_group.id).toBe(newGroup.id);
await newGroup.destroy();
});
test("a destination cannot track a deleted group", async () => {
await group.update({ state: "deleted" });
await expect(
destination.updateTracking("group", group.id)
).rejects.toThrow(/cannot track deleted Group/);
const _group = await destination.$get("group");
expect(_group).toBe(null);
});
test("a group can be unTracked", async () => {
await destination.updateTracking("group", group.id);
let _group = await destination.$get("group");
expect(_group.id).toBe(group.id);
await destination.updateTracking("none");
_group = await destination.$get("group");
expect(_group).toBe(null);
});
test("recordPreview - without updates - with group", async () => {
const record = await helper.factories.record();
await record.addOrUpdateProperties({
userId: [1],
email: ["yoshi@example.com"],
});
await GroupMember.create({ recordId: record.id, groupId: group.id });
await destination.updateTracking("group", group.id);
const mapping = {
"primary-id": "userId",
email: "email",
};
const destinationGroupMemberships: Record<string, string> = {};
destinationGroupMemberships[group.id] = "another-group-tag";
const _record = await destination.recordPreview(
record,
mapping,
destinationGroupMemberships
);
expect(_record.properties["primary-id"].values[0]).toBe(1);
expect(_record.properties["email"].values[0]).toBe("yoshi@example.com");
expect(_record.groupNames).toEqual(["another-group-tag"]);
await record.destroy();
});
test("destination record previews will convert the type of the property to match the destination", async () => {
const record = await helper.factories.record();
await record.addOrUpdateProperties({
userId: [1],
email: ["yoshi@example.com"],
ltv: [123],
});
await GroupMember.create({ recordId: record.id, groupId: group.id });
await destination.updateTracking("group", group.id);
const mapping = {
"primary-id": "userId",
"string-property": "ltv",
};
const _record = await destination.recordPreview(record, mapping, {});
expect(_record.properties["primary-id"].values[0]).toBe(1);
expect(_record.properties["primary-id"].type).toBe("integer");
expect(_record.properties["string-property"].values[0]).toBe("123");
expect(_record.properties["string-property"].type).toBe("string");
await record.destroy();
});
describe("relevantFor", () => {
it("determined relevant destinations for a record", async () => {
const otherGroupDestination = await helper.factories.destination();
const modelDestination = await helper.factories.destination();
const otherGroup = await helper.factories.group();
const irrelevantDestination = await helper.factories.destination();
await irrelevantDestination.updateTracking("group", otherGroup.id);
const record = await helper.factories.record();
await GroupMember.create({ recordId: record.id, groupId: group.id });
// before the destinations are ready
await destination.updateTracking("group", group.id);
let destinations = await DestinationOps.relevantFor(
record,
await record.$get("groups")
);
expect(destinations.length).toBe(0);
// after the destinations are ready
await destination.setOptions({ table: "users" });
await destination.update({ state: "ready" });
await otherGroupDestination.setOptions({ table: "users" });
await otherGroupDestination.updateTracking("group", group.id);
await modelDestination.updateTracking("model");
destinations = await DestinationOps.relevantFor(
record,
await record.$get("groups")
);
expect(destinations.length).toBe(3);
expect(destinations.map((d) => d.id).sort()).toEqual(
[
destination.id,
otherGroupDestination.id,
modelDestination.id,
].sort()
);
expect(destinations.map((d) => d.id).sort()).not.toContain(
irrelevantDestination.id
);
await destination.updateTracking("none");
await GroupMember.destroy({ where: { recordId: record.id } });
await otherGroupDestination.updateTracking("none");
await otherGroupDestination.destroy();
await modelDestination.updateTracking("none");
await modelDestination.destroy();
await irrelevantDestination.updateTracking("none");
await irrelevantDestination.destroy();
await otherGroup.destroy();
});
test("when the group being tracked is removed, the previous group should be exported one last time", async () => {
const { newRun: runA } = await destination.updateTracking(
"group",
group.id
);
const { oldRun: runB } = await destination.updateTracking("none");
await runA.reload();
await runB.reload();
expect(runA.creatorId).toEqual(runB.creatorId);
expect(runA.state).toBe("stopped");
expect(runB.state).toBe("running");
expect(runA.destinationId).toBe(destination.id);
expect(runB.destinationId).toBe(destination.id);
});
test("when the model being tracked is removed, the previous records should be exported one last time", async () => {
const { newRun: runA } = await destination.updateTracking("model");
const { oldRun: runB } = await destination.updateTracking("none");
await runA.reload();
await runB.reload();
expect(runA.creatorId).toEqual(runB.creatorId);
expect(runA.state).toBe("stopped");
expect(runB.state).toBe("running");
expect(runA.destinationId).toBe(destination.id);
expect(runB.destinationId).toBe(destination.id);
});
test("when the group being tracked is changed, the previous group should be exported one last time", async () => {
const otherGroup = await helper.factories.group();
await otherGroup.update({ state: "ready" });
const { newRun: runA } = await destination.updateTracking(
"group",
group.id
);
const { newRun: runB } = await destination.updateTracking(
"group",
otherGroup.id
);
expect(runA.creatorId).toEqual(group.id);
expect(runB.creatorId).toEqual(otherGroup.id);
expect(runA.state).toBe("running");
expect(runB.state).toBe("running");
expect(runB.destinationId).toBe(destination.id);
await otherGroup.destroy();
});
});
});
describe("parameterizedOptions", () => {
it("replaces mustache template strings with variables", async () => {
destination = new Destination({
name: "outgoing pg destination",
type: "test-plugin-export",
appId: app.id,
modelId: model.id,
});
await destination.save();
await destination.setOptions({
table: "users",
where: "thing-{{{ now.sql }}}",
});
const parameterizedOptions = await destination.parameterizedOptions();
const now = new Date();
expect(parameterizedOptions.where).toMatch(
`thing-${now.getFullYear()}`
);
});
});
});
});