emberflow 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +231 -4
  2. package/lib/index-utils.d.ts +18 -7
  3. package/lib/index-utils.js +70 -75
  4. package/lib/index-utils.js.map +1 -1
  5. package/lib/index.d.ts +22 -6
  6. package/lib/index.js +43 -34
  7. package/lib/index.js.map +1 -1
  8. package/lib/logics/view-logics.d.ts +0 -2
  9. package/lib/logics/view-logics.js +1 -19
  10. package/lib/logics/view-logics.js.map +1 -1
  11. package/lib/sample-custom/cleanup-configs.d.ts +2 -0
  12. package/lib/sample-custom/cleanup-configs.js +17 -0
  13. package/lib/sample-custom/cleanup-configs.js.map +1 -0
  14. package/lib/sample-custom/index.js +14 -1
  15. package/lib/sample-custom/index.js.map +1 -1
  16. package/lib/sample-custom/one-time-patches.d.ts +2 -0
  17. package/lib/sample-custom/one-time-patches.js +14 -0
  18. package/lib/sample-custom/one-time-patches.js.map +1 -0
  19. package/lib/tests/index-utils.test.js +345 -85
  20. package/lib/tests/index-utils.test.js.map +1 -1
  21. package/lib/tests/index.test.js +33 -5
  22. package/lib/tests/index.test.js.map +1 -1
  23. package/lib/tests/logics/patch-logics.test.js +50 -5
  24. package/lib/tests/logics/patch-logics.test.js.map +1 -1
  25. package/lib/tests/logics/view-logics.test.js +22 -4
  26. package/lib/tests/logics/view-logics.test.js.map +1 -1
  27. package/lib/tests/utils/cleanup.test.d.ts +1 -0
  28. package/lib/tests/utils/cleanup.test.js +243 -0
  29. package/lib/tests/utils/cleanup.test.js.map +1 -0
  30. package/lib/tests/utils/distribution.test.js +256 -1
  31. package/lib/tests/utils/distribution.test.js.map +1 -1
  32. package/lib/tests/utils/forms.test.js +10 -86
  33. package/lib/tests/utils/forms.test.js.map +1 -1
  34. package/lib/tests/utils/misc.test.js +96 -2
  35. package/lib/tests/utils/misc.test.js.map +1 -1
  36. package/lib/tests/utils/paths.test.js +20 -2
  37. package/lib/tests/utils/paths.test.js.map +1 -1
  38. package/lib/tests/utils/pubsub.test.js +10 -31
  39. package/lib/tests/utils/pubsub.test.js.map +1 -1
  40. package/lib/types.d.ts +51 -2
  41. package/lib/utils/cleanup.d.ts +5 -0
  42. package/lib/utils/cleanup.js +99 -0
  43. package/lib/utils/cleanup.js.map +1 -0
  44. package/lib/utils/distribution.d.ts +17 -3
  45. package/lib/utils/distribution.js +61 -23
  46. package/lib/utils/distribution.js.map +1 -1
  47. package/lib/utils/forms.d.ts +0 -2
  48. package/lib/utils/forms.js +1 -35
  49. package/lib/utils/forms.js.map +1 -1
  50. package/lib/utils/misc.d.ts +1 -0
  51. package/lib/utils/misc.js +32 -1
  52. package/lib/utils/misc.js.map +1 -1
  53. package/lib/utils/pubsub.d.ts +0 -2
  54. package/lib/utils/pubsub.js +1 -16
  55. package/lib/utils/pubsub.js.map +1 -1
  56. package/package.json +1 -1
  57. package/src/sample-custom/cleanup-configs.ts +15 -0
  58. package/src/sample-custom/index.ts +15 -11
  59. package/src/sample-custom/one-time-patches.ts +13 -0
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ const admin = __importStar(require("firebase-admin"));
27
+ const index_1 = require("../../index");
28
+ const db_structure_1 = require("../../sample-custom/db-structure");
29
+ const security_1 = require("../../sample-custom/security");
30
+ const validators_1 = require("../../sample-custom/validators");
31
+ const patch_logics_1 = require("../../sample-custom/patch-logics");
32
+ const misc = __importStar(require("../../utils/misc"));
33
+ const cleanup_1 = require("../../utils/cleanup");
34
+ const projectConfig = {
35
+ projectId: "your-project-id",
36
+ region: "asia-southeast1",
37
+ rtdbName: "your-rtdb-name",
38
+ budgetAlertTopicName: "budget-alerts",
39
+ maxCostLimitPerFunction: 100,
40
+ specialCostLimitPerFunction: {
41
+ function1: 50,
42
+ function2: 75,
43
+ function3: 120,
44
+ },
45
+ };
46
+ admin.initializeApp({
47
+ databaseURL: "https://test-project.firebaseio.com",
48
+ });
49
+ const projectCleanupConfigs = [
50
+ {
51
+ collectionPath: "askJaris",
52
+ isCollectionGroup: true,
53
+ timestampField: "createdAt",
54
+ olderThan: { value: 1, unit: "months" },
55
+ conditions: [
56
+ { fieldName: "hasTopic", operator: "==", value: false },
57
+ ],
58
+ },
59
+ ];
60
+ function initWithCleanupConfigs(cleanupConfigs) {
61
+ (0, index_1.initializeEmberFlow)({
62
+ projectConfig,
63
+ admin,
64
+ dbStructure: db_structure_1.dbStructure,
65
+ Entity: db_structure_1.Entity,
66
+ securityConfigs: security_1.securityConfigs,
67
+ validatorConfigs: validators_1.validatorConfigs,
68
+ logicConfigs: [],
69
+ patchLogicConfigs: patch_logics_1.patchLogicConfigs,
70
+ cleanupConfigs,
71
+ });
72
+ }
73
+ describe("computeCutoffDate", () => {
74
+ beforeEach(() => {
75
+ jest.useFakeTimers();
76
+ });
77
+ afterEach(() => {
78
+ jest.useRealTimers();
79
+ });
80
+ it("subtracts hours via millisecond math", () => {
81
+ jest.setSystemTime(new Date("2023-06-15T12:00:00.000Z"));
82
+ const result = (0, cleanup_1.computeCutoffDate)(2, "hours");
83
+ expect(result.getTime()).toBe(Date.now() - 2 * 3600000);
84
+ });
85
+ it("subtracts days via millisecond math", () => {
86
+ jest.setSystemTime(new Date("2023-06-15T12:00:00.000Z"));
87
+ const result = (0, cleanup_1.computeCutoffDate)(3, "days");
88
+ expect(result.getTime()).toBe(Date.now() - 3 * 86400000);
89
+ });
90
+ it("subtracts months using calendar arithmetic", () => {
91
+ jest.setSystemTime(new Date(2023, 5, 15, 12, 0, 0));
92
+ const result = (0, cleanup_1.computeCutoffDate)(1, "months");
93
+ expect(result.getFullYear()).toBe(2023);
94
+ expect(result.getMonth()).toBe(4); // May
95
+ expect(result.getDate()).toBe(15);
96
+ });
97
+ it("uses calendar arithmetic (not fixed 30 days) for month overflow", () => {
98
+ // Mar 31 minus 1 month => Feb 31 which rolls forward into March,
99
+ // demonstrating Date.setMonth calendar arithmetic rather than fixed ms.
100
+ jest.setSystemTime(new Date(2023, 2, 31, 12, 0, 0));
101
+ const result = (0, cleanup_1.computeCutoffDate)(1, "months");
102
+ expect(result.getMonth()).toBe(2); // rolled back into March
103
+ });
104
+ });
105
+ describe("getInternalCleanupConfigs", () => {
106
+ it("returns the five built-in entries", () => {
107
+ const configs = (0, cleanup_1.getInternalCleanupConfigs)();
108
+ expect(configs).toHaveLength(5);
109
+ const byPath = (path) => configs.find((c) => c.collectionPath === path);
110
+ expect(byPath("processedIds")).toMatchObject({
111
+ isCollectionGroup: true,
112
+ timestampField: "timestamp",
113
+ olderThan: { value: 7, unit: "days" },
114
+ recursive: false,
115
+ });
116
+ expect(byPath("executions")).toMatchObject({
117
+ isCollectionGroup: true,
118
+ timestampField: "execDate",
119
+ olderThan: { value: 7, unit: "days" },
120
+ recursive: false,
121
+ });
122
+ expect(byPath("computations")).toMatchObject({
123
+ isCollectionGroup: true,
124
+ timestampField: "createdAt",
125
+ olderThan: { value: 30, unit: "days" },
126
+ recursive: false,
127
+ });
128
+ expect(byPath("@emberflow/internal/viewLogicExecutions")).toMatchObject({
129
+ timestampField: "execDate",
130
+ olderThan: { value: 7, unit: "days" },
131
+ });
132
+ const actionsEntry = byPath("@actions");
133
+ expect(actionsEntry).toMatchObject({
134
+ timestampField: "timeCreated",
135
+ olderThan: { value: 7, unit: "days" },
136
+ });
137
+ expect(typeof (actionsEntry === null || actionsEntry === void 0 ? void 0 : actionsEntry.onBatchDeleted)).toBe("function");
138
+ });
139
+ });
140
+ describe("cleanupCollections", () => {
141
+ let query;
142
+ let collectionMock;
143
+ let collectionGroupMock;
144
+ let deleteCollectionSpy;
145
+ let deleteCollectionRecursiveSpy;
146
+ let rtdbUpdateMock;
147
+ const snapshot = {
148
+ size: 2,
149
+ docs: [
150
+ { data: () => ({ eventContext: { formId: "f1", uid: "u1" } }) },
151
+ { data: () => ({ eventContext: { formId: "f2", uid: "u2" } }) },
152
+ ],
153
+ };
154
+ beforeEach(() => {
155
+ initWithCleanupConfigs(projectCleanupConfigs);
156
+ query = { where: jest.fn() };
157
+ query.where.mockReturnValue(query);
158
+ collectionMock = jest.fn().mockReturnValue(query);
159
+ collectionGroupMock = jest.fn().mockReturnValue(query);
160
+ jest.spyOn(admin.firestore(), "collection").mockImplementation(collectionMock);
161
+ jest.spyOn(admin.firestore(), "collectionGroup").mockImplementation(collectionGroupMock);
162
+ rtdbUpdateMock = jest.fn().mockResolvedValue(undefined);
163
+ jest.spyOn(admin.database(), "ref").mockReturnValue({
164
+ update: rtdbUpdateMock,
165
+ });
166
+ deleteCollectionSpy = jest.spyOn(misc, "deleteCollection")
167
+ .mockImplementation(async (q, callback) => {
168
+ if (callback) {
169
+ await callback(snapshot);
170
+ }
171
+ });
172
+ deleteCollectionRecursiveSpy = jest.spyOn(misc, "deleteCollectionRecursive")
173
+ .mockImplementation(async (q, callback) => {
174
+ if (callback) {
175
+ await callback(snapshot);
176
+ }
177
+ });
178
+ });
179
+ afterEach(() => {
180
+ jest.restoreAllMocks();
181
+ });
182
+ it("processes both internal and project entries in one pass", async () => {
183
+ await (0, cleanup_1.cleanupCollections)({});
184
+ // Internal collection-group entries
185
+ expect(collectionGroupMock).toHaveBeenCalledWith("processedIds");
186
+ expect(collectionGroupMock).toHaveBeenCalledWith("executions");
187
+ expect(collectionGroupMock).toHaveBeenCalledWith("computations");
188
+ // Internal exact-path entries
189
+ expect(collectionMock).toHaveBeenCalledWith("@emberflow/internal/viewLogicExecutions");
190
+ expect(collectionMock).toHaveBeenCalledWith("@actions");
191
+ // Project entry (collection group)
192
+ expect(collectionGroupMock).toHaveBeenCalledWith("askJaris");
193
+ });
194
+ it("builds the age threshold and extra conditions in one query", async () => {
195
+ await (0, cleanup_1.cleanupCollections)({});
196
+ expect(query.where).toHaveBeenCalledWith("createdAt", "<", expect.any(Date));
197
+ expect(query.where).toHaveBeenCalledWith("timeCreated", "<", expect.any(Date));
198
+ // project askJaris condition is chained onto the timestamp filter
199
+ expect(query.where).toHaveBeenCalledWith("hasTopic", "==", false);
200
+ });
201
+ it("deletes recursively by default and flatly when recursive is false", async () => {
202
+ await (0, cleanup_1.cleanupCollections)({});
203
+ // flat: processedIds, executions, computations (recursive: false)
204
+ expect(deleteCollectionSpy).toHaveBeenCalledTimes(3);
205
+ // recursive (default): viewLogicExecutions, @actions, askJaris
206
+ expect(deleteCollectionRecursiveSpy).toHaveBeenCalledTimes(3);
207
+ });
208
+ it("runs the @actions RTDB hook to null forms/{uid}/{formId}", async () => {
209
+ await (0, cleanup_1.cleanupCollections)({});
210
+ expect(rtdbUpdateMock).toHaveBeenCalledTimes(1);
211
+ expect(rtdbUpdateMock).toHaveBeenCalledWith({
212
+ "forms/u1/f1": null,
213
+ "forms/u2/f2": null,
214
+ });
215
+ });
216
+ it("logs the number of deleted documents per entry", async () => {
217
+ const infoSpy = jest.spyOn(console, "info").mockImplementation();
218
+ await (0, cleanup_1.cleanupCollections)({});
219
+ expect(infoSpy).toHaveBeenCalledWith("cleanupCollections: deleted 2 from @actions");
220
+ expect(infoSpy).toHaveBeenCalledWith("cleanupCollections: deleted 2 from askJaris");
221
+ });
222
+ it("isolates per-entry failures and continues processing", async () => {
223
+ const errorSpy = jest.spyOn(console, "error").mockImplementation();
224
+ collectionGroupMock.mockImplementation((name) => {
225
+ if (name === "executions") {
226
+ throw new Error("boom");
227
+ }
228
+ return query;
229
+ });
230
+ await (0, cleanup_1.cleanupCollections)({});
231
+ expect(errorSpy).toHaveBeenCalledWith("cleanupCollections failed for executions", expect.any(Error));
232
+ // later entries still processed
233
+ expect(collectionMock).toHaveBeenCalledWith("@actions");
234
+ expect(collectionGroupMock).toHaveBeenCalledWith("askJaris");
235
+ });
236
+ it("runs internal entries only when no project configs are supplied", async () => {
237
+ initWithCleanupConfigs([]);
238
+ await (0, cleanup_1.cleanupCollections)({});
239
+ expect(collectionGroupMock).not.toHaveBeenCalledWith("askJaris");
240
+ expect(collectionMock).toHaveBeenCalledWith("@actions");
241
+ });
242
+ });
243
+ //# sourceMappingURL=cleanup.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup.test.js","sourceRoot":"","sources":["../../../src/tests/utils/cleanup.test.ts"],"names":[],"mappings":"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"}
@@ -70,7 +70,16 @@ admin.initializeApp({
70
70
  databaseURL: "https://test-project.firebaseio.com",
71
71
  });
