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
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ Emberflow is a library for Firebase Functions that simplifies the process of set
|
|
|
9
9
|
- **Business Logics**: Define complex business rules that are automatically triggered by Firestore changes.
|
|
10
10
|
- **View Logics**: Easily create and maintain denormalized data (views) across your database.
|
|
11
11
|
- **Patch Logics**: Handle versioning and data migrations seamlessly.
|
|
12
|
+
- **Group Patch Engine**: Run one-time back-fills or bulk patch-logics runs over an entire collection, with progress tracking.
|
|
13
|
+
- **Pluggable Cleanup**: Automatically purge stale documents on a schedule using declarative, config-driven cleanup rules.
|
|
12
14
|
- **Billing Protection**: Built-in budget monitoring to prevent unexpected costs.
|
|
13
15
|
|
|
14
16
|
## Usage
|
|
@@ -36,19 +38,23 @@ import { securityConfigs } from "./security";
|
|
|
36
38
|
import { validatorConfigs } from "./validators";
|
|
37
39
|
import { logics } from "./business-logics";
|
|
38
40
|
import { patchLogicConfigs } from "./patch-logics";
|
|
41
|
+
import { cleanupConfigs } from "./cleanup-configs";
|
|
42
|
+
import { backFillPatchConfigs } from "./one-time-patches";
|
|
39
43
|
|
|
40
44
|
admin.initializeApp();
|
|
41
45
|
|
|
42
|
-
const { functionsConfig } = initializeEmberFlow(
|
|
46
|
+
const { functionsConfig } = initializeEmberFlow({
|
|
43
47
|
projectConfig,
|
|
44
48
|
admin,
|
|
45
49
|
dbStructure,
|
|
46
50
|
Entity,
|
|
47
51
|
securityConfigs,
|
|
48
52
|
validatorConfigs,
|
|
49
|
-
logics,
|
|
50
|
-
patchLogicConfigs
|
|
51
|
-
|
|
53
|
+
logicConfigs: logics,
|
|
54
|
+
patchLogicConfigs,
|
|
55
|
+
cleanupConfigs, // optional, see "Collection Cleanup" below
|
|
56
|
+
backFillPatchConfigs, // optional, see "Group Patch Engine" below
|
|
57
|
+
});
|
|
52
58
|
|
|
53
59
|
// Export the generated functions
|
|
54
60
|
Object.entries(functionsConfig).forEach(([key, value]) => {
|
|
@@ -56,6 +62,8 @@ Object.entries(functionsConfig).forEach(([key, value]) => {
|
|
|
56
62
|
});
|
|
57
63
|
```
|
|
58
64
|
|
|
65
|
+
`initializeEmberFlow` takes a single options object (`InitializeEmberFlowOptions`). `projectConfig`, `admin`, `dbStructure`, `Entity`, `securityConfigs`, `validatorConfigs`, `logicConfigs`, and `patchLogicConfigs` are required; `cleanupConfigs`, `backFillPatchConfigs`, and `userRegisterFn` are optional.
|
|
66
|
+
|
|
59
67
|
## Configuration
|
|
60
68
|
|
|
61
69
|
Emberflow relies on several configuration objects to define how your project behaves.
|
|
@@ -139,6 +147,225 @@ export const logics: LogicConfig[] = [
|
|
|
139
147
|
];
|
|
140
148
|
```
|
|
141
149
|
|
|
150
|
+
### Patch Logics (`patchLogicConfigs`)
|
|
151
|
+
|
|
152
|
+
Patch logics handle data migrations and versioning. They are triggered when a document's `@dataVersion` is lower than the required version defined in the patch logic configurations.
|
|
153
|
+
|
|
154
|
+
#### 1. Define the `PatchLogicFn`
|
|
155
|
+
A patch logic function transforms existing document data to a new version.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { PatchLogicFn, LogicResult } from "emberflow/src/types";
|
|
159
|
+
|
|
160
|
+
const updateUserData: PatchLogicFn = async (dstPath, data) => {
|
|
161
|
+
const { fullName } = data;
|
|
162
|
+
const [firstName, lastName] = fullName.split(" ");
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
name: "updateUserData",
|
|
166
|
+
status: "finished",
|
|
167
|
+
documents: [
|
|
168
|
+
{
|
|
169
|
+
action: "merge",
|
|
170
|
+
dstPath: dstPath,
|
|
171
|
+
doc: { firstName, lastName },
|
|
172
|
+
instructions: { fullName: "del" },
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### 2. Configure the `PatchLogicConfig`
|
|
180
|
+
Register the patch logic for a specific entity and version.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { PatchLogicConfig } from "emberflow/src/types";
|
|
184
|
+
|
|
185
|
+
export const patchLogicConfigs: PatchLogicConfig[] = [
|
|
186
|
+
{
|
|
187
|
+
name: "updateUserData",
|
|
188
|
+
entity: "User",
|
|
189
|
+
patchLogicFn: updateUserData,
|
|
190
|
+
version: "1.1.0", // The version this patch achieves
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### 3. How it Works
|
|
196
|
+
- **Triggering**: There are two ways patch logics run:
|
|
197
|
+
- **Automatic, per-document (default)**: You only register `patchLogicConfigs`. Emberflow
|
|
198
|
+
automatically queues and runs them for a single document during form submissions or
|
|
199
|
+
document distribution whenever a version mismatch is detected — projects don't call
|
|
200
|
+
anything directly here.
|
|
201
|
+
- **On-demand, collection-wide (bulk migration)**: To re-run patch logics across every
|
|
202
|
+
existing document in a collection, call the public `queueGroupPatch({ path, patchType:
|
|
203
|
+
"patch-logics", appVersion })` (see the [Group Patch Engine](#group-patch-engine-queuegrouppatch-getgrouppatchprogress-backfillpatchconfig)
|
|
204
|
+
below). Progress can be polled with `getGroupPatchProgress`.
|
|
205
|
+
- **Asynchronous Execution**: They run asynchronously via Pub/Sub to ensure high performance.
|
|
206
|
+
- **Version-gating**: In both cases a `PatchLogicConfig` only fires when `config.version <=`
|
|
207
|
+
the current `appVersion` **and** the document's `@dataVersion` is older than `config.version`.
|
|
208
|
+
After a successful patch, the document's `@dataVersion` is bumped so it won't run again.
|
|
209
|
+
- **Versioning**:
|
|
210
|
+
- **`@dataVersion`**: Incremented automatically after a patch is successfully applied.
|
|
211
|
+
- **`minDataVersion`**: In `LogicConfig`, use this to ensure business logic only runs on compatible data.
|
|
212
|
+
- **`obsoleteStartingFromVersion`**: In `LogicConfig`, use this to retire old logic based on the `appVersion`.
|
|
213
|
+
- **Transactions**: Executed within Firestore transactions to ensure data integrity.
|
|
214
|
+
|
|
215
|
+
### Group Patch Engine (`queueGroupPatch`, `getGroupPatchProgress`, `BackFillPatchConfig`)
|
|
216
|
+
|
|
217
|
+
Emberflow ships with a generic, collection-wide batch engine for running a patch over every
|
|
218
|
+
document in a collection (paging 500 docs at a time, tracking progress, and self-rescheduling
|
|
219
|
+
over Pub/Sub until done). Each run is identified by a `patchType`:
|
|
220
|
+
|
|
221
|
+
- **`"back-fill"`**: a version-free, one-time bulk back-fill. The actual work is delegated to a
|
|
222
|
+
`BackFillPatchConfig` resolved by `backFillPatchName`. Emberflow ships with a built-in
|
|
223
|
+
`ancestorIdsPatchConfig` (named `"ancestor-ids"`) that populates the `@entity`/ancestor id
|
|
224
|
+
fields used internally — it is **always registered automatically**, so you can trigger it with
|
|
225
|
+
`queueGroupPatch` at any time without registering it yourself. `appVersion` is **not** used for
|
|
226
|
+
`back-fill` runs (it's only required for `"patch-logics"`).
|
|
227
|
+
- **`"patch-logics"`**: runs `runPatchLogics(appVersion, path)` for every document in the
|
|
228
|
+
collection, useful for bulk-applying `patchLogicConfigs` migrations.
|
|
229
|
+
|
|
230
|
+
#### 1. Register a custom `BackFillPatchConfig`
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { BackFillPatchConfig } from "emberflow/src/types";
|
|
234
|
+
|
|
235
|
+
const myBackFill: BackFillPatchConfig = {
|
|
236
|
+
name: "my-back-fill", // used as backFillPatchName; must be unique
|
|
237
|
+
patchFn: async (collectionPath, docs) => {
|
|
238
|
+
// Bulk-update `docs` here, e.g. via a single db.batch() commit.
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export const backFillPatchConfigs: BackFillPatchConfig[] = [myBackFill];
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Pass `backFillPatchConfigs` to `initializeEmberFlow` (see step 2 above). The built-in
|
|
246
|
+
`"ancestor-ids"` config is always registered automatically; registering a config with a
|
|
247
|
+
duplicate name, or the reserved name `"ancestor-ids"`, throws during initialization.
|
|
248
|
+
|
|
249
|
+
#### 2. Trigger a group patch
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { queueGroupPatch } from "emberflow";
|
|
253
|
+
|
|
254
|
+
// Kick off the built-in ancestor-ids back-fill for a collection
|
|
255
|
+
await queueGroupPatch({
|
|
256
|
+
path: "/users/user123/feeds",
|
|
257
|
+
patchType: "back-fill",
|
|
258
|
+
backFillPatchName: "ancestor-ids",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Kick off your own back-fill
|
|
262
|
+
await queueGroupPatch({
|
|
263
|
+
path: "/users/user123/feeds",
|
|
264
|
+
patchType: "back-fill",
|
|
265
|
+
backFillPatchName: "my-back-fill",
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Bulk-apply patch logics for a target appVersion
|
|
269
|
+
await queueGroupPatch({
|
|
270
|
+
path: "/users/user123/feeds",
|
|
271
|
+
patchType: "patch-logics",
|
|
272
|
+
appVersion: "1.2.0",
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`queueGroupPatch` accepts either a collection path or a document path (in which case the parent
|
|
277
|
+
collection is derived). There is no guard/auth on it — it's meant to be called from your own
|
|
278
|
+
trusted code (e.g. an admin-only Cloud Function or script). Placeholder paths (e.g.
|
|
279
|
+
`/users/{userId}/feeds`) are automatically hydrated into concrete collection paths before patching.
|
|
280
|
+
|
|
281
|
+
> If `backFillPatchName` doesn't resolve to a registered `BackFillPatchConfig` (or a
|
|
282
|
+
> `"patch-logics"` run is missing its `appVersion`), the run is set to status `"error"` — there is
|
|
283
|
+
> no silent default.
|
|
284
|
+
|
|
285
|
+
#### 3. Track progress
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import { getGroupPatchProgress } from "emberflow";
|
|
289
|
+
|
|
290
|
+
const progress = await getGroupPatchProgress({
|
|
291
|
+
collectionPath: "/users/user123/feeds",
|
|
292
|
+
patchType: "back-fill",
|
|
293
|
+
backFillPatchName: "ancestor-ids",
|
|
294
|
+
});
|
|
295
|
+
// progress?.status -> "running" | "completed" | "error" | "reset"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Progress is tracked independently per `patchType`/`backFillPatchName`, so different patches
|
|
299
|
+
running against the same collection never clobber each other's status. The status doc lives at
|
|
300
|
+
`@emberflow/internal/group-patches/<collection>_back-fill_<backFillPatchName>` (or
|
|
301
|
+
`..._patch-logics` for `"patch-logics"` runs).
|
|
302
|
+
|
|
303
|
+
### Collection Cleanup (`cleanupConfigs`, `CleanupConfig`)
|
|
304
|
+
|
|
305
|
+
Emberflow ships with a single, scheduled `cleanupCollections` Cloud Function that runs **every
|
|
306
|
+
hour** and purges stale documents based on declarative rules. Instead of writing a bespoke
|
|
307
|
+
scheduled function for each collection you want to prune, you describe *what* to delete with a
|
|
308
|
+
`CleanupConfig` and Emberflow handles the *how* (querying, batching, recursive subtree deletion,
|
|
309
|
+
and self-paced iteration).
|
|
310
|
+
|
|
311
|
+
Two layers of rules are merged and executed by the same runner:
|
|
312
|
+
|
|
313
|
+
1. **Built-in (framework) rules** — always active. They keep Emberflow's own internal
|
|
314
|
+
bookkeeping collections tidy (Pub/Sub `processedIds`, metric `executions`/`computations`,
|
|
315
|
+
view-logic executions, and `@actions` — the latter also nulls out the corresponding
|
|
316
|
+
`forms/{uid}/{formId}` entries in the Realtime Database).
|
|
317
|
+
2. **Project-supplied rules** — whatever you pass via the optional `cleanupConfigs` init option.
|
|
318
|
+
These are appended to the built-in rules, so your rules run alongside them.
|
|
319
|
+
|
|
320
|
+
#### 1. Define a `CleanupConfig`
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import { CleanupConfig } from "emberflow/src/types";
|
|
324
|
+
|
|
325
|
+
export const cleanupConfigs: CleanupConfig[] = [
|
|
326
|
+
{
|
|
327
|
+
// Exact collection path, or a collection-group name when isCollectionGroup=true.
|
|
328
|
+
collectionPath: "askJaris",
|
|
329
|
+
// Match this subcollection name anywhere in Firestore (collection-group query).
|
|
330
|
+
isCollectionGroup: true,
|
|
331
|
+
// Timestamp/Date field compared against the computed cutoff.
|
|
332
|
+
timestampField: "createdAt",
|
|
333
|
+
// Delete docs whose timestampField is older than (value · unit).
|
|
334
|
+
// unit is one of "hours" | "days" | "months".
|
|
335
|
+
olderThan: { value: 1, unit: "months" },
|
|
336
|
+
// Optional extra server-side filters, ANDed with the age threshold.
|
|
337
|
+
conditions: [
|
|
338
|
+
{ fieldName: "hasTopic", operator: "==", value: false },
|
|
339
|
+
],
|
|
340
|
+
// recursive defaults to true: each matched doc is deleted with its whole
|
|
341
|
+
// subtree. Set to false to delete only the matched documents.
|
|
342
|
+
recursive: true,
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Then pass `cleanupConfigs` to `initializeEmberFlow` (see step 2 in **Usage** above).
|
|
348
|
+
|
|
349
|
+
#### 2. How it Works
|
|
350
|
+
|
|
351
|
+
- **Scheduling**: A single `cleanupCollections` scheduled function runs `every 1 hours`. You can
|
|
352
|
+
override its schedule/region/memory/timeout via `projectConfig.functionsConfig.cleanupCollections`.
|
|
353
|
+
- **Cutoff computation**: `olderThan` is converted to a cutoff `Date`; documents whose
|
|
354
|
+
`timestampField` is `< cutoff` are selected. `"months"` is calendar-aware (it subtracts
|
|
355
|
+
calendar months rather than a fixed number of days).
|
|
356
|
+
- **Extra filters (`conditions`)**: Optional `QueryCondition` entries (`{ fieldName, operator,
|
|
357
|
+
value }`) are ANDed with the age threshold as additional server-side `where` clauses.
|
|
358
|
+
- **Deletion mode (`recursive`)**: Defaults to `true`, deleting each matched document together
|
|
359
|
+
with its entire subtree. Set `recursive: false` to delete only the matched documents (leaving
|
|
360
|
+
any subcollections untouched).
|
|
361
|
+
- **Isolation**: Each config is executed independently inside its own `try/catch`, so a failure
|
|
362
|
+
in one rule (e.g. a missing index) won't stop the others; failures are logged and the number of
|
|
363
|
+
deleted documents per collection is reported to the logs.
|
|
364
|
+
|
|
365
|
+
> **Composite indexes**: Collection-group queries and any `conditions` combined with the
|
|
366
|
+
> timestamp filter may require composite Firestore indexes in your project. If a rule fails,
|
|
367
|
+
> check the function logs for an index-creation link.
|
|
368
|
+
|
|
142
369
|
## Reference
|
|
143
370
|
|
|
144
371
|
For more detailed examples on how to set up these configuration files, you can check the `src/sample-custom` folder in the Emberflow repository.
|
package/lib/index-utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Action, LogicResult, LogicResultDoc, MetricExecution, RunBusinessLogicStatus, SecurityFn, TxnGet, ValidateFormResult } from "./types";
|
|
1
|
+
import { Action, GroupPatchType, LogicResult, LogicResultDoc, MetricExecution, BackFillPatchConfig, RunBusinessLogicStatus, SecurityFn, TxnGet, ValidateFormResult } from "./types";
|
|
2
2
|
import { database, firestore } from "firebase-admin";
|
|
3
3
|
import { BatchUtil } from "./utils/batch";
|
|
4
4
|
import type { FirestoreEvent } from "firebase-functions/v2/firestore";
|
|
@@ -10,6 +10,7 @@ import Transaction = firestore.Transaction;
|
|
|
10
10
|
export declare const _mockable: {
|
|
11
11
|
getViewLogicConfigs: () => import("./types").ViewLogicConfig[];
|
|
12
12
|
getPatchLogicConfigs: () => import("./types").PatchLogicConfig[];
|
|
13
|
+
getOneTimePatchConfigs: () => BackFillPatchConfig[];
|
|
13
14
|
createNowTimestamp: () => firestore.Timestamp;
|
|
14
15
|
saveMetricExecution: typeof saveMetricExecution;
|
|
15
16
|
getBatchUtil: () => BatchUtil;
|
|
@@ -33,14 +34,24 @@ export declare function onDeleteFunction(event: FirestoreEvent<QueryDocumentSnap
|
|
|
33
34
|
export declare function createMetricLogicDoc(logicName: string): Promise<void>;
|
|
34
35
|
export declare function convertLogicResultsToMetricExecutions(logicResults: LogicResult[]): MetricExecution[];
|
|
35
36
|
declare function saveMetricExecution(metricExecutions: MetricExecution[]): Promise<void>;
|
|
36
|
-
export declare function cleanMetricExecutions(_event: ScheduledEvent): Promise<void>;
|
|
37
37
|
export declare function createMetricComputation(_event: ScheduledEvent): Promise<void>;
|
|
38
|
-
export declare function cleanMetricComputations(_event: ScheduledEvent): Promise<void>;
|
|
39
38
|
export declare function distributeFnTransactional(txn: Transaction, logicResults: LogicResult[], appVersion: string): Promise<LogicResultDoc[]>;
|
|
40
39
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* Built-in, version-free bulk back-fill that restores the original ancestor-ids
|
|
41
|
+
* patching behavior. Runs `addAncestorIds` per doc and commits all updates in a
|
|
42
|
+
* single `db.batch()`, only updating keys that are currently undefined.
|
|
44
43
|
*/
|
|
45
|
-
export declare
|
|
44
|
+
export declare const ancestorIdsPatchConfig: BackFillPatchConfig;
|
|
45
|
+
export interface PatchGroupDocsParams {
|
|
46
|
+
collectionPath: string;
|
|
47
|
+
patchType: GroupPatchType;
|
|
48
|
+
backFillPatchName?: string;
|
|
49
|
+
appVersion?: string;
|
|
50
|
+
lastPatchedId?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Internal engine logic for patching a collection page-by-page. Exported for background worker.
|
|
54
|
+
* @param {PatchGroupDocsParams} params The collection path, patch type, and cursor.
|
|
55
|
+
*/
|
|
56
|
+
export declare function patchGroupDocs(params: PatchGroupDocsParams): Promise<void>;
|
|
46
57
|
export {};
|
package/lib/index-utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.patchGroupDocs = exports.ancestorIdsPatchConfig = exports.distributeFnTransactional = exports.createMetricComputation = exports.convertLogicResultsToMetricExecutions = exports.createMetricLogicDoc = exports.onDeleteFunction = exports.expandConsolidateAndGroupByDstPath = exports.getSecurityFn = exports.groupDocsByTargetDocPath = exports.runBusinessLogics = exports.delayFormSubmissionAndCheckIfCancelled = exports.getFormModifiedFields = exports.validateForm = exports.distributeLater = exports.distributeFnNonTransactional = exports.distributeDoc = exports._mockable = void 0;
|
|
4
4
|
const firebase_admin_1 = require("firebase-admin");
|
|
5
5
|
const index_1 = require("./index");
|
|
6
6
|
const paths_1 = require("./utils/paths");
|
|
@@ -15,6 +15,7 @@ var Timestamp = firebase_admin_1.firestore.Timestamp;
|
|
|
15
15
|
exports._mockable = {
|
|
16
16
|
getViewLogicConfigs: () => index_1.viewLogicConfigs,
|
|
17
17
|
getPatchLogicConfigs: () => index_1.patchLogicConfigs,
|
|
18
|
+
getOneTimePatchConfigs: () => index_1.backFillPatchConfigs,
|
|
18
19
|
createNowTimestamp: () => index_1.admin.firestore.Timestamp.now(),
|
|
19
20
|
saveMetricExecution: saveMetricExecution,
|
|
20
21
|
getBatchUtil: () => batch_1.BatchUtil.getInstance(),
|
|
@@ -91,18 +92,6 @@ async function distributeDoc(logicResultDoc, appVersion, batch, txn) {
|
|
|
91
92
|
if (doc) {
|
|
92
93
|
if (action === "create") {
|
|
93
94
|
(0, paths_1.addAncestorIds)(dstPath, doc);
|
|
94
|
-
// Only patch siblings in real environment, not during tests that expect a specific number of firestore calls
|
|
95
|
-
if (process.env.JEST_WORKER_ID === undefined) {
|
|
96
|
-
const collectionPath = (0, paths_1.getParentPath)(dstPath);
|
|
97
|
-
if (collectionPath && collectionPath.includes("/")) {
|
|
98
|
-
const patchStatusPath = `@emberflow/internal/group-query-patches/${collectionPath.replace(/\//g, "_")}`;
|
|
99
|
-
const patchStatusRef = index_1.db.doc(patchStatusPath);
|
|
100
|
-
const patchStatusDoc = await patchStatusRef.get();
|
|
101
|
-
if (!patchStatusDoc.exists) {
|
|
102
|
-
await (0, distribution_1.queueAncestorIdsPatch)(dstPath);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
95
|
}
|
|
107
96
|
let updateData = {};
|
|
108
97
|
if (destProp) {
|
|
@@ -140,7 +129,7 @@ async function distributeDoc(logicResultDoc, appVersion, batch, txn) {
|
|
|
140
129
|
}
|
|
141
130
|
else if (action === "submit-form") {
|
|
142
131
|
if (txn) {
|
|
143
|
-
console.
|
|
132
|
+
console.debug("Submit-form in transactional logic result will be handled after transaction success");
|
|
144
133
|
}
|
|
145
134
|
else {
|
|
146
135
|
console.debug("Queuing submit form...");
|
|
@@ -499,20 +488,6 @@ async function saveMetricExecution(metricExecutions) {
|
|
|
499
488
|
});
|
|
500
489
|
}
|
|
501
490
|
}
|
|
502
|
-
async function cleanMetricExecutions(_event) {
|
|
503
|
-
console.info("Running cleanMetricExecutions");
|
|
504
|
-
const metricsSnapshot = await index_1.db.collection("@metrics").get();
|
|
505
|
-
let i = 0;
|
|
506
|
-
for (const metricDoc of metricsSnapshot.docs) {
|
|
507
|
-
const query = metricDoc.ref.collection("executions")
|
|
508
|
-
.where("execDate", "<", new Date(Date.now() - 1000 * 60 * 60 * 24 * 7));
|
|
509
|
-
await (0, misc_1.deleteCollection)(query, (snapshot) => {
|
|
510
|
-
i += snapshot.size;
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
console.info(`Cleaned ${i} logic metric executions`);
|
|
514
|
-
}
|
|
515
|
-
exports.cleanMetricExecutions = cleanMetricExecutions;
|
|
516
491
|
async function createMetricComputation(_event) {
|
|
517
492
|
console.info("Creating metric computation");
|
|
518
493
|
const metricsSnapshot = await index_1.db.collection("@metrics").get();
|
|
@@ -556,20 +531,6 @@ async function createMetricComputation(_event) {
|
|
|
556
531
|
}
|
|
557
532
|
}
|
|
558
533
|
exports.createMetricComputation = createMetricComputation;
|
|
559
|
-
async function cleanMetricComputations(_event) {
|
|
560
|
-
console.info("Running cleanMetricComputations");
|
|
561
|
-
const metricsSnapshot = await index_1.db.collection("@metrics").get();
|
|
562
|
-
let i = 0;
|
|
563
|
-
for (const metricDoc of metricsSnapshot.docs) {
|
|
564
|
-
const query = metricDoc.ref.collection("computations")
|
|
565
|
-
.where("createdAt", "<", new Date(Date.now() - 1000 * 60 * 60 * 24 * 30));
|
|
566
|
-
await (0, misc_1.deleteCollection)(query, (snapshot) => {
|
|
567
|
-
i += snapshot.size;
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
console.info(`Cleaned ${i} logic metric computations`);
|
|
571
|
-
}
|
|
572
|
-
exports.cleanMetricComputations = cleanMetricComputations;
|
|
573
534
|
async function distributeFnTransactional(txn, logicResults, appVersion) {
|
|
574
535
|
const distributedLogicResultDocs = [];
|
|
575
536
|
const transactionalResults = logicResults.filter((result) => result.transactional);
|
|
@@ -590,12 +551,40 @@ async function distributeFnTransactional(txn, logicResults, appVersion) {
|
|
|
590
551
|
}
|
|
591
552
|
exports.distributeFnTransactional = distributeFnTransactional;
|
|
592
553
|
/**
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
554
|
+
* Built-in, version-free bulk back-fill that restores the original ancestor-ids
|
|
555
|
+
* patching behavior. Runs `addAncestorIds` per doc and commits all updates in a
|
|
556
|
+
* single `db.batch()`, only updating keys that are currently undefined.
|
|
596
557
|
*/
|
|
597
|
-
|
|
598
|
-
|
|
558
|
+
exports.ancestorIdsPatchConfig = {
|
|
559
|
+
name: "ancestor-ids",
|
|
560
|
+
patchFn: async (_collectionPath, docs) => {
|
|
561
|
+
const currentBatch = index_1.db.batch();
|
|
562
|
+
let count = 0;
|
|
563
|
+
for (const doc of docs) {
|
|
564
|
+
const data = doc.data();
|
|
565
|
+
const updatedData = {};
|
|
566
|
+
const fullPath = doc.ref.path.startsWith("/") ? doc.ref.path : `/${doc.ref.path}`;
|
|
567
|
+
(0, paths_1.addAncestorIds)(fullPath, updatedData);
|
|
568
|
+
const keysToUpdate = Object.keys(updatedData).filter((key) => data[key] === undefined);
|
|
569
|
+
if (keysToUpdate.length > 0) {
|
|
570
|
+
const patchData = {};
|
|
571
|
+
keysToUpdate.forEach((key) => patchData[key] = updatedData[key]);
|
|
572
|
+
currentBatch.update(doc.ref, patchData);
|
|
573
|
+
count++;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (count > 0) {
|
|
577
|
+
await currentBatch.commit();
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
/**
|
|
582
|
+
* Internal engine logic for patching a collection page-by-page. Exported for background worker.
|
|
583
|
+
* @param {PatchGroupDocsParams} params The collection path, patch type, and cursor.
|
|
584
|
+
*/
|
|
585
|
+
async function patchGroupDocs(params) {
|
|
586
|
+
const { collectionPath, patchType, backFillPatchName, appVersion, lastPatchedId } = params;
|
|
587
|
+
const patchStatusPath = (0, distribution_1.getGroupPatchStatusPath)(collectionPath, patchType, backFillPatchName);
|
|
599
588
|
const patchStatusRef = index_1.db.doc(patchStatusPath);
|
|
600
589
|
const patchStatusDoc = await patchStatusRef.get();
|
|
601
590
|
const patchStatusData = patchStatusDoc.data();
|
|
@@ -604,59 +593,65 @@ async function patchSiblingsWithAncestorIds(collectionPath, lastPatchedId) {
|
|
|
604
593
|
}
|
|
605
594
|
// Handle "reset" status by starting from scratch
|
|
606
595
|
const effectiveLastId = (patchStatusData === null || patchStatusData === void 0 ? void 0 : patchStatusData.status) === "reset" ? undefined : lastPatchedId;
|
|
607
|
-
console.info(`[
|
|
596
|
+
console.info(`[GroupPatch] Patching docs for collection: ${collectionPath}${effectiveLastId ? ` starting from ${effectiveLastId}` : ""}`);
|
|
608
597
|
let query = index_1.db.collection(collectionPath).orderBy(index_1.admin.firestore.FieldPath.documentId());
|
|
609
598
|
if (effectiveLastId) {
|
|
610
599
|
query = query.startAfter(effectiveLastId);
|
|
611
600
|
}
|
|
612
|
-
const
|
|
613
|
-
if (
|
|
601
|
+
const docsSnapshot = await query.limit(500).get();
|
|
602
|
+
if (docsSnapshot.empty) {
|
|
614
603
|
await patchStatusRef.set({
|
|
615
604
|
status: "completed",
|
|
605
|
+
patchType,
|
|
606
|
+
backFillPatchName: backFillPatchName !== null && backFillPatchName !== void 0 ? backFillPatchName : null,
|
|
616
607
|
patchedAt: index_1.admin.firestore.Timestamp.now(),
|
|
617
608
|
collectionPath,
|
|
618
609
|
}, { merge: true });
|
|
619
|
-
console.info(`[
|
|
610
|
+
console.info(`[GroupPatch] Completed patching for collection: ${collectionPath}`);
|
|
620
611
|
return;
|
|
621
612
|
}
|
|
622
|
-
const
|
|
623
|
-
let count = 0;
|
|
624
|
-
let lastId = lastPatchedId;
|
|
625
|
-
for (const siblingDoc of siblingsSnapshot.docs) {
|
|
626
|
-
const data = siblingDoc.data();
|
|
627
|
-
const updatedData = {};
|
|
628
|
-
const fullPath = siblingDoc.ref.path.startsWith("/") ? siblingDoc.ref.path : `/${siblingDoc.ref.path}`;
|
|
629
|
-
(0, paths_1.addAncestorIds)(fullPath, updatedData);
|
|
630
|
-
const keysToUpdate = Object.keys(updatedData).filter((key) => data[key] === undefined);
|
|
631
|
-
if (keysToUpdate.length > 0) {
|
|
632
|
-
const patchData = {};
|
|
633
|
-
keysToUpdate.forEach((key) => patchData[key] = updatedData[key]);
|
|
634
|
-
currentBatch.update(siblingDoc.ref, patchData);
|
|
635
|
-
count++;
|
|
636
|
-
}
|
|
637
|
-
lastId = siblingDoc.id;
|
|
638
|
-
}
|
|
613
|
+
const lastId = docsSnapshot.docs[docsSnapshot.docs.length - 1].id;
|
|
639
614
|
try {
|
|
640
|
-
if (
|
|
641
|
-
|
|
615
|
+
if (patchType === "patch-logics") {
|
|
616
|
+
if (!appVersion) {
|
|
617
|
+
throw new Error(`Missing appVersion for patch-logics on collection: ${collectionPath}`);
|
|
618
|
+
}
|
|
619
|
+
for (const doc of docsSnapshot.docs) {
|
|
620
|
+
const fullPath = doc.ref.path.startsWith("/") ? doc.ref.path : `/${doc.ref.path}`;
|
|
621
|
+
await (0, patch_logics_1.runPatchLogics)(appVersion, fullPath);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else if (patchType === "back-fill") {
|
|
625
|
+
const config = exports._mockable.getOneTimePatchConfigs().find((c) => c.name === backFillPatchName);
|
|
626
|
+
if (!config) {
|
|
627
|
+
throw new Error(`Unresolved back-fill patch "${backFillPatchName}" for collection: ${collectionPath}`);
|
|
628
|
+
}
|
|
629
|
+
await config.patchFn(collectionPath, docsSnapshot.docs);
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
throw new Error(`Unknown patchType "${patchType}" for collection: ${collectionPath}`);
|
|
642
633
|
}
|
|
643
|
-
const totalPatched = ((patchStatusData === null || patchStatusData === void 0 ? void 0 : patchStatusData.status) === "reset" ? 0 : ((patchStatusData === null || patchStatusData === void 0 ? void 0 : patchStatusData.count) || 0)) +
|
|
634
|
+
const totalPatched = ((patchStatusData === null || patchStatusData === void 0 ? void 0 : patchStatusData.status) === "reset" ? 0 : ((patchStatusData === null || patchStatusData === void 0 ? void 0 : patchStatusData.count) || 0)) + docsSnapshot.docs.length;
|
|
644
635
|
await patchStatusRef.set({
|
|
645
|
-
status: "
|
|
636
|
+
status: "running",
|
|
637
|
+
patchType,
|
|
638
|
+
backFillPatchName: backFillPatchName !== null && backFillPatchName !== void 0 ? backFillPatchName : null,
|
|
646
639
|
lastPatchedId: lastId,
|
|
647
640
|
collectionPath,
|
|
648
641
|
count: totalPatched,
|
|
649
642
|
updatedAt: index_1.admin.firestore.Timestamp.now(),
|
|
650
643
|
}, { merge: true });
|
|
651
|
-
console.info(`[
|
|
644
|
+
console.info(`[GroupPatch] Batch Complete: Patched ${docsSnapshot.docs.length} docs for collection: ${collectionPath}. Total patched so far: ${totalPatched}. Rescheduling next batch...`);
|
|
652
645
|
// Reschedule for next batch
|
|
653
|
-
await (0, distribution_1.
|
|
646
|
+
await (0, distribution_1.queueGroupPatch)({ path: collectionPath, patchType, backFillPatchName, appVersion, lastPatchedId: lastId });
|
|
654
647
|
}
|
|
655
648
|
catch (error) {
|
|
656
|
-
console.error(`[
|
|
649
|
+
console.error(`[GroupPatch] Error in patchGroupDocs for ${collectionPath}:`, error);
|
|
657
650
|
// On error, we set status to error and store the error message
|
|
658
651
|
await patchStatusRef.set({
|
|
659
652
|
status: "error",
|
|
653
|
+
patchType,
|
|
654
|
+
backFillPatchName: backFillPatchName !== null && backFillPatchName !== void 0 ? backFillPatchName : null,
|
|
660
655
|
error: error instanceof Error ? error.message : String(error),
|
|
661
656
|
updatedAt: index_1.admin.firestore.Timestamp.now(),
|
|
662
657
|
}, { merge: true });
|
|
@@ -666,5 +661,5 @@ async function patchSiblingsWithAncestorIds(collectionPath, lastPatchedId) {
|
|
|
666
661
|
throw error;
|
|
667
662
|
}
|
|
668
663
|
}
|
|
669
|
-
exports.
|
|
664
|
+
exports.patchGroupDocs = patchGroupDocs;
|
|
670
665
|
//# sourceMappingURL=index-utils.js.map
|