Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions apps/api/src/action-handler/actions/store-data.action.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,60 @@ describe('StoreDataAction', () => {
expect((action as any).storedData.testKey).toBe('testValue');
});

it('stores a dotted key as a nested object', async () => {
// Regression: templates resolve {{storedData.a.b}} as a PATH, so a flat
// key "a.b" is invisible to them. The DB loader builds nested objects
// (convertJobDataToNestedObject) — the runtime store has to match,
// otherwise a value written during a run cannot be read back in it.
action.params = { key: 'amazon.downloadedInvoices', value: 'a,b' } as any;
(action as any).storedData = {};

await action.run();

expect((action as any).storedData.amazon.downloadedInvoices).toBe('a,b');
});

it('keeps the flat key as well, for configs that rely on it', async () => {
action.params = { key: 'amazon.lastOrderId', value: '306-1' } as any;
(action as any).storedData = {};

await action.run();

expect((action as any).storedData['amazon.lastOrderId']).toBe('306-1');
});

it('handles deeply nested keys', async () => {
action.params = { key: 'a.b.c.d', value: 'tief' } as any;
(action as any).storedData = {};

await action.run();

expect((action as any).storedData.a.b.c.d).toBe('tief');
});

it('merges into an existing branch instead of replacing it', async () => {
action.params = { key: 'amazon.lastOrderId', value: '306-2' } as any;
(action as any).storedData = { amazon: { downloadedInvoices: 'a,b' } };

await action.run();

expect((action as any).storedData.amazon).toEqual({
downloadedInvoices: 'a,b',
lastOrderId: '306-2',
});
});

it('replaces a non-object value that blocks the path', async () => {
// "amazon" was stored as a plain string earlier — without this the
// nested write would throw or silently do nothing.
action.params = { key: 'amazon.foo', value: 'bar' } as any;
(action as any).storedData = { amazon: 'irgendwas' };

await action.run();

expect((action as any).storedData.amazon.foo).toBe('bar');
});

it('should warn when databaseService is not available', async () => {
action.params = { key: 'k', value: 'v' } as any;
(action as any).data = { databaseService: null };
Expand Down
45 changes: 44 additions & 1 deletion apps/api/src/action-handler/actions/store-data.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,22 @@ export class StoreDataAction extends BaseAction<StoreDataParams> {
async run(): Promise<void> {
const { key, value, persist = false, attachToRun } = this.params;

// Speichere die neuen Werte im Runtime-Speicher (für Template-Zugriff in diesem Run)
// Speichere die neuen Werte im Runtime-Speicher (für Template-Zugriff in diesem Run).
//
// Der Wert muss GENAUSO abgelegt werden, wie er beim Laden aus der DB
// aufgebaut wird — dort zerlegt convertJobDataToNestedObject() den Key am
// Punkt in ein verschachteltes Objekt. Eine rein flache Ablage unter
// "amazon.downloadedInvoices" ist für Handlebars unsichtbar:
// {{storedData.amazon.downloadedInvoices}} löst als Pfad auf und findet
// den flachen Key nicht — der Ausdruck liefert dann einen leeren String.
//
// Folge vor diesem Fix: Ein im selben Run gespeicherter Wert war für alle
// folgenden Actions unsichtbar. Erst der nächste Run sah ihn, weil er den
// Umweg über die DB nahm. Wer damit eine Liste fortschreibt, bekommt pro
// Run genau EINEN neuen Eintrag statt einen pro Schleifendurchlauf.
this.setNested(key, value);
// Zusätzlich flach ablegen — Konfigurationen, die den Key mit Punkt als
// ganzes lesen, funktionieren damit unverändert weiter.
this.storedData[key] = value;

this.logger.log(`💾 Stored: ${key} = ${value}`);
Expand Down Expand Up @@ -68,4 +83,32 @@ export class StoreDataAction extends BaseAction<StoreDataParams> {
this.logger.error(`❌ Failed to persist data: ${error.message}`);
}
}

/**
* Legt einen Punkt-Key als verschachteltes Objekt ab — spiegelbildlich zu
* convertJobDataToNestedObject() in ScrapeDataService, das die Daten beim
* Laden aus der DB genauso aufbaut.
*
* "a.b.c" → storedData.a.b.c
*
* Ein bestehender Nicht-Objekt-Wert auf dem Weg wird ersetzt, sonst würde
* ein früher gespeichertes "a" das Anlegen von "a.b" verhindern.
*/
private setNested(key: string, value: unknown): void {
const teile = key.split('.');
if (teile.length === 1) {
this.storedData[key] = value;
return;
}

let ziel: Record<string, any> = this.storedData;
for (let i = 0; i < teile.length - 1; i++) {
const t = teile[i];
if (typeof ziel[t] !== 'object' || ziel[t] === null) {
ziel[t] = {};
}
ziel = ziel[t];
}
ziel[teile[teile.length - 1]] = value;
}
}