Skip to content

Commit 9ff611e

Browse files
committed
Fix persistent Apify publication storage [skip ci]
1 parent 45bc695 commit 9ff611e

4 files changed

Lines changed: 68 additions & 17 deletions

File tree

‎web/data/apify-config.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
"url": "https://www.linkedin.com/company/uffescience-research-group"
88
}
99
],
10-
"storeId": "tOJCj89elzSvIqNdg",
10+
"storeName": "uffescience-publications",
1111
"backfillRunId": "xe43itFUVizUfgCWt"
1212
}

‎web/lib/apify-client.js‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export function createApifyClient(token, fetchImpl = fetch) {
2+
return async function api(endpoint, { method = 'GET', body } = {}) {
3+
const response = await fetchImpl(`https://api.apify.com/v2/${endpoint}`, {
4+
method,
5+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
6+
body: body === undefined ? undefined : JSON.stringify(body),
7+
signal: AbortSignal.timeout(30000),
8+
});
9+
if (!response.ok) {
10+
const error = new Error(`Apify respondeu HTTP ${response.status} em ${method} ${endpoint}.`);
11+
error.status = response.status;
12+
throw error;
13+
}
14+
if (response.status === 204) return null;
15+
const text = await response.text();
16+
return text ? JSON.parse(text) : null;
17+
};
18+
}
19+
20+
export async function openPublicationStore(api, config) {
21+
// Named stores are retained and POST is get-or-create, including after deletion.
22+
const name = config.storeName || 'uffescience-publications';
23+
const result = await api(`key-value-stores?name=${encodeURIComponent(name)}`, { method: 'POST' });
24+
if (!result?.data?.id) throw new Error('Apify não retornou o ID do armazenamento nomeado.');
25+
return result.data.id;
26+
}

‎web/scripts/apify-client.test.mjs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { createApifyClient, openPublicationStore } from '../lib/apify-client.js';
4+
5+
test('opens a named persistent store instead of reusing an expired ID', async () => {
6+
const calls = [];
7+
const api = createApifyClient('test-key', async (url, options) => {
8+
calls.push([url, options.method]);
9+
return new Response(JSON.stringify({ data: { id: 'persistent-store' } }), { status: 201 });
10+
});
11+
assert.equal(await openPublicationStore(api, { storeId: 'expired-store' }), 'persistent-store');
12+
assert.deepEqual(calls, [
13+
['https://api.apify.com/v2/key-value-stores?name=uffescience-publications', 'POST'],
14+
]);
15+
});
16+
17+
test('accepts successful writes with an empty body', async () => {
18+
const api = createApifyClient('test-key', async () => new Response('', { status: 201 }));
19+
assert.equal(
20+
await api('key-value-stores/store/records/WEEK', { method: 'PUT', body: { id: 'run' } }),
21+
null,
22+
);
23+
});
24+
25+
test('identifies the endpoint and HTTP status without disclosing credentials', async () => {
26+
const api = createApifyClient('secret-test-key', async () => new Response('{}', { status: 404 }));
27+
await assert.rejects(api('actor-tasks/missing/runs'), (error) => {
28+
assert.equal(error.status, 404);
29+
assert.match(error.message, /GET actor-tasks\/missing\/runs/);
30+
assert.ok(!error.message.includes('secret-test-key'));
31+
return true;
32+
});
33+
});

‎web/scripts/sync-publications.mjs‎

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
22
import path from 'node:path';
33
import { fileURLToPath } from 'node:url';
44
import { createHash } from 'node:crypto';
5+
import { createApifyClient, openPublicationStore } from '../lib/apify-client.js';
56
import {
67
normalizePublication,
78
mergePublications,
@@ -51,17 +52,7 @@ try {
5152
console.log('Uma sincronização já está em andamento.');
5253
process.exit(0);
5354
}
54-
async function api(endpoint, { method = 'GET', body } = {}) {
55-
const response = await fetch(`https://api.apify.com/v2/${endpoint}`, {
56-
method,
57-
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
58-
body: body === undefined ? undefined : JSON.stringify(body),
59-
signal: AbortSignal.timeout(30000),
60-
});
61-
if (!response.ok) throw new Error(`Apify respondeu HTTP ${response.status}.`);
62-
if (response.status === 204) return null;
63-
return response.json();
64-
}
55+
const api = createApifyClient(token);
6556
const mediaCache = new Map();
6657
async function archiveImage(url) {
6758
if (!mediaUrl(url)) return null;
@@ -108,6 +99,7 @@ async function archiveImage(url) {
10899
}
109100
try {
110101
const config = JSON.parse(await fs.readFile(path.join(dataDir, 'apify-config.json'), 'utf8'));
102+
const storeId = await openPublicationStore(api, config);
111103
const incoming = [],
112104
sources = [];
113105
for (let index = 0; index < config.tasks.length; index++) {
@@ -123,9 +115,9 @@ try {
123115
// Persist the run ID remotely before waiting. Retries resume it instead of starting another extraction.
124116
const record = `WEEK-${weekKey}-${source.id}`;
125117
try {
126-
run = await api(`key-value-stores/${config.storeId}/records/${record}`);
118+
run = await api(`key-value-stores/${storeId}/records/${record}`);
127119
} catch (error) {
128-
if (!error.message.includes('404')) throw error;
120+
if (error.status !== 404) throw error;
129121
}
130122
if (!run) {
131123
const runs = (await api(`actor-tasks/${task.id}/runs?limit=100&desc=1`)).data.items;
@@ -146,7 +138,7 @@ try {
146138
},
147139
})
148140
).data;
149-
await api(`key-value-stores/${config.storeId}/records/${record}`, {
141+
await api(`key-value-stores/${storeId}/records/${record}`, {
150142
method: 'PUT',
151143
body: { id: run.id },
152144
});
@@ -242,9 +234,9 @@ try {
242234
const temporary = `${storePath}.tmp`;
243235
await fs.writeFile(temporary, JSON.stringify(store, null, 2));
244236
await fs.rename(temporary, storePath);
245-
if (config.storeId) {
237+
if (storeId) {
246238
try {
247-
await api(`key-value-stores/${config.storeId}/records/PUBLICATIONS`, {
239+
await api(`key-value-stores/${storeId}/records/PUBLICATIONS`, {
248240
method: 'PUT',
249241
body: store,
250242
});

0 commit comments

Comments
 (0)