UNPKG

@grouparoo/core

Version:
1,110 lines (966 loc) 38.5 kB
import fs from "fs"; import glob from "glob"; import path from "path"; import os from "os"; import faker from "@faker-js/faker"; import * as uuid from "uuid"; import { helper } from "@grouparoo/spec-helper"; import { App } from "../../src/models/App"; import { Destination } from "../../src/models/Destination"; import { Group } from "../../src/models/Group"; import { Property } from "../../src/models/Property"; import { Schedule } from "../../src/models/Schedule"; import { Setting } from "../../src/models/Setting"; import { Source } from "../../src/models/Source"; import { GrouparooModel } from "../../src/models/GrouparooModel"; import { GrouparooRecord } from "../../src/models/GrouparooRecord"; import { ConfigWriter } from "../../src/modules/configWriter"; import { MappingHelper } from "../../src/modules/mappingHelper"; import { AnyConfigurationObject, ScheduleConfigurationObject, SourceConfigurationObject, } from "../../src/classes/codeConfig"; const workerId = process.env.JEST_WORKER_ID; const configDir = `${os.tmpdir()}/test/${workerId}/configWriter`; const configFilePattern = path.join(configDir, "**/*.{json,js}"); process.env.GROUPAROO_CONFIG_DIR = configDir; const getClusterNameSetting = async (): Promise<Setting> => { const setting = await Setting.findOne({ where: { key: "cluster-name" }, }); return setting; }; const changeClusterNameSetting = async () => { const setting: Setting = await getClusterNameSetting(); await setting.update({ value: faker.lorem.words(3) }); return setting; }; const resetClusterNameSetting = async () => { const setting: Setting = await getClusterNameSetting(); await setting.update({ value: setting.defaultValue }); return setting; }; describe("modules/configWriter", () => { helper.grouparooTestServer({ truncate: true, enableTestPlugin: true, resetSettings: true, }); beforeEach(async () => { process.env.GROUPAROO_RUN_MODE = "cli:config"; }); afterEach(async () => { process.env.GROUPAROO_RUN_MODE = undefined; }); // ---------------------------------------- | ConfigWriter.generateId() describe("generateId()", () => { test("it returns a slugified, lowercase name", () => { let res = ConfigWriter.generateId("Hello World"); expect(res).toEqual("hello_world"); res = ConfigWriter.generateId("!@#$%^&*(){}[]:\";'<>,./? Hello World"); expect(res).toEqual("hello_world"); }); test("it returns undefined when no name is passed", () => { expect(ConfigWriter.generateId(null)).toBeUndefined(); }); test("it returns undefined when name is an empty string", () => { expect(ConfigWriter.generateId("")).toBeUndefined(); }); test("it throws an error when name does not have url-friendly characters", () => { expect(() => { ConfigWriter.generateId("!"); }).toThrow("Could not generate ID from name."); }); test("it preserves hyphens", () => { expect(ConfigWriter.generateId("hello-world")).toEqual("hello-world"); }); test("it replaces multiple word boundaries with a single underscore", () => { expect(ConfigWriter.generateId("Hello - World")).toEqual("hello_world"); }); }); // ---------------------------------------- | ConfigWriter.generateFilePath() describe("generateFilePath()", () => { afterEach(async () => { await App.destroy({ truncate: true }); }); test("provides a url-friendly path from the name in the object", async () => { const app: App = await helper.factories.app({ name: "HELLO @#$ WORLD" }); const configObject = await app.getConfigObject(); const res = ConfigWriter.generateFilePath(configObject); expect(res).toEqual("hello_world.json"); }); test("returns undefined if the object has no id value", async () => { const res = ConfigWriter.generateFilePath({ name: "Hello World" }); expect(res).toBeUndefined(); }); test("supports objects as arrays, and uses the first one to build the name", async () => { const app: App = await helper.factories.app({ name: "HELLO @#$ WORLD" }); const configObject = await app.getConfigObject(); const res = ConfigWriter.generateFilePath([configObject]); expect(res).toEqual("hello_world.json"); }); test("adds a prefix, if specified", async () => { const app: App = await helper.factories.app({ name: "HELLO @#$ WORLD" }); const configObject = await app.getConfigObject(); const res = ConfigWriter.generateFilePath(configObject, "apps"); expect(res).toEqual("apps/hello_world.json"); }); }); // ---------------------------------------- | ConfigWriter.run() describe("run()", () => { afterEach(async () => { await GrouparooModel.destroy({ truncate: true }); await App.destroy({ truncate: true }); await Source.destroy({ truncate: true }); await Property.destroy({ truncate: true }); const files = glob.sync(configFilePattern); for (let file of files) fs.rmSync(file); ConfigWriter.resetConfigFileCache(); }); test("does nothing unless in cli:config mode", async () => { const app: App = await helper.factories.app(); const appFilePath = path.join( configDir, `apps/${app.getConfigId()}.json` ); process.env.GROUPAROO_RUN_MODE = "x"; await ConfigWriter.run(); let files = glob.sync(configFilePattern); expect(files).toEqual([]); process.env.GROUPAROO_RUN_MODE = "cli:config"; await ConfigWriter.run(); files = glob.sync(configFilePattern); expect(files).toContain(appFilePath); }); test("writes a file for each object", async () => { const model: GrouparooModel = await GrouparooModel.create({ type: "profile", name: "Profiles", id: "mod_" + uuid.v4(), }); const app: App = await helper.factories.app(); const source: Source = await helper.factories.source(app, { modelId: model.id, }); await source.setOptions({ table: "test-table-04" }); await source.bootstrapUniqueProperty({ key: "userId_04", type: "integer", mappedColumn: "id", }); await source.setMapping({ id: "userId_04" }); await source.update({ state: "ready" }); const property: Property = await Property.findOne(); await ConfigWriter.run(); const files = glob.sync(configFilePattern); const expConfigFiles = [ `models/${model.getConfigId()}.json`, `apps/${app.getConfigId()}.json`, `sources/${source.getConfigId()}.json`, `properties/${property.getConfigId()}.json`, ].map((configFilePath) => path.join(configDir, configFilePath)); // Verify all expected files exist. expect(files.sort()).toEqual(expConfigFiles.sort()); // Check the app's config file contents const appConfig = JSON.parse( fs.readFileSync(expConfigFiles[0]).toString() ); expect(appConfig).toEqual(await app.getConfigObject()); const sourceConfig = JSON.parse( fs.readFileSync( expConfigFiles.find((f) => f.includes("sources")), { encoding: "utf8" } ) ); expect(sourceConfig.modelId).toBe("profiles"); }); test("does not write objects that are locked", async () => { const app: App = await helper.factories.app(); await app.update({ locked: "config:test" }); await ConfigWriter.run(); let files = glob.sync(configFilePattern); expect(files).toEqual([]); }); test("deletes an object's file after the record is deleted", async () => { const app: App = await helper.factories.app(); await ConfigWriter.run(); let files = glob.sync(configFilePattern); expect(files).toEqual([ path.join(configDir, `apps/${app.getConfigId()}.json`), ]); await app.destroy(); await ConfigWriter.run(); files = glob.sync(configFilePattern); expect(files).toEqual([]); }); test("does not delete or duplicate locked (JS) files", async () => { const app: App = await helper.factories.app(); await app.update({ locked: "config:test" }); const configObject = await app.getConfigObject(); // Create a dummy file just to ensure it is not deleted. const configFilePath = path.join( configDir, `apps/${app.getConfigId()}.js` ); fs.mkdirSync(path.dirname(configFilePath), { recursive: true }); fs.writeFileSync(configFilePath, ""); // Check to make sure the file is there. let files = glob.sync(configFilePattern); expect(files).toEqual([configFilePath]); // Store the reference to the file (mimics what configLoader does). await ConfigWriter.cacheConfigFile({ absFilePath: configFilePath, objects: [configObject], }); // Run the writer, which calls deleteFiles() await ConfigWriter.run(); files = glob.sync(configFilePattern); expect(files).toEqual([configFilePath]); }); test("does not delete files when hitting an error while retrieving objects", async () => { const app: App = await helper.factories.app(); await ConfigWriter.run(); let files = glob.sync(configFilePattern); const filename = `${app.getConfigId()}.json`; expect(files).toEqual([path.join(configDir, `apps/${filename}`)]); await app.update({ name: "$$" }, {}); expect(app.name).toEqual("$$"); await expect(ConfigWriter.run()).rejects.toThrow( "Could not generate ID from name." ); files = glob.sync(configFilePattern); expect(files).toEqual([path.join(configDir, `apps/${filename}`)]); }); }); // ---------------------------------------- | ConfigWriter -> File Cache describe("File Cache", () => { let app: App; beforeEach(async () => { app = await helper.factories.app(); }); afterEach(async () => { await App.destroy({ truncate: true }); await resetClusterNameSetting(); ConfigWriter.resetConfigFileCache(); }); test("stores a cache of config objects", async () => { const configObject = await app.getConfigObject(); const absFilePath = path.join( configDir, `apps/${app.getConfigId()}.json` ); await ConfigWriter.cacheConfigFile({ absFilePath, objects: [configObject], }); const cache = await ConfigWriter.getConfigFileCache(); expect(cache.length).toEqual(1); expect(cache[0].absFilePath).toEqual(absFilePath); expect(cache[0].objects[0]).toEqual(configObject); }); test("does not store locked files", async () => { const configObject = await app.getConfigObject(); const absFilePath = path.join(configDir, `apps/${app.getConfigId()}.js`); const res = await ConfigWriter.cacheConfigFile({ absFilePath, objects: [configObject], }); expect(res).toEqual(null); const cache = await ConfigWriter.getConfigFileCache(); expect(cache.length).toEqual(0); }); test("is reset-able", async () => { const configObject = await app.getConfigObject(); const absFilePath = path.join( configDir, `apps/${app.getConfigId()}.json` ); await ConfigWriter.cacheConfigFile({ absFilePath, objects: [configObject], }); let cache = await ConfigWriter.getConfigFileCache(); expect(cache.length).toEqual(1); ConfigWriter.resetConfigFileCache(); cache = await ConfigWriter.getConfigFileCache(); expect(cache.length).toEqual(0); }); describe("getLockKey()", () => { let configObject: AnyConfigurationObject; beforeEach(async () => { configObject = await app.getConfigObject(); }); afterEach(() => { process.env.GROUPAROO_RUN_MODE = undefined; }); test('returns "config:writer" for JS files (LOCKED)', async () => { process.env.GROUPAROO_RUN_MODE = "cli:config"; const absFilePath = path.join( configDir, `apps/${app.getConfigId()}.js` ); await ConfigWriter.cacheConfigFile({ absFilePath, objects: [configObject], }); expect(ConfigWriter.getLockKey(configObject)).toEqual("config:writer"); }); test("returns null for JSON files (UNLOCKED)", async () => { process.env.GROUPAROO_RUN_MODE = "cli:config"; const absFilePath = path.join( configDir, `apps/${app.getConfigId()}.json` ); await ConfigWriter.cacheConfigFile({ absFilePath, objects: [configObject], }); expect(ConfigWriter.getLockKey(configObject)).toEqual(null); }); test('returns "code:config" when in start mode', async () => { process.env.GROUPAROO_RUN_MODE = "cli:start"; expect(ConfigWriter.getLockKey(configObject)).toEqual("config:code"); }); }); }); // ---------------------------------------- | ConfigWriter.getConfigObjects() describe("getConfigObjects()", () => { afterEach(async () => { await GrouparooModel.destroy({ truncate: true }); await App.destroy({ truncate: true }); await Source.destroy({ truncate: true }); await Property.destroy({ truncate: true }); }); test("returns an empty array when there are no objects", async () => { const configObjects = await ConfigWriter.getConfigObjects(); expect(configObjects).toEqual([]); }); test("skips objects without an id", async () => { let app: App = await helper.factories.app(); let configObjects = await ConfigWriter.getConfigObjects(); expect(configObjects).toEqual([ { filePath: `apps/${app.getConfigId()}.json`, object: await app.getConfigObject(), }, ]); app.name = ""; await app.save(); configObjects = await ConfigWriter.getConfigObjects(); expect(configObjects).toEqual([]); }); test("lists the formatted config objects, ready to be written", async () => { const model: GrouparooModel = await GrouparooModel.create({ type: "profile", name: "Profiles", id: "mod_" + uuid.v4(), }); const app: App = await helper.factories.app(); const source: Source = await helper.factories.source(app); await source.setOptions({ table: "test-table-03" }); await source.bootstrapUniqueProperty({ key: "userId_03", type: "integer", mappedColumn: "id", }); await source.setMapping({ id: "userId_03" }); await source.update({ state: "ready" }); const property: Property = await Property.findOne(); const record: GrouparooRecord = await helper.factories.record(); const record2: GrouparooRecord = await helper.factories.record(); const setting: Setting = await changeClusterNameSetting(); const configObjects = await ConfigWriter.getConfigObjects(); expect(configObjects).toEqual([ { filePath: `models/${model.getConfigId()}.json`, object: await model.getConfigObject(), }, { filePath: `apps/${app.getConfigId()}.json`, object: await app.getConfigObject(), }, { filePath: `sources/${source.getConfigId()}.json`, object: await source.getConfigObject(), }, { filePath: `properties/${property.getConfigId()}.json`, object: await property.getConfigObject(), }, { filePath: `development/${model.getConfigId()}/records.json`, object: [ await record.getConfigObject(), await record2.getConfigObject(), ], }, { filePath: `settings/${setting.getConfigId()}.json`, object: await setting.getConfigObject(), }, ]); }); }); // ---------------------------------------- | Model Config Builders describe("Model Config Providers", () => { let model: GrouparooModel; let source: Source; let property: Property; let group: Group; let sourceOptions = { table: "test-table-02" }; let bootstrapPropertyCol: string = faker.database.column(); let bootstrapPropertyKey: string = faker.random.alphaNumeric(12); let bootstrapPropertyId: string = faker.datatype.uuid(); let propertyKey: string = faker.random.alphaNumeric(8); let propertyCol: string = faker.database.column(); beforeEach(async () => { model = await GrouparooModel.create({ type: "profile", name: "Profiles", id: "mod_" + uuid.v4(), }); source = await helper.factories.source(); await source.setOptions(sourceOptions); await source.bootstrapUniqueProperty({ key: bootstrapPropertyKey, type: "integer", mappedColumn: bootstrapPropertyCol, id: bootstrapPropertyId, }); await source.setMapping({ [bootstrapPropertyCol]: bootstrapPropertyKey }); await source.update({ state: "ready" }); property = await helper.factories.property( source, { key: propertyKey }, { column: propertyCol } ); group = await helper.factories.group(); await group.setRules([ { key: propertyKey, match: "nobody", operation: { op: "eq" } }, ]); }); afterEach(async () => { await GrouparooModel.destroy({ truncate: true }); await App.destroy({ truncate: true }); await Source.destroy({ truncate: true }); await Schedule.destroy({ truncate: true }); await Property.destroy({ truncate: true }); await Destination.destroy({ truncate: true }); await Group.destroy({ truncate: true }); }); // --- Model --- test("models should only humanize their ID if it matches default pattern", async () => { let newModel: GrouparooModel = await GrouparooModel.create({ type: "profile", name: "People", }); expect(newModel.getConfigId()).toEqual( ConfigWriter.generateId(newModel.name) ); expect(newModel.getConfigId()).not.toEqual(newModel.id); newModel = await GrouparooModel.create({ type: "profile", name: "People Again", id: "id-with-hyphen", }); expect(newModel.id).toEqual("id-with-hyphen"); expect(newModel.getConfigId()).not.toEqual( ConfigWriter.generateId(newModel.name) ); expect(newModel.getConfigId()).toEqual(newModel.id); }); // --- App --- test("apps should only humanize their ID if it matches default pattern", async () => { let app: App = await helper.factories.app(); expect(app.getConfigId()).toEqual(ConfigWriter.generateId(app.name)); expect(app.getConfigId()).not.toEqual(app.id); app = await helper.factories.app({ id: "hello-world" }); expect(app.id).toEqual("hello-world"); expect(app.getConfigId()).not.toEqual(ConfigWriter.generateId(app.name)); expect(app.getConfigId()).toEqual(app.id); }); test("apps can provide their config objects", async () => { const app: App = await helper.factories.app(); const config = await app.getConfigObject(); expect(config.id).toBeTruthy(); const options = await app.$get("__options"); expect(options.length).toEqual(1); const { name, type } = app; expect(config).toEqual({ class: "App", id: app.getConfigId(), name, type, options: Object.fromEntries(options.map((o) => [o.key, o.value])), }); }); test("apps without a name will not provide a config object", async () => { const app: App = await helper.factories.app({ name: undefined }); const config = await app.getConfigObject(); expect(config).toBeUndefined(); }); test("apps don't transform env vars", async () => { process.env.GROUPAROO_OPTION__APP__CONFIG_WRITER_ENV_VAR = "my_file_123"; const app: App = await helper.factories.app({ options: { fileId: "CONFIG_WRITER_ENV_VAR" }, }); const config = await app.getConfigObject(); expect(config.options.fileId).toEqual("CONFIG_WRITER_ENV_VAR"); const options = await app.getOptions(); expect(options.fileId).toEqual("my_file_123"); }); // --- Source --- test("sources should only humanize their ID if it matches default pattern", async () => { let source: Source = await helper.factories.source(); expect(source.getConfigId()).toEqual( ConfigWriter.generateId(source.name) ); expect(source.getConfigId()).not.toEqual(source.id); source = await helper.factories.source(undefined, { id: "hello-world" }); expect(source.id).toEqual("hello-world"); expect(source.getConfigId()).not.toEqual( ConfigWriter.generateId(source.name) ); expect(source.getConfigId()).toEqual(source.id); }); test("sources can provide their config objects", async () => { const config = (await source.getConfigObject()) as SourceConfigurationObject; expect(config.id).toBeTruthy(); const { name, type } = source; const app = await source.$get("app"); const options = await source.$get("__options"); expect(options.length).toEqual(1); const mappingProperty = await Property.findByPk(bootstrapPropertyId); expect(config).toEqual({ class: "Source", id: source.getConfigId(), modelId: "profiles", name, type, appId: app.getConfigId(), mapping: { [bootstrapPropertyCol]: mappingProperty.getConfigId() }, options: Object.fromEntries(options.map((o) => [o.key, o.value])), }); }); test("sources without a name will not provide a config object", async () => { const source: Source = await helper.factories.source(undefined, { name: undefined, }); const config = await source.getConfigObject(); expect(config).toBeUndefined(); }); test("sources without a mapping will not add mappings to the config object", async () => { const source: Source = await helper.factories.source(); const config = (await source.getConfigObject()) as SourceConfigurationObject; expect(config.mapping).toBeUndefined(); }); test("sources will also bring their own schedule", async () => { const schedule: Schedule = await helper.factories.schedule(source); const config = (await source.getConfigObject()) as [ SourceConfigurationObject, ScheduleConfigurationObject ]; const scheduleConfig = await schedule.getConfigObject(); const { name, type } = source; const app = await source.$get("app"); const options = await source.getOptions(); const mapping = await MappingHelper.getConfigMapping(source); expect(config.length).toEqual(2); expect(config[0]).toEqual({ class: "Source", id: source.getConfigId(), modelId: "profiles", name, type, appId: app.getConfigId(), mapping, options, }); expect(config[1]).toEqual(scheduleConfig); }); test("sources don't transform env vars", async () => { process.env.GROUPAROO_OPTION__SOURCE__CONFIG_WRITER_ENV_VAR = "my_table_123"; await source.setOptions({ table: "CONFIG_WRITER_ENV_VAR" }); const config = (await source.getConfigObject()) as SourceConfigurationObject; expect(config.options.table).toEqual("CONFIG_WRITER_ENV_VAR"); const options = await source.getOptions(); expect(options.table).toEqual("my_table_123"); }); // --- Schedule --- test("schedules should only humanize their ID if it matches default pattern", async () => { let schedule: Schedule = await helper.factories.schedule(source); expect(schedule.getConfigId()).toEqual( ConfigWriter.generateId(schedule.name) ); expect(schedule.getConfigId()).not.toEqual(schedule.id); await schedule.destroy(); await source.reload(); schedule = await helper.factories.schedule(source, { id: "hello-world", }); expect(schedule.id).toEqual("hello-world"); expect(schedule.getConfigId()).not.toEqual( ConfigWriter.generateId(schedule.name) ); expect(schedule.getConfigId()).toEqual(schedule.id); }); test("schedules can provide their config objects", async () => { const schedule: Schedule = await helper.factories.schedule(source); const filterObj = { key: "id", match: "0", op: "gt" }; // @ts-ignore await schedule.setFilters([filterObj]); const config = await schedule.getConfigObject(); expect(config.id).toBeTruthy(); const { name, recurring, recurringFrequency, confirmRecords } = schedule; const options = await schedule.$get("__options"); expect(options.length).toEqual(1); expect(config).toEqual({ class: "Schedule", id: schedule.getConfigId(), name, sourceId: source.getConfigId(), recurring, incremental: true, recurringFrequency, refreshEnabled: true, confirmRecords, options: Object.fromEntries(options.map((o) => [o.key, o.value])), filters: [ { ...filterObj, relativeMatchDirection: null, relativeMatchNumber: null, relativeMatchUnit: null, }, ], }); }); test("schedules without a name will not provide a config object", async () => { const schedule: Schedule = await helper.factories.schedule(source); schedule.name = undefined; // There's no need to save here because getConfigObject is reading the // attributes from memory. const config = await schedule.getConfigObject(); expect(config).toBeUndefined(); }); test("sources with a nameless schedule provide a single object", async () => { const model: GrouparooModel = await GrouparooModel.create({ type: "profile", name: "People", id: "mod_" + uuid.v4(), }); const app = await helper.factories.app(); const source = await helper.factories.source(app, { modelId: model.id }); await source.setOptions({ table: "test-table-05" }); await source.bootstrapUniqueProperty({ key: "uId05", type: "integer", mappedColumn: "id", }); await source.setMapping({ id: "uId05" }); await source.update({ state: "ready" }); property = await helper.factories.property( source, { key: "firstName05" }, { column: "firstName05" } ); const schedule: Schedule = await helper.factories.schedule(source); await schedule.update({ name: "" }, { hooks: false }); const config = await source.getConfigObject(); const { name, type } = source; const options = await source.getOptions(); const mapping = await MappingHelper.getConfigMapping(source); expect(config).toEqual({ class: "Source", id: source.getConfigId(), modelId: "people", name, type, appId: app.getConfigId(), mapping, options, }); }); test("schedules don't transform env vars", async () => { process.env.GROUPAROO_OPTION__SCHEDULE__CONFIG_WRITER_ENV_VAR = "created_at"; const schedule: Schedule = await helper.factories.schedule(source, { options: { maxColumn: "CONFIG_WRITER_ENV_VAR" }, }); const config = await schedule.getConfigObject(); expect(config.options.maxColumn).toEqual("CONFIG_WRITER_ENV_VAR"); const options = await schedule.getOptions(); expect(options.maxColumn).toEqual( process.env.GROUPAROO_OPTION__SCHEDULE__CONFIG_WRITER_ENV_VAR ); }); // --- Property --- test("properties should only humanize their ID if it matches default pattern", async () => { let property: Property = await helper.factories.property( source, { key: uuid.v4() }, { column: faker.database.column() } ); expect(property.getConfigId()).toEqual( ConfigWriter.generateId(property.key) ); property = await helper.factories.property( source, { id: "hello-world", key: uuid.v4(), }, { column: faker.database.column() } ); expect(property.id).toEqual("hello-world"); expect(property.getConfigId()).not.toEqual( ConfigWriter.generateId(property.key) ); expect(property.getConfigId()).toEqual(property.id); }); test("properties can provide their config objects", async () => { const filterObj = { key: "id", match: "0", op: "gt" }; // @ts-ignore await property.setFilters([filterObj]); const config = await property.getConfigObject(); expect(config.id).toBeTruthy(); const { key, type, unique, isArray } = property; const options = await property.$get("__options"); expect(options.length).toEqual(1); expect(config).toEqual({ class: "Property", id: property.getConfigId(), type, key, sourceId: source.getConfigId(), unique, isArray, options: Object.fromEntries(options.map((o) => [o.key, o.value])), filters: [ { ...filterObj, relativeMatchDirection: null, relativeMatchNumber: null, relativeMatchUnit: null, }, ], }); }); test("properties without a name will not provide a config object", async () => { property.key = undefined; const config = await property.getConfigObject(); expect(config).toBeUndefined(); }); // --- Group --- test("groups should only humanize their ID if it matches default pattern", async () => { let group: Group = await helper.factories.group(); expect(group.getConfigId()).toEqual(ConfigWriter.generateId(group.name)); expect(group.getConfigId()).not.toEqual(group.id); group = await helper.factories.group({ id: "hello-world" }); expect(group.id).toEqual("hello-world"); expect(group.getConfigId()).not.toEqual( ConfigWriter.generateId(group.name) ); expect(group.getConfigId()).toEqual(group.id); expect((await group.getConfigObject()).modelId).toBe("profiles"); }); test("group rules properly set IDs for record column properties", async () => { let group: Group = await helper.factories.group(); await group.setRules([ { key: "grouparooId", match: "nobody", operation: { op: "eq" } }, ]); const config = await group.getConfigObject(); expect(config.rules).toEqual([ { propertyId: "grouparooId", match: "nobody", op: "eq", relativeMatchDirection: null, relativeMatchNumber: null, relativeMatchUnit: null, }, ]); }); test("groups can provide their config objects", async () => { const config = await group.getConfigObject(); expect(config.id).toBeTruthy(); const { name } = group; expect(config).toEqual({ class: "Group", id: group.getConfigId(), modelId: "profiles", name, rules: [ { propertyId: property.getConfigId(), match: "nobody", op: "eq", relativeMatchDirection: null, relativeMatchNumber: null, relativeMatchUnit: null, }, ], }); }); test("properties don't transform env vars", async () => { process.env.GROUPAROO_OPTION__PROPERTY__CONFIG_WRITER_ENV_VAR = faker.database.column(); await property.setOptions({ column: "CONFIG_WRITER_ENV_VAR" }); const config = await property.getConfigObject(); expect(config.options.column).toEqual("CONFIG_WRITER_ENV_VAR"); const options = await property.getOptions(); expect(options.column).toEqual( process.env.GROUPAROO_OPTION__PROPERTY__CONFIG_WRITER_ENV_VAR ); }); test("groups without a name will not provide a config object", async () => { group.name = undefined; const config = await group.getConfigObject(); expect(config).toBeUndefined(); }); // --- Destination --- test("destinations should only humanize their ID if it matches default pattern", async () => { let destination: Destination = await helper.factories.destination(); expect(destination.getConfigId()).toEqual( ConfigWriter.generateId(destination.name) ); expect(destination.getConfigId()).not.toEqual(destination.id); destination = await helper.factories.destination(undefined, { id: "hello-world", }); expect(destination.id).toEqual("hello-world"); expect(destination.getConfigId()).not.toEqual( ConfigWriter.generateId(destination.name) ); expect(destination.getConfigId()).toEqual(destination.id); expect((await destination.getConfigObject()).modelId).toBe("profiles"); }); test("destinations can provide their config objects", async () => { const destination: Destination = await helper.factories.destination( undefined, { groupId: group.id, collection: "group" } ); const app: App = await destination.$get("app"); const destinationGroupMemberships: Record<string, string> = {}; destinationGroupMemberships[group.id] = "My Dest Tag"; await destination.setDestinationGroupMemberships( destinationGroupMemberships ); await destination.setMapping({ "primary-id": bootstrapPropertyKey }); const mappingProperty = await Property.findByPk(bootstrapPropertyId); await MappingHelper.getConfigMapping(destination); const config = await destination.getConfigObject(); const { name, type, syncMode } = destination; const options = await destination.$get("__options"); expect(options.length).toEqual(1); expect(config.id).toBeTruthy(); expect(config).toEqual({ class: "Destination", id: destination.getConfigId(), name, type, appId: app.getConfigId(), groupId: group.getConfigId(), collection: "group", syncMode, options: Object.fromEntries(options.map((o) => [o.key, o.value])), mapping: { "primary-id": mappingProperty.getConfigId() }, modelId: "profiles", destinationGroupMemberships: { "My Dest Tag": group.getConfigId(), }, }); }); test("destinations without a name will not provide a config object", async () => { const destination: Destination = await helper.factories.destination( undefined, { groupId: group.id } ); destination.name = undefined; const config = await destination.getConfigObject(); expect(config).toBeUndefined(); }); test("destinations don't transform env vars", async () => { process.env.GROUPAROO_OPTION__DESTINATION__CONFIG_WRITER_ENV_VAR = "my_table_456"; const destination: Destination = await helper.factories.destination( undefined, { groupId: group.id, options: { table: "CONFIG_WRITER_ENV_VAR" } } ); const config = await destination.getConfigObject(); expect(config.options.table).toEqual("CONFIG_WRITER_ENV_VAR"); const options = await destination.getOptions(); expect(options.table).toEqual("my_table_456"); }); // --- GrouparooRecord --- test("records can provide their config objects", async () => { const record: GrouparooRecord = await helper.factories.record(); const properties = { [bootstrapPropertyId]: [12] }; const bootstrapProperty = await Property.findByPk(bootstrapPropertyId); await record.addOrUpdateProperties({ ...properties, [property.id]: ["some_value"], }); const config = await record.getConfigObject(); expect(config.id).toBeTruthy(); expect(config).toEqual({ class: "Record", id: record.id, modelId: "profiles", properties: { [bootstrapProperty.getConfigId()]: [12], }, }); }); test("record without a primary key property value will not be persisted", async () => { const record: GrouparooRecord = await helper.factories.record(); const properties = { [bootstrapPropertyId]: [null as string] }; await record.addOrUpdateProperties(properties); const config = await record.getConfigObject(); expect(config).toBeUndefined(); }); // --- Setting --- test("settings should only humanize their ID if it matches default pattern", async () => { const setting: Setting = await changeClusterNameSetting(); expect(setting.getConfigId()).toEqual( ConfigWriter.generateId(setting.key) ); expect(setting.getConfigId()).not.toEqual(setting.id); await setting.update({ key: "cluster-name--bak" }); await Setting.destroy({ where: { key: "cluster-name" } }); const newSetting = await Setting.create({ id: "hello-world", pluginName: "core", key: "cluster-name", value: "My Grouparoo Cluster", defaultValue: "My Grouparoo Cluster", type: "string", }); expect(newSetting.id).toEqual("hello-world"); expect(newSetting.getConfigId()).not.toEqual( ConfigWriter.generateId(newSetting.key) ); expect(newSetting.getConfigId()).toEqual(newSetting.id); await newSetting.destroy(); await setting.update({ key: "cluster-name" }); }); test("settings can provide their config objects", async () => { const setting: Setting = await changeClusterNameSetting(); const config = await setting.getConfigObject(); expect(config.id).toBeTruthy(); const { pluginName, key, value } = setting; expect(config).toEqual({ class: "Setting", id: setting.getConfigId(), pluginName, key, value, }); }); }); });