emberflow 1.5.0 → 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.
- package/README.md +231 -4
- package/lib/index-utils.d.ts +18 -7
- package/lib/index-utils.js +71 -76
- package/lib/index-utils.js.map +1 -1
- package/lib/index.d.ts +22 -6
- package/lib/index.js +51 -36
- package/lib/index.js.map +1 -1
- package/lib/logics/view-logics.d.ts +0 -2
- package/lib/logics/view-logics.js +1 -19
- package/lib/logics/view-logics.js.map +1 -1
- package/lib/sample-custom/cleanup-configs.d.ts +2 -0
- package/lib/sample-custom/cleanup-configs.js +17 -0
- package/lib/sample-custom/cleanup-configs.js.map +1 -0
- package/lib/sample-custom/index.js +14 -1
- package/lib/sample-custom/index.js.map +1 -1
- package/lib/sample-custom/one-time-patches.d.ts +2 -0
- package/lib/sample-custom/one-time-patches.js +14 -0
- package/lib/sample-custom/one-time-patches.js.map +1 -0
- package/lib/tests/index-utils.test.js +349 -88
- package/lib/tests/index-utils.test.js.map +1 -1
- package/lib/tests/index.test.js +68 -4
- package/lib/tests/index.test.js.map +1 -1
- package/lib/tests/logics/patch-logics.test.js +50 -5
- package/lib/tests/logics/patch-logics.test.js.map +1 -1
- package/lib/tests/logics/view-logics.test.js +22 -4
- package/lib/tests/logics/view-logics.test.js.map +1 -1
- package/lib/tests/utils/cleanup.test.d.ts +1 -0
- package/lib/tests/utils/cleanup.test.js +243 -0
- package/lib/tests/utils/cleanup.test.js.map +1 -0
- package/lib/tests/utils/distribution.test.js +256 -1
- package/lib/tests/utils/distribution.test.js.map +1 -1
- package/lib/tests/utils/forms.test.js +10 -86
- package/lib/tests/utils/forms.test.js.map +1 -1
- package/lib/tests/utils/misc.test.js +96 -2
- package/lib/tests/utils/misc.test.js.map +1 -1
- package/lib/tests/utils/paths.test.js +20 -2
- package/lib/tests/utils/paths.test.js.map +1 -1
- package/lib/tests/utils/pubsub.test.js +10 -31
- package/lib/tests/utils/pubsub.test.js.map +1 -1
- package/lib/types.d.ts +51 -2
- package/lib/utils/cleanup.d.ts +5 -0
- package/lib/utils/cleanup.js +99 -0
- package/lib/utils/cleanup.js.map +1 -0
- package/lib/utils/distribution.d.ts +17 -3
- package/lib/utils/distribution.js +61 -23
- package/lib/utils/distribution.js.map +1 -1
- package/lib/utils/forms.d.ts +0 -2
- package/lib/utils/forms.js +1 -35
- package/lib/utils/forms.js.map +1 -1
- package/lib/utils/misc.d.ts +1 -0
- package/lib/utils/misc.js +32 -1
- package/lib/utils/misc.js.map +1 -1
- package/lib/utils/pubsub.d.ts +0 -2
- package/lib/utils/pubsub.js +1 -16
- package/lib/utils/pubsub.js.map +1 -1
- package/package.json +1 -1
- package/src/sample-custom/cleanup-configs.ts +15 -0
- package/src/sample-custom/index.ts +15 -11
- 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)(
|
|
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
|