Skip to content

Commit 275e904

Browse files
refactor(pushDataWithSchemaRepair): rename safePushData to pushDataWithSchemaRepair (#6)
Rename the exported function, its option/result types, the source and test files, and every doc/log/comment mention so the name says what the wrapper actually does: push, and repair against the dataset schema. - safePushData -> pushDataWithSchemaRepair - SafePushDataResult -> PushDataWithSchemaRepairResult - SafePushDataOptions -> PushDataWithSchemaRepairOptions - src/safePushData.ts -> src/pushDataWithSchemaRepair.ts - test/safePushData.test.ts -> test/pushDataWithSchemaRepair.test.ts Log-line prefixes change with the name, and the tests asserting on them were updated to match. Historical CHANGELOG entries are left as-is. Claude-Session: https://claude.ai/code/session_012iFDPXYxFwFGUe4PEwSAj7 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 30260d7 commit 275e904

5 files changed

Lines changed: 113 additions & 95 deletions

File tree

README.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
# safePushData
1+
# pushDataWithSchemaRepair
22

33
TypeScript library wrapper that survives Apify dataset schema-validation
44
failures. When an upstream data source produces an item that doesn't match
55
the dataset's JSON schema, the platform rejects the **entire batch** with
6-
a 400 error — losing every other valid item in that push. `safePushData`
7-
parses the validation error, strips the offending fields, and retries.
6+
a 400 error — losing every other valid item in that push.
7+
`pushDataWithSchemaRepair` parses the validation error, strips the
8+
offending fields, and retries.
89

910
## Usage
1011

1112
```ts
1213
import { Actor } from 'apify';
13-
import { safePushData } from 'apify-actor-utils';
14+
import { pushDataWithSchemaRepair } from 'apify-actor-utils';
1415

1516
await Actor.init();
1617

17-
const result = await safePushData((batch) => Actor.pushData(batch), items);
18+
const result = await pushDataWithSchemaRepair((batch) => Actor.pushData(batch), items);
1819

1920
console.log(result);
2021
// { pushedCount: 2, droppedItems: [...], attemptCount: 2, pushResult: undefined }
@@ -28,7 +29,7 @@ Whatever `pushFn` resolves to comes back as `pushResult`, so a push
2829
function with a meaningful return value stays usable:
2930

3031
```ts
31-
const { pushResult } = await safePushData((batch) => client.dataset(id).pushItems(batch), items);
32+
const { pushResult } = await pushDataWithSchemaRepair((batch) => client.dataset(id).pushItems(batch), items);
3233
```
3334

3435
## Performance notes
@@ -96,8 +97,8 @@ Every failed round logs which fields went wrong, so you can fix the schema
9697
(or the scraper) without digging through the returned `droppedItems`:
9798

9899
```
99-
safePushData: schema validation failed on attempt 1: 12 invalid item(s); repaired fields: /age (type), /name (required), /tags/[] (type); dropped 2 item(s) on unfixable fields: /email (format); retrying with 10 item(s).
100-
safePushData: gave up after 5 attempts; dropped 3 item(s) still failing on fields: /sku (pattern); pushing the 9 valid item(s) left.
100+
pushDataWithSchemaRepair: schema validation failed on attempt 1: 12 invalid item(s); repaired fields: /age (type), /name (required), /tags/[] (type); dropped 2 item(s) on unfixable fields: /email (format); retrying with 10 item(s).
101+
pushDataWithSchemaRepair: gave up after 5 attempts; dropped 3 item(s) still failing on fields: /sku (pattern); pushing the 9 valid item(s) left.
101102
```
102103

103104
The field list is a **set**, not a per-item breakdown — one bad field
@@ -118,7 +119,7 @@ Names say what they hold: `*Count` is a number, `*Items` is an array of
118119
objects.
119120

120121
```ts
121-
interface SafePushDataResult<T, R = unknown> {
122+
interface PushDataWithSchemaRepairResult<T, R = unknown> {
122123
/** How many of the caller's items made it into the dataset. */
123124
pushedCount: number;
124125
/** The items we couldn't repair, each with the errors that doomed it. */
@@ -144,12 +145,12 @@ Highlights:
144145

145146
```
146147
.
147-
├── index.ts # package entry point, re-exports src/
148-
├── src/safePushData.ts # the library
149-
├── test/safePushData.test.ts # node:test suite
148+
├── index.ts # package entry point, re-exports src/
149+
├── src/pushDataWithSchemaRepair.ts # the library
150+
├── test/pushDataWithSchemaRepair.test.ts # node:test suite
150151
├── scripts/
151-
│ ├── check-pushdata.mjs # CI guard against direct .pushData() calls
152-
│ └── probe-errors.mjs # reference: re-derive the API error shape
152+
│ ├── check-pushdata.mjs # CI guard against direct .pushData() calls
153+
│ └── probe-errors.mjs # reference: re-derive the API error shape
153154
├── tsconfig.json
154155
└── package.json
155156
```

index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
export { safePushData, isSchemaValidationError } from './src/safePushData.js';
1+
export { pushDataWithSchemaRepair, isSchemaValidationError } from './src/pushDataWithSchemaRepair.js';
22
export type {
33
ValidationError,
44
DroppedItem,
5-
SafePushDataResult,
6-
SafePushDataOptions,
5+
PushDataWithSchemaRepairResult,
6+
PushDataWithSchemaRepairOptions,
77
PushFn,
8-
} from './src/safePushData.js';
8+
} from './src/pushDataWithSchemaRepair.js';

scripts/check-pushdata.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// CI check: forbid direct calls to `.pushData(...)` anywhere in the repo.
22
//
3-
// safePushData is a library wrapper; callers pass `Actor.pushData` (or any
4-
// dataset's push function) as `pushFn`. The whole point is to ensure nothing
5-
// bypasses the wrapper, so a stray `Actor.pushData(items)` inside this repo
6-
// would defeat that goal.
3+
// pushDataWithSchemaRepair is a library wrapper; callers pass `Actor.pushData`
4+
// (or any dataset's push function) as `pushFn`. The whole point is to ensure
5+
// nothing bypasses the wrapper, so a stray `Actor.pushData(items)` inside this
6+
// repo would defeat that goal.
77
//
88
// Exits non-zero (and prints the offending lines) if any file under src/
99
// or test/ contains a `.pushData(` call. This script itself, and the
@@ -72,7 +72,7 @@ if (offenders.length === 0) {
7272
}
7373

7474
console.error('check-pushdata: FAIL — direct .pushData() calls are forbidden.');
75-
console.error('Wrap every push through safePushData and pass the push function as pushFn.\n');
75+
console.error('Wrap every push through pushDataWithSchemaRepair and pass the push function as pushFn.\n');
7676
for (const o of offenders) {
7777
console.error(` ${o.file}:${o.line} ${o.text}`);
7878
}
Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
// safePushData: parse the Apify dataset schema-validation error, repair the
2-
// offending items (strip bad fields, placeholder missing required ones), and
3-
// retry the push.
1+
// pushDataWithSchemaRepair: parse the Apify dataset schema-validation error,
2+
// repair the offending items (strip bad fields, placeholder missing required
3+
// ones), and retry the push.
44
//
55
// NOTE: instead of recursively healing the data one error-round at a time, we
66
// could parse the Actor's `dataset_schema.json` up front and fix every item in
@@ -61,7 +61,7 @@ export interface DroppedItem<T> {
6161

6262
// Field names say what they hold: `*Count` is a number, `*Items` is an array
6363
// of objects. `R` is whatever the caller's push function resolves to.
64-
export interface SafePushDataResult<T, R = unknown> {
64+
export interface PushDataWithSchemaRepairResult<T, R = unknown> {
6565
/** How many of the caller's items made it into the dataset. */
6666
pushedCount: number;
6767
/** The items we couldn't repair, each with the errors that doomed it. */
@@ -75,7 +75,7 @@ export interface SafePushDataResult<T, R = unknown> {
7575
pushResult?: R;
7676
}
7777

78-
export interface SafePushDataOptions {
78+
export interface PushDataWithSchemaRepairOptions {
7979
maxAttempts?: number;
8080
}
8181

@@ -92,11 +92,11 @@ export type PushFn<T, R = unknown> = (items: T[]) => Promise<R>;
9292
*
9393
* Whatever `pushFn` resolves to is handed back untouched as `pushResult`.
9494
*/
95-
export async function safePushData<T, R = unknown>(
95+
export async function pushDataWithSchemaRepair<T, R = unknown>(
9696
pushFn: PushFn<T, R>,
9797
input: T | T[],
98-
options: SafePushDataOptions = {},
99-
): Promise<SafePushDataResult<T, R>> {
98+
options: PushDataWithSchemaRepairOptions = {},
99+
): Promise<PushDataWithSchemaRepairResult<T, R>> {
100100
const items = Array.isArray(input) ? input : [input];
101101

102102
// Happy path: assume validation will succeed (the overwhelmingly common
@@ -118,7 +118,7 @@ async function cleanAndRetry<T, R>(
118118
originalItems: readonly T[],
119119
initialError: SchemaValidationError,
120120
maxAttempts: number,
121-
): Promise<SafePushDataResult<T, R>> {
121+
): Promise<PushDataWithSchemaRepairResult<T, R>> {
122122
// working[i] is what we'll send on the next push. We mutate this array
123123
// in place (splicing drops, replacing cleaned entries); the caller's
124124
// `originalItems` is never touched.
@@ -155,7 +155,7 @@ async function cleanAndRetry<T, R>(
155155
// everything that isn't in it, so "original minus dropped" is exactly what
156156
// landed. (A rejected push stores nothing at all — not even the items the
157157
// API found no fault with.)
158-
const result = (attemptCount: number, pushResult?: R): SafePushDataResult<T, R> => ({
158+
const result = (attemptCount: number, pushResult?: R): PushDataWithSchemaRepairResult<T, R> => ({
159159
pushedCount: originalItems.length - dropped.length,
160160
droppedItems: dropped,
161161
attemptCount,
@@ -182,7 +182,9 @@ async function cleanAndRetry<T, R>(
182182
// position outside the batch we actually sent) instead of
183183
// crashing on `working[i]` being undefined.
184184
if (i < 0 || i >= working.length) {
185-
console.log(`safePushData: ignoring out-of-range itemPosition ${i} in validation error response.`);
185+
console.log(
186+
`pushDataWithSchemaRepair: ignoring out-of-range itemPosition ${i} in validation error response.`,
187+
);
186188
continue;
187189
}
188190
const cleaned = cleanItemFields(working[i], invalid.validationErrors, placeholderPaths[i]);
@@ -198,7 +200,7 @@ async function cleanAndRetry<T, R>(
198200
}
199201

200202
const report = [
201-
`safePushData: schema validation failed on attempt ${attempts}: ${lastError.data.invalidItems.length} invalid item(s)`,
203+
`pushDataWithSchemaRepair: schema validation failed on attempt ${attempts}: ${lastError.data.invalidItems.length} invalid item(s)`,
202204
];
203205
if (repairedFields.size > 0) report.push(`repaired fields: ${formatFields(repairedFields)}`);
204206
if (droppedThisRound > 0) {
@@ -227,7 +229,7 @@ async function cleanAndRetry<T, R>(
227229
unresolved++;
228230
dropAt(i, roundErrors[i]);
229231
}
230-
const giveUp = [`safePushData: gave up after ${maxAttempts} attempts`];
232+
const giveUp = [`pushDataWithSchemaRepair: gave up after ${maxAttempts} attempts`];
231233
if (unresolved > 0) {
232234
giveUp.push(`dropped ${unresolved} item(s) still failing on fields: ${formatFields(unresolvedFields)}`);
233235
}
@@ -249,7 +251,9 @@ async function cleanAndRetry<T, R>(
249251
for (const invalid of err.data.invalidItems) {
250252
errorsAt.set(invalid.itemPosition, invalid.validationErrors);
251253
}
252-
console.log(`safePushData: final push of ${working.length} item(s) was rejected too; dropping them.`);
254+
console.log(
255+
`pushDataWithSchemaRepair: final push of ${working.length} item(s) was rejected too; dropping them.`,
256+
);
253257
for (let i = working.length - 1; i >= 0; i--) dropAt(i, errorsAt.get(i) ?? NO_ERRORS);
254258
return result(attempts);
255259
}

0 commit comments

Comments
 (0)