DataModule.spec.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. // @ts-nocheck
  2. import chai from "chai";
  3. import sinon from "sinon";
  4. import sinonChai from "sinon-chai";
  5. import chaiAsPromised from "chai-as-promised";
  6. import { ObjectId } from "mongodb";
  7. import JobContext from "../JobContext";
  8. import JobQueue from "../JobQueue";
  9. import LogBook from "../LogBook";
  10. import ModuleManager from "../ModuleManager";
  11. import DataModule from "./DataModule";
  12. const should = chai.should();
  13. chai.use(sinonChai);
  14. chai.use(chaiAsPromised);
  15. describe("Data Module", function () {
  16. const moduleManager = Object.getPrototypeOf(
  17. sinon.createStubInstance(ModuleManager)
  18. );
  19. ModuleManager.setPrimaryInstance(moduleManager);
  20. const logBook = sinon.createStubInstance(LogBook);
  21. LogBook.setPrimaryInstance(logBook);
  22. moduleManager.jobQueue = sinon.createStubInstance(JobQueue);
  23. const dataModule = new DataModule();
  24. const jobContext = sinon.createStubInstance(JobContext);
  25. const testData = { abc: [] };
  26. before(async function () {
  27. await dataModule.startup();
  28. dataModule.redisClient = sinon.spy(dataModule.redisClient);
  29. });
  30. beforeEach(async function () {
  31. testData.abc = await Promise.all(
  32. Array.from({ length: 10 }).map(async () => {
  33. const doc = {
  34. name: `Test${Math.round(Math.random() * 1000)}`,
  35. autofill: {
  36. enabled: !!Math.round(Math.random())
  37. },
  38. someNumbers: Array.from({
  39. length: Math.max(1, Math.round(Math.random() * 50))
  40. }).map(() => Math.round(Math.random() * 10000)),
  41. songs: Array.from({
  42. length: Math.max(1, Math.round(Math.random() * 10))
  43. }).map(() => ({
  44. _id: new ObjectId()
  45. })),
  46. restrictedName: `RestrictedTest${Math.round(
  47. Math.random() * 1000
  48. )}`,
  49. createdAt: new Date(),
  50. updatedAt: new Date(),
  51. testData: true
  52. };
  53. const res =
  54. await dataModule.collections?.abc.collection.insertOne({
  55. ...doc,
  56. testData: true
  57. });
  58. return { _id: res.insertedId, ...doc };
  59. })
  60. );
  61. });
  62. it("module loaded and started", function () {
  63. logBook.log.should.have.been.called;
  64. dataModule.getName().should.equal("data");
  65. dataModule.getStatus().should.equal("STARTED");
  66. });
  67. describe("find job", function () {
  68. // Run cache test twice to validate mongo and redis sourced data
  69. [false, true, true].forEach(useCache => {
  70. const useCacheString = `${useCache ? "with" : "without"} cache`;
  71. it(`filter by one _id string ${useCacheString}`, async function () {
  72. const [document] = testData.abc;
  73. const find = await dataModule.find(jobContext, {
  74. collection: "abc",
  75. filter: { _id: document._id },
  76. limit: 1,
  77. useCache
  78. });
  79. find.should.deep.equal({
  80. _id: document._id,
  81. name: document.name,
  82. autofill: {
  83. enabled: document.autofill.enabled
  84. },
  85. someNumbers: document.someNumbers,
  86. songs: document.songs,
  87. createdAt: document.createdAt,
  88. updatedAt: document.updatedAt
  89. });
  90. if (useCache) {
  91. dataModule.redisClient?.GET.should.have.been.called;
  92. }
  93. });
  94. // it(`filter by name string ${useCacheString}`, async function () {
  95. // const [document] = testData.abc;
  96. // const find = await dataModule.find(jobContext, {
  97. // collection: "abc",
  98. // filter: { restrictedName: document.restrictedName },
  99. // limit: 1,
  100. // useCache
  101. // });
  102. // find.should.be.an("object");
  103. // find._id.should.deep.equal(document._id);
  104. // find.should.have.keys([
  105. // "_id",
  106. // "createdAt",
  107. // "updatedAt",
  108. // "name",
  109. // "autofill",
  110. // "someNumbers",
  111. // "songs"
  112. // ]);
  113. // find.should.not.have.keys(["restrictedName"]);
  114. // // RestrictedName is restricted, so it won't be returned and the query should not be cached
  115. // find.should.not.have.keys(["name"]);
  116. // dataModule.redisClient?.GET.should.not.have.been.called;
  117. // dataModule.redisClient?.SET.should.not.have.been.called;
  118. // });
  119. });
  120. it(`filter by normal array item`, async function () {
  121. const [document] = testData.abc;
  122. const resultDocument = await dataModule.find(jobContext, {
  123. collection: "abc",
  124. filter: { someNumbers: document.someNumbers[0] },
  125. limit: 1,
  126. useCache: false
  127. });
  128. resultDocument.should.be.an("object");
  129. resultDocument._id.should.deep.equal(document._id);
  130. });
  131. it(`filter by normal array item that doesn't exist`, async function () {
  132. const resultDocument = dataModule.find(jobContext, {
  133. collection: "abc",
  134. filter: { someNumbers: -1 },
  135. limit: 1,
  136. useCache: false
  137. });
  138. await resultDocument.should.eventually.be.null;
  139. });
  140. it(`filter by schema array item`, async function () {
  141. const [document] = testData.abc;
  142. const resultDocument = await dataModule.find(jobContext, {
  143. collection: "abc",
  144. filter: { songs: { _id: document.songs[0]._id } },
  145. limit: 1,
  146. useCache: false
  147. });
  148. resultDocument.should.be.an("object");
  149. resultDocument._id.should.deep.equal(document._id);
  150. });
  151. it(`filter by schema array item, invalid`, async function () {
  152. const jobPromise = dataModule.find(jobContext, {
  153. collection: "abc",
  154. filter: { songs: { randomProperty: "Value" } },
  155. limit: 1,
  156. useCache: false
  157. });
  158. await jobPromise.should.eventually.be.rejectedWith(
  159. `Key "randomProperty" does not exist in the schema.`
  160. );
  161. });
  162. it(`filter by schema array item with dot notation`, async function () {
  163. const [document] = testData.abc;
  164. const resultDocument = await dataModule.find(jobContext, {
  165. collection: "abc",
  166. filter: { "songs._id": document.songs[0]._id },
  167. limit: 1,
  168. useCache: false
  169. });
  170. resultDocument.should.be.an("object");
  171. resultDocument._id.should.deep.equal(document._id);
  172. });
  173. it(`filter by schema array item with dot notation, invalid`, async function () {
  174. const jobPromise = dataModule.find(jobContext, {
  175. collection: "abc",
  176. filter: { "songs.randomProperty": "Value" },
  177. limit: 1,
  178. useCache: false
  179. });
  180. await jobPromise.should.eventually.be.rejectedWith(
  181. `Key "randomProperty" does not exist in the schema.`
  182. );
  183. });
  184. describe("filter $in operator by type", function () {
  185. Object.entries({
  186. objectId: ["_id", new ObjectId()],
  187. string: ["name", "RandomName"],
  188. number: ["someNumbers", -1],
  189. date: ["createdAt", new Date()]
  190. }).forEach(([type, [attribute, invalidValue]]) => {
  191. it(`${type}, where document exists`, async function () {
  192. const [document] = testData.abc;
  193. const filter = {};
  194. filter[attribute] = {
  195. $in: [
  196. Array.isArray(document[attribute])
  197. ? document[attribute][0]
  198. : document[attribute],
  199. invalidValue
  200. ]
  201. };
  202. const resultDocument = await dataModule.find(jobContext, {
  203. collection: "abc",
  204. filter,
  205. limit: 1,
  206. useCache: false
  207. });
  208. resultDocument.should.deep.equal({
  209. _id: document._id,
  210. name: document.name,
  211. autofill: {
  212. enabled: document.autofill.enabled
  213. },
  214. someNumbers: document.someNumbers,
  215. songs: document.songs,
  216. createdAt: document.createdAt,
  217. updatedAt: document.updatedAt
  218. });
  219. });
  220. it(`${type}, where document doesnt exist`, async function () {
  221. const filter = {};
  222. filter[attribute] = { $in: [invalidValue, invalidValue] };
  223. const jobPromise = dataModule.find(jobContext, {
  224. collection: "abc",
  225. filter,
  226. limit: 1,
  227. useCache: false
  228. });
  229. await jobPromise.should.eventually.be.null;
  230. });
  231. });
  232. });
  233. it(`find should not have restricted properties`, async function () {
  234. const [document] = testData.abc;
  235. const resultDocument = await dataModule.find(jobContext, {
  236. collection: "abc",
  237. filter: { _id: document._id },
  238. limit: 1,
  239. useCache: false
  240. });
  241. resultDocument.should.be.an("object");
  242. resultDocument._id.should.deep.equal(document._id);
  243. resultDocument.should.have.all.keys([
  244. "_id",
  245. "createdAt",
  246. "updatedAt",
  247. "name",
  248. "autofill",
  249. "someNumbers",
  250. "songs"
  251. ]);
  252. resultDocument.should.not.have.any.keys(["restrictedName"]);
  253. });
  254. it(`find should have all restricted properties`, async function () {
  255. const [document] = testData.abc;
  256. const resultDocument = await dataModule.find(jobContext, {
  257. collection: "abc",
  258. filter: { _id: document._id },
  259. allowedRestricted: true,
  260. limit: 1,
  261. useCache: false
  262. });
  263. resultDocument.should.be.an("object");
  264. resultDocument._id.should.deep.equal(document._id);
  265. resultDocument.should.have.all.keys([
  266. "_id",
  267. "createdAt",
  268. "updatedAt",
  269. "name",
  270. "autofill",
  271. "someNumbers",
  272. "songs",
  273. "restrictedName"
  274. ]);
  275. });
  276. it(`find should have a specific restricted property`, async function () {
  277. const [document] = testData.abc;
  278. const resultDocument = await dataModule.find(jobContext, {
  279. collection: "abc",
  280. filter: { _id: document._id },
  281. allowedRestricted: ["restrictedName"],
  282. limit: 1,
  283. useCache: false
  284. });
  285. resultDocument.should.be.an("object");
  286. resultDocument._id.should.deep.equal(document._id);
  287. resultDocument.should.have.all.keys([
  288. "_id",
  289. "createdAt",
  290. "updatedAt",
  291. "name",
  292. "autofill",
  293. "someNumbers",
  294. "songs",
  295. "restrictedName"
  296. ]);
  297. });
  298. describe("filter by date types", function () {
  299. it("Date", async function () {
  300. const [document] = testData.abc;
  301. const { createdAt } = document;
  302. const resultDocument = await dataModule.find(jobContext, {
  303. collection: "abc",
  304. filter: { createdAt },
  305. limit: 1,
  306. useCache: false
  307. });
  308. should.exist(resultDocument);
  309. resultDocument.createdAt.should.deep.equal(document.createdAt);
  310. });
  311. it("String", async function () {
  312. const [document] = testData.abc;
  313. const { createdAt } = document;
  314. const resultDocument = await dataModule.find(jobContext, {
  315. collection: "abc",
  316. filter: { createdAt: createdAt.toString() },
  317. limit: 1,
  318. useCache: false
  319. });
  320. should.exist(resultDocument);
  321. resultDocument.createdAt.should.deep.equal(document.createdAt);
  322. });
  323. it("Number", async function () {
  324. const [document] = testData.abc;
  325. const { createdAt } = document;
  326. const resultDocument = await dataModule.find(jobContext, {
  327. collection: "abc",
  328. filter: { createdAt: createdAt.getTime() },
  329. limit: 1,
  330. useCache: false
  331. });
  332. should.exist(resultDocument);
  333. resultDocument.createdAt.should.deep.equal(document.createdAt);
  334. });
  335. });
  336. });
  337. describe("normalize projection", function () {
  338. const dataModuleProjection = Object.getPrototypeOf(dataModule);
  339. it(`basics`, function () {
  340. dataModuleProjection.normalizeProjection.should.be.a("function");
  341. });
  342. it(`empty object/array projection`, function () {
  343. const expectedResult = { projection: [], mode: "includeAllBut" };
  344. const resultWithArray = dataModuleProjection.normalizeProjection(
  345. []
  346. );
  347. const resultWithObject = dataModuleProjection.normalizeProjection(
  348. {}
  349. );
  350. resultWithArray.should.deep.equal(expectedResult);
  351. resultWithObject.should.deep.equal(expectedResult);
  352. });
  353. it(`null/undefined projection`, function () {
  354. const expectedResult = { projection: [], mode: "includeAllBut" };
  355. const resultWithNull =
  356. dataModuleProjection.normalizeProjection(null);
  357. const resultWithUndefined =
  358. dataModuleProjection.normalizeProjection(undefined);
  359. const resultWithNothing =
  360. dataModuleProjection.normalizeProjection();
  361. resultWithNull.should.deep.equal(expectedResult);
  362. resultWithUndefined.should.deep.equal(expectedResult);
  363. resultWithNothing.should.deep.equal(expectedResult);
  364. });
  365. it(`simple exclude projection`, function () {
  366. const expectedResult = {
  367. projection: [["name", false]],
  368. mode: "includeAllBut"
  369. };
  370. const resultWithBoolean = dataModuleProjection.normalizeProjection({
  371. name: false
  372. });
  373. const resultWithNumber = dataModuleProjection.normalizeProjection({
  374. name: 0
  375. });
  376. resultWithBoolean.should.deep.equal(expectedResult);
  377. resultWithNumber.should.deep.equal(expectedResult);
  378. });
  379. it(`simple include projection`, function () {
  380. const expectedResult = {
  381. projection: [["name", true]],
  382. mode: "excludeAllBut"
  383. };
  384. const resultWithObject = dataModuleProjection.normalizeProjection({
  385. name: true
  386. });
  387. const resultWithArray = dataModuleProjection.normalizeProjection([
  388. "name"
  389. ]);
  390. resultWithObject.should.deep.equal(expectedResult);
  391. resultWithArray.should.deep.equal(expectedResult);
  392. });
  393. it(`simple include/exclude projection`, function () {
  394. const expectedResult = {
  395. projection: [
  396. ["color", false],
  397. ["name", true]
  398. ],
  399. mode: "excludeAllBut"
  400. };
  401. const result = dataModuleProjection.normalizeProjection({
  402. color: false,
  403. name: true
  404. });
  405. result.should.deep.equal(expectedResult);
  406. });
  407. it(`simple nested include projection`, function () {
  408. const expectedResult = {
  409. projection: [["location.city", true]],
  410. mode: "excludeAllBut"
  411. };
  412. const resultWithObject = dataModuleProjection.normalizeProjection({
  413. location: {
  414. city: true
  415. }
  416. });
  417. const resultWithArray = dataModuleProjection.normalizeProjection([
  418. "location.city"
  419. ]);
  420. resultWithObject.should.deep.equal(expectedResult);
  421. resultWithArray.should.deep.equal(expectedResult);
  422. });
  423. it(`simple nested exclude projection`, function () {
  424. const expectedResult = {
  425. projection: [["location.city", false]],
  426. mode: "includeAllBut"
  427. };
  428. const result = dataModuleProjection.normalizeProjection({
  429. location: {
  430. city: false
  431. }
  432. });
  433. result.should.deep.equal(expectedResult);
  434. });
  435. it(`path collision`, function () {
  436. (() =>
  437. dataModuleProjection.normalizeProjection({
  438. location: {
  439. city: false
  440. },
  441. "location.city": true
  442. })).should.throw("Path collision, non-unique key");
  443. });
  444. it(`path collision 2`, function () {
  445. (() =>
  446. dataModuleProjection.normalizeProjection({
  447. location: {
  448. city: {
  449. extra: false
  450. }
  451. },
  452. "location.city": true
  453. })).should.throw(
  454. "Path collision! location.city.extra collides with location.city"
  455. );
  456. });
  457. });
  458. afterEach(async function () {
  459. sinon.reset();
  460. await dataModule.collections?.abc.collection.deleteMany({
  461. testData: true
  462. });
  463. });
  464. after(async function () {
  465. await dataModule.shutdown();
  466. });
  467. });