72
72
  jest.spyOn(paths._mockable, "doesPathExists").mockResolvedValue(true);
73
- (0, index_1.initializeEmberFlow)(projectConfig, admin, db_structure_1.dbStructure, db_structure_1.Entity, security_1.securityConfigs, validators_1.validatorConfigs, [], []);
73
+ (0, index_1.initializeEmberFlow)({
74
+ projectConfig,
75
+ admin,
76
+ dbStructure: db_structure_1.dbStructure,
77
+ Entity: db_structure_1.Entity,
78
+ securityConfigs: security_1.securityConfigs,
79
+ validatorConfigs: validators_1.validatorConfigs,
80
+ logicConfigs: [],
81
+ patchLogicConfigs: [],
82
+ });
74
83
  describe("queueForDistributionLater", () => {
75
84
  let publishMessageSpy;
76
85
  beforeEach(() => {
@@ -934,4 +943,250 @@ describe("instructionsReducer", () => {
934
943
  expect(reducedInstructions.get(doc1.dstPath)).toStrictEqual(expectedReducedInstructions);
935
944
  });
936
945
  });
946
+ describe("getGroupPatchStatusPath", () => {
947
+ it("computes independent status paths per patch type/backFillPatchName on the same collection", () => {
948
+ const collectionPath = "/users/user1/feeds";
949
+ const ancestorIdsPath = distribution.getGroupPatchStatusPath(collectionPath, "back-fill", "ancestor-ids");
950
+ const customBackFillPath = distribution.getGroupPatchStatusPath(collectionPath, "back-fill", "custom-back-fill");
951
+ const patchLogicsPath = distribution.getGroupPatchStatusPath(collectionPath, "patch-logics");
952
+ expect(ancestorIdsPath).not.toEqual(customBackFillPath);
953
+ expect(ancestorIdsPath).not.toEqual(patchLogicsPath);
954
+ expect(customBackFillPath).not.toEqual(patchLogicsPath);
955
+ expect(ancestorIdsPath).toContain("back-fill_ancestor-ids");
956
+ expect(customBackFillPath).toContain("back-fill_custom-back-fill");
957
+ expect(patchLogicsPath).toContain("patch-logics");
958
+ });
959
+ });
960
+ describe("queueGroupPatch", () => {
961
+ let publishMessageSpy;
962
+ let runTransactionSpy;
963
+ let txnGetMock;
964
+ let txnSetMock;
965
+ beforeEach(() => {
966
+ jest.restoreAllMocks();
967
+ publishMessageSpy = jest.spyOn(index_1.GROUP_PATCH_TOPIC, "publishMessage")
968
+ .mockImplementation(() => {
969
+ return Promise.resolve("message-id");
970
+ });
971
+ txnGetMock = jest.fn().mockResolvedValue({ exists: false });
972
+ txnSetMock = jest.fn();
973
+ runTransactionSpy = jest.spyOn(admin.firestore(), "runTransaction")
974
+ .mockImplementation(async (fn) => fn({ get: txnGetMock, set: txnSetMock }));
975
+ });
976
+ it("locks the status doc and publishes when there is no lastPatchedId", async () => {
977
+ await distribution.queueGroupPatch({
978
+ path: "/users/user1/feeds/feed1",
979
+ patchType: "back-fill",
980
+ backFillPatchName: "ancestor-ids",
981
+ });
982
+ expect(runTransactionSpy).toHaveBeenCalledTimes(1);
983
+ expect(txnSetMock).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
984
+ status: "queued",
985
+ collectionPath: "/users/user1/feeds",
986
+ count: 0,
987
+ lastPatchedId: null,
988
+ }), { merge: true });
989
+ expect(publishMessageSpy).toHaveBeenCalledWith({
990
+ json: {
991
+ collectionPath: "/users/user1/feeds",
992
+ patchType: "back-fill",
993
+ backFillPatchName: "ancestor-ids",
994
+ appVersion: undefined,
995
+ lastPatchedId: undefined,
996
+ },
997
+ });
998
+ });
999
+ it("does not publish when a patch is already queued and not in error/reset state", async () => {
1000
+ txnGetMock.mockResolvedValue({ exists: true, data: () => ({ status: "running" }) });
1001
+ await distribution.queueGroupPatch({
1002
+ path: "/users/user1/feeds",
1003
+ patchType: "back-fill",
1004
+ backFillPatchName: "ancestor-ids",
1005
+ });
1006
+ expect(publishMessageSpy).not.toHaveBeenCalled();
1007
+ });
1008
+ it("re-locks and publishes when the previous status is error", async () => {
1009
+ txnGetMock.mockResolvedValue({ exists: true, data: () => ({ status: "error" }) });
1010
+ await distribution.queueGroupPatch({
1011
+ path: "/users/user1/feeds",
1012
+ patchType: "back-fill",
1013
+ backFillPatchName: "ancestor-ids",
1014
+ });
1015
+ expect(txnSetMock).toHaveBeenCalled();
1016
+ expect(publishMessageSpy).toHaveBeenCalled();
1017
+ });
1018
+ it("skips locking and publishes directly when lastPatchedId is provided", async () => {
1019
+ await distribution.queueGroupPatch({
1020
+ path: "/users/user1/feeds",
1021
+ patchType: "patch-logics",
1022
+ appVersion: "1.0.0",
1023
+ lastPatchedId: "feed5",
1024
+ });
1025
+ expect(runTransactionSpy).not.toHaveBeenCalled();
1026
+ expect(publishMessageSpy).toHaveBeenCalledWith({
1027
+ json: {
1028
+ collectionPath: "/users/user1/feeds",
1029
+ patchType: "patch-logics",
1030
+ backFillPatchName: undefined,
1031
+ appVersion: "1.0.0",
1032
+ lastPatchedId: "feed5",
1033
+ },
1034
+ });
1035
+ });
1036
+ it("tracks independent locks for different patch types/back-fills on the same collection", async () => {
1037
+ const docSpy = jest.spyOn(admin.firestore(), "doc");
1038
+ await distribution.queueGroupPatch({
1039
+ path: "/users/user1/feeds",
1040
+ patchType: "back-fill",
1041
+ backFillPatchName: "ancestor-ids",
1042
+ });
1043
+ await distribution.queueGroupPatch({
1044
+ path: "/users/user1/feeds",
1045
+ patchType: "patch-logics",
1046
+ appVersion: "1.0.0",
1047
+ });
1048
+ const calledPaths = docSpy.mock.calls.map((call) => call[0]);
1049
+ expect(new Set(calledPaths).size).toBe(calledPaths.length);
1050
+ expect(publishMessageSpy).toHaveBeenCalledTimes(2);
1051
+ });
1052
+ });
1053
+ describe("onMessageGroupPatchQueue", () => {
1054
+ let patchGroupDocsSpy;
1055
+ beforeEach(() => {
1056
+ isProcessedMock.mockResolvedValue(false);
1057
+ patchGroupDocsSpy = jest.spyOn(indexUtils, "patchGroupDocs").mockResolvedValue();
1058
+ });
1059
+ it("skips already processed messages", async () => {
1060
+ isProcessedMock.mockResolvedValue(true);
1061
+ const event = {
1062
+ id: "test-event",
1063
+ data: { message: { json: { collectionPath: "/users/user1/feeds", patchType: "back-fill", backFillPatchName: "ancestor-ids" } } },
1064
+ };
1065
+ await distribution.onMessageGroupPatchQueue(event);
1066
+ expect(patchGroupDocsSpy).not.toHaveBeenCalled();
1067
+ });
1068
+ it("dispatches to patchGroupDocs for non-placeholder collection paths", async () => {
1069
+ const event = {
1070
+ id: "test-event",
1071
+ data: {
1072
+ message: {
1073
+ json: {
1074
+ collectionPath: "/users/user1/feeds",
1075
+ patchType: "back-fill",
1076
+ backFillPatchName: "ancestor-ids",
1077
+ lastPatchedId: "feed1",
1078
+ },
1079
+ },
1080
+ },
1081
+ };
1082
+ await distribution.onMessageGroupPatchQueue(event);
1083
+ expect(patchGroupDocsSpy).toHaveBeenCalledWith({
1084
+ collectionPath: "/users/user1/feeds",
1085
+ patchType: "back-fill",
1086
+ backFillPatchName: "ancestor-ids",
1087
+ appVersion: undefined,
1088
+ lastPatchedId: "feed1",
1089
+ });
1090
+ expect(trackProcessedIdsMock).toHaveBeenCalledWith(index_1.GROUP_PATCH_TOPIC_NAME, event.id);
1091
+ });
1092
+ it("dispatches to patchGroupDocs for patch-logics messages", async () => {
1093
+ const event = {
1094
+ id: "test-event",
1095
+ data: {
1096
+ message: {
1097
+ json: {
1098
+ collectionPath: "/users/user1/feeds",
1099
+ patchType: "patch-logics",
1100
+ appVersion: "1.0.0",
1101
+ },
1102
+ },
1103
+ },
1104
+ };
1105
+ await distribution.onMessageGroupPatchQueue(event);
1106
+ expect(patchGroupDocsSpy).toHaveBeenCalledWith({
1107
+ collectionPath: "/users/user1/feeds",
1108
+ patchType: "patch-logics",
1109
+ backFillPatchName: undefined,
1110
+ appVersion: "1.0.0",
1111
+ lastPatchedId: undefined,
1112
+ });
1113
+ });
1114
+ });
1115
+ describe("getGroupPatchProgress", () => {
1116
+ let docGetMock;
1117
+ beforeEach(() => {
1118
+ docGetMock = jest.fn();
1119
+ jest.spyOn(admin.firestore(), "doc").mockReturnValue({
1120
+ get: docGetMock,
1121
+ });
1122
+ });
1123
+ it("returns undefined when the status doc does not exist", async () => {
1124
+ docGetMock.mockResolvedValue({ exists: false });
1125
+ const progress = await distribution.getGroupPatchProgress({
1126
+ collectionPath: "/users/user1/feeds",
1127
+ patchType: "back-fill",
1128
+ backFillPatchName: "ancestor-ids",
1129
+ });
1130
+ expect(progress).toBeUndefined();
1131
+ });
1132
+ it("returns a running progress", async () => {
1133
+ docGetMock.mockResolvedValue({
1134
+ exists: true,
1135
+ data: () => ({ status: "running", count: 500, lastPatchedId: "doc500" }),
1136
+ });
1137
+ const progress = await distribution.getGroupPatchProgress({
1138
+ collectionPath: "/users/user1/feeds",
1139
+ patchType: "back-fill",
1140
+ backFillPatchName: "ancestor-ids",
1141
+ });
1142
+ expect(progress).toEqual(expect.objectContaining({
1143
+ status: "running",
1144
+ patchedCount: 500,
1145
+ lastPatchedId: "doc500",
1146
+ collectionPath: "/users/user1/feeds",
1147
+ patchType: "back-fill",
1148
+ backFillPatchName: "ancestor-ids",
1149
+ }));
1150
+ });
1151
+ it("returns a completed progress", async () => {
1152
+ docGetMock.mockResolvedValue({
1153
+ exists: true,
1154
+ data: () => ({ status: "completed", count: 1000 }),
1155
+ });
1156
+ const progress = await distribution.getGroupPatchProgress({
1157
+ collectionPath: "/users/user1/feeds",
1158
+ patchType: "patch-logics",
1159
+ });
1160
+ expect(progress).toEqual(expect.objectContaining({ status: "completed", patchedCount: 1000 }));
1161
+ });
1162
+ it("returns an error progress with the error message", async () => {
1163
+ docGetMock.mockResolvedValue({
1164
+ exists: true,
1165
+ data: () => ({ status: "error", error: "Something went wrong" }),
1166
+ });
1167
+ const progress = await distribution.getGroupPatchProgress({
1168
+ collectionPath: "/users/user1/feeds",
1169
+ patchType: "back-fill",
1170
+ backFillPatchName: "custom-back-fill",
1171
+ });
1172
+ expect(progress).toEqual(expect.objectContaining({ status: "error", error: "Something went wrong" }));
1173
+ });
1174
+ it("tracks independent progress per patch type on the same collection", async () => {
1175
+ const docSpy = jest.spyOn(admin.firestore(), "doc").mockReturnValue({
1176
+ get: docGetMock,
1177
+ });
1178
+ docGetMock.mockResolvedValue({ exists: true, data: () => ({ status: "running", count: 1 }) });
1179
+ await distribution.getGroupPatchProgress({
1180
+ collectionPath: "/users/user1/feeds",
1181
+ patchType: "back-fill",
1182
+ backFillPatchName: "ancestor-ids",
1183
+ });
1184
+ await distribution.getGroupPatchProgress({
1185
+ collectionPath: "/users/user1/feeds",
1186
+ patchType: "patch-logics",
1187
+ });
1188
+ const calledPaths = docSpy.mock.calls.map((call) => call[0]);
1189
+ expect(calledPaths[0]).not.toEqual(calledPaths[1]);
1190
+ });
1191
+ });
937
1192
  //# sourceMappingURL=distribution.test.js.map