Initialen Implementierungsplan hinzugefügt
This commit is contained in:
266
CLAUDE.md
Normal file
266
CLAUDE.md
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
# CLAUDE.md — Oracle Database
|
||||||
|
|
||||||
|
## Projektbeschreibung
|
||||||
|
|
||||||
|
Projekt für verschiedene Oracle-Datenbank Entwicklungen.
|
||||||
|
Enthält Tabellendefinitionen, PL/SQL-Packages, ORDS-Endpunkte, Views, Trigger etc.
|
||||||
|
|
||||||
|
## SQL / PL/SQL Guidelines
|
||||||
|
|
||||||
|
### Formatierung
|
||||||
|
|
||||||
|
**Keywords immer klein:**
|
||||||
|
```sql
|
||||||
|
select, from, where, insert into, create table, begin, end, exception, ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tab-Ausrichtung in mehrzeiligen Statements:**
|
||||||
|
|
||||||
|
Schlüsselwort + Tab, dann Inhalt. Erste Spalte und alle nachfolgenden Kommas stehen auf
|
||||||
|
demselben Tab-Stop (eine Fluchtlinie). Komma direkt vor dem Spaltennamen ohne Leerzeichen.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
select order_id
|
||||||
|
,principal_order_number
|
||||||
|
,total_amount
|
||||||
|
from fgt_order
|
||||||
|
where status = 'COMPLETE'
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
insert into fgt_order (
|
||||||
|
order_id
|
||||||
|
,principal_order_number
|
||||||
|
,total_amount
|
||||||
|
) values (
|
||||||
|
fgt_order_seq.nextval
|
||||||
|
,p_order_number
|
||||||
|
,p_total_amount
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Block-Keywords in eigener Zeile (lineup bracing):**
|
||||||
|
|
||||||
|
`loop`, `then`, `exception` und ihre schließenden `end`-Varianten stehen auf gleicher
|
||||||
|
Einrückungsebene wie der Block-Öffner:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
for rec in cur_open_orders
|
||||||
|
loop
|
||||||
|
l_count := l_count + 1;
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
if l_count > 0
|
||||||
|
then
|
||||||
|
do_something();
|
||||||
|
elsif l_count < 0
|
||||||
|
then
|
||||||
|
handle_negative();
|
||||||
|
end if;
|
||||||
|
|
||||||
|
begin
|
||||||
|
l_result := do_something();
|
||||||
|
exception
|
||||||
|
when e_not_found
|
||||||
|
then
|
||||||
|
l_result := null;
|
||||||
|
when others
|
||||||
|
then
|
||||||
|
raise;
|
||||||
|
end;
|
||||||
|
```
|
||||||
|
|
||||||
|
**`end`-Anweisungen immer mit Label:**
|
||||||
|
```sql
|
||||||
|
end my_procedure;
|
||||||
|
end my_package;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Indentation:** 4 Spaces innerhalb von Blöcken.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Benennungskonventionen
|
||||||
|
|
||||||
|
| Objekt | Konvention | Beispiel |
|
||||||
|
|---|---|---|
|
||||||
|
| Tabellen | Modulabhängiger Präfix, z.B.`adr_`, **Singular**, Dateiendung `.tab` | `sy_parameter`, `adr_adresse_zuweisung`, |
|
||||||
|
| Views | `v_` Präfix | `v_adr_adresse_zuweisung` |
|
||||||
|
| Packages | `pck_` Präfix, Separate header und body Dateien mit den Endungen `.pkh` und `.pkb` | `pck_system` |
|
||||||
|
| Prozeduren | `p_`-Präfix | `p_insert_order`, `p_delete_folder` |
|
||||||
|
| Funktionen | `f_`-Präfix | `f_get_order`, `f_build_url` |
|
||||||
|
| Lokale Variablen | `l_`-Präfix | `l_order_id`, `l_count` |
|
||||||
|
| Parameter (Input) | `i_`-Präfix | `i_json`, `i_order_id` |
|
||||||
|
| Package-globale Variablen | `g_`-Präfix | `g_default_currency` |
|
||||||
|
| Konstanten | `c_`-Präfix | `c_max_retries` |
|
||||||
|
| Cursor | `cur_`-Präfix | `cur_open_orders` |
|
||||||
|
| Exceptions | `e_`-Präfix | `e_not_found`, `e_duplicate` |
|
||||||
|
| Views | `_v`-Suffix | `fgt_order_v` |
|
||||||
|
| Trigger | `trg_`-Präfix | `trg_fgt_order_created_at` |
|
||||||
|
| Sequences | `<zweck>_seq` | `fgt_order_id_seq` |
|
||||||
|
| Primary Key (Spalte) | `<tabellenname>_id` | `order_id`, `shipment_id` |
|
||||||
|
| Foreign Key (Constraint) | `<beschreibung>_fk` | `fgt_order_bill_addr_fk` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### PL/SQL-Struktur
|
||||||
|
|
||||||
|
- `as` für Package-Spec/Body-Header, `is` für Prozedur-/Funktionskörper innerhalb eines Packages
|
||||||
|
- Implizite Cursor-FOR-Loops bevorzugen gegenüber expliziten Cursors
|
||||||
|
- `is null` / `is not null` verwenden — niemals `= null` / `!= null`
|
||||||
|
- Explizite Formatmasken in `to_date`, `to_char`, `to_timestamp`
|
||||||
|
- `commit` nur im äußersten Aufrufer — nie in Sub-Prozeduren
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create or replace package body pck_orders as
|
||||||
|
|
||||||
|
procedure insert_order (i_json in clob, i_run_id in varchar2)
|
||||||
|
is
|
||||||
|
l_order_id number;
|
||||||
|
l_status varchar2(20);
|
||||||
|
begin
|
||||||
|
-- Implementierung
|
||||||
|
l_order_id := bet_order_id_seq.nextval;
|
||||||
|
end insert_order;
|
||||||
|
|
||||||
|
end fgt_orders;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Exception Handling
|
||||||
|
|
||||||
|
- **Niemals** `when others then null` — Exceptions nie stillschweigend schlucken
|
||||||
|
- Exceptions mit `e_`-Präfix benennen und im Deklarationsteil definieren
|
||||||
|
- `sqlerrm` und `sqlcode` für Fehlerinformationen nutzen
|
||||||
|
|
||||||
|
```sql
|
||||||
|
declare
|
||||||
|
e_invalid_status exception;
|
||||||
|
l_status varchar2(20);
|
||||||
|
begin
|
||||||
|
if l_status not in ('COMPLETE', 'INCOMPLETE')
|
||||||
|
then
|
||||||
|
raise e_invalid_status;
|
||||||
|
end if;
|
||||||
|
exception
|
||||||
|
when e_invalid_status
|
||||||
|
then
|
||||||
|
-- gezielt behandeln
|
||||||
|
log_error('Ungültiger Status: ' || l_status);
|
||||||
|
when others
|
||||||
|
then
|
||||||
|
-- sqlcode / sqlerrm immer loggen, nie schlucken
|
||||||
|
log_error(sqlerrm);
|
||||||
|
raise;
|
||||||
|
end;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Audit-Spalten
|
||||||
|
|
||||||
|
Jede Tabelle bekommt am Ende vier Audit-Spalten:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create table bet_order (
|
||||||
|
bet_order_id number generated by default as identity not null enable
|
||||||
|
, ...
|
||||||
|
,bet_mit_id_angelegt NUMBER(10) not null,
|
||||||
|
,bet_datum_angelegt DATE not null,
|
||||||
|
,bet_mit_id_geaendert NUMBER(10) not null,
|
||||||
|
,bet_datum_geaendert DATE not null,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Befüllt werden sie über einen `before insert or update`-Trigger pro Tabelle.
|
||||||
|
Namenskonvention: `trg_<tabellenname>_audit`.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create or replace editionable trigger trg_fgt_order_audit
|
||||||
|
before
|
||||||
|
insert or update on fgt_order
|
||||||
|
referencing
|
||||||
|
new as new
|
||||||
|
old as old
|
||||||
|
for each row
|
||||||
|
declare
|
||||||
|
l_user varchar2(100 byte) := substr(upper(coalesce(
|
||||||
|
sys_context('apex$session', 'app_user'),
|
||||||
|
sys_context('userenv', 'os_user'),
|
||||||
|
sys_context('userenv', 'session_user'))), 1, 100);
|
||||||
|
l_now date := cast(current_timestamp at time zone 'Europe/Berlin' as date);
|
||||||
|
begin
|
||||||
|
if inserting
|
||||||
|
then
|
||||||
|
:new.created_at := l_now;
|
||||||
|
:new.created_by := l_user;
|
||||||
|
end if;
|
||||||
|
:new.changed_at := l_now;
|
||||||
|
:new.changed_by := l_user;
|
||||||
|
end trg_fgt_order_audit;
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Benutzername wird bevorzugt aus der APEX-Session gelesen, mit Fallback auf OS- bzw. DB-Session-User.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tabellenänderungen (Migrations-Skripte)
|
||||||
|
|
||||||
|
Tabellen können von SQLcl Projects nicht gedropt und neu erstellt werden, wenn sie Daten enthalten.
|
||||||
|
Änderungen an bestehenden Tabellen werden deshalb als `alter table`-Statements **am Ende der
|
||||||
|
jeweiligen Tabellendatei** ergänzt — nach dem `create table` und dem Primary-Key-Constraint,
|
||||||
|
aber **vor** dem `-- sqlcl_snapshot`-Kommentar.
|
||||||
|
|
||||||
|
So kann die gesamte Datei als Skript auf eine bestehende Datenbank angewendet werden:
|
||||||
|
Das `create table` schlägt fehl (Tabelle existiert schon), aber die `alter table`-Statements
|
||||||
|
laufen durch und bringen die Tabelle auf den aktuellen Stand.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
create table fgt_order ( ... );
|
||||||
|
|
||||||
|
alter table fgt_order
|
||||||
|
add constraint pk_fgt_order primary key (order_id)
|
||||||
|
using index enable;
|
||||||
|
|
||||||
|
-- Migrationsscript — hinzugefügt am 2026-03-30
|
||||||
|
alter table fgt_order add (
|
||||||
|
status varchar2(16 char) default 'COMPLETE' not null
|
||||||
|
,run_id varchar2(36 char)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- sqlcl_snapshot { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Planungsdokumente
|
||||||
|
|
||||||
|
Für komplexere Features/Packages wird ein Planungsdokument als `.md`-Datei im Projektverzeichnis
|
||||||
|
abgelegt (z.B. `plan_pck_net_storage.md`). Der Plan wird während der Umsetzung aktuell gehalten —
|
||||||
|
jede Änderung an Architektur oder Schnittstelle wird sofort im Plan nachgeführt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Kommentare
|
||||||
|
|
||||||
|
- Beschreibungen auf **Deutsch**, Code (außer Fachbegriffe der Anwendungsdomäne) auf **Englisch**
|
||||||
|
- Block-Kommentar zu Beginn Prozedur/Funktion mit Zweck und Parametern
|
||||||
|
- Inline-Kommentare mit `--` nur für nicht-offensichtliche Logik
|
||||||
|
|
||||||
|
```sql
|
||||||
|
procedure insert_order (i_json in clob, i_run_id in varchar2)
|
||||||
|
/*Kopf------------------------------------------------------------------------------------------------
|
||||||
|
-- Beschreibung: Fügt einen verarbeiteten Auftrag in die Datenbank ein.
|
||||||
|
------------------------------------------------------------------------------------------------------
|
||||||
|
-- Parameter: i_json Klippa-Ergebnis als JSON-String
|
||||||
|
-- i_run_id Ausführungs-Korrelations-ID des Verarbeitungslaufs
|
||||||
|
------------------------------------------------------------------------------------------------------
|
||||||
|
-- Rückgabe: <nur bei funktionen, nicht bei procedures>
|
||||||
|
------------------------------------------------------------------------------------------------------
|
||||||
|
-- MA Datum Änderung
|
||||||
|
-- SCK 2026-04-07 Funktion erstellt
|
||||||
|
------------------------------------------------------------------------------------------------Kopf*/
|
||||||
|
is
|
||||||
|
...
|
||||||
|
```
|
||||||
287
plan_pck_net_storage.md
Normal file
287
plan_pck_net_storage.md
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
# Plan: PCK_NET_STORAGE
|
||||||
|
|
||||||
|
**Stand:** 2026-04-07
|
||||||
|
**Status:** Planung abgeschlossen — bereit zur Implementierung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
PL/SQL Package `PCK_NET_STORAGE` kapselt alle Zugriffe auf OCI Object Storage über REST API.
|
||||||
|
Wird in Oracle APEX für einen Dateimanager genutzt (Browse, Upload, Download, Rename, Move, Delete).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
```
|
||||||
|
tables/
|
||||||
|
lg_app_log.tab
|
||||||
|
|
||||||
|
packages/
|
||||||
|
pck_log.pkh
|
||||||
|
pck_log.pkb
|
||||||
|
pck_net_storage.pkh
|
||||||
|
pck_net_storage.pkb
|
||||||
|
```
|
||||||
|
|
||||||
|
Keine Schema-Level Types.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
|
||||||
|
**APEX Web Credentials** (Typ: Oracle Cloud Infrastructure)
|
||||||
|
Static ID: `OCI_OBJ_STORE_CRED`
|
||||||
|
Wird in APEX Admin UI einmalig angelegt mit: OCI User OCID, Tenancy OCID, Fingerprint, Private Key (PEM).
|
||||||
|
In PL/SQL referenziert via `p_credential_static_id => c_credential_id` in `apex_web_service.make_rest_request`.
|
||||||
|
|
||||||
|
Grund: DBMS_CLOUD ist auf dieser DB nicht installiert. APEX übernimmt OCI HTTP Signature V1 (SHA-256) automatisch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Konfigurationsparameter
|
||||||
|
|
||||||
|
Alle zur Laufzeit via `pck_system.f_get_par_wert_by_programmid`:
|
||||||
|
|
||||||
|
| Parameter-ID | Inhalt |
|
||||||
|
|---|---|
|
||||||
|
| `NET_STORAGE_BUCKET` | Bucket-Name |
|
||||||
|
| `NET_STORAGE_NAMESPACE` | OCI Object Storage Namespace |
|
||||||
|
| `NET_STORAGE_REGION` | z.B. `eu-frankfurt-1` |
|
||||||
|
| `NET_STORAGE_TENANT_ID` | Erlaubter Root-Prefix, z.B. `mandant_42/` |
|
||||||
|
| `NET_STORAGE_APEX_CREDENTIAL_ID` | Credential Static ID für apex_web_service calls, z.B. 'OCI_OBJ_STORE_CRED' |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Öffentliche Schnittstelle (Package Spec)
|
||||||
|
|
||||||
|
### Record Type `t_object_meta`
|
||||||
|
|
||||||
|
| Feld | Typ |
|
||||||
|
|---|---|
|
||||||
|
| `object_name` | `varchar2(1024)` |
|
||||||
|
| `object_size` | `number` |
|
||||||
|
| `last_modified` | `date` |
|
||||||
|
| `content_type` | `varchar2(256)` |
|
||||||
|
| `etag` | `varchar2(256)` |
|
||||||
|
|
||||||
|
### Öffentliche Prozeduren und Funktionen
|
||||||
|
|
||||||
|
Namenskonvention: Prozeduren `p_`-Präfix, Funktionen `f_`-Präfix. Parameter immer `i_`-Präfix.
|
||||||
|
|
||||||
|
| Name | Typ | Signatur | Rückgabe | Anmerkung |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `f_list_objects` | Funktion | `(i_prefix, i_delimiter, i_start_with, i_limit)` | `sys_refcursor` | Cursor-Spalten: `object_name, object_size, last_modified, is_folder(Y/N), etag` |
|
||||||
|
| `f_download_object` | Funktion | `(i_object_key)` | `blob` | |
|
||||||
|
| `p_upload_object` | Prozedur | `(i_object_key, i_content blob, i_content_type)` | — | |
|
||||||
|
| `p_delete_object` | Prozedur | `(i_object_key)` | — | |
|
||||||
|
| `p_delete_folder` | Prozedur | `(i_prefix)` | — | intern: f_list_objects-Loop + p_delete_object |
|
||||||
|
| `p_rename_object` | Prozedur | `(i_object_key, i_new_name)` | — | intern: OCI RenameObject API |
|
||||||
|
| `p_move_object` | Prozedur | `(i_object_key, i_target_prefix)` | — | intern: OCI RenameObject API |
|
||||||
|
| `p_create_folder` | Prozedur | `(i_prefix, i_folder_name)` | — | PUT leeres Objekt mit trailing `/` |
|
||||||
|
| `f_get_object_metadata` | Funktion | `(i_object_key)` | `t_object_meta` | HEAD Request |
|
||||||
|
|
||||||
|
Rechteprüfungen:
|
||||||
|
|
||||||
|
Zu beginn von delete-operationen muss folgender Aufruf durchgeführt werden, um die Rechte des nutzers zu prüfen: `pck_mitarbeiterrecht.p_hat_recht('ADMIN');`
|
||||||
|
|
||||||
|
Bei anderen schreib-Operationen, wie rename oder move muss die Prüfung von folgendem Recht erfolgen:
|
||||||
|
`pck_mitarbeiterrecht.p_hat_recht('SCHREIBEN_ALLES');`
|
||||||
|
|
||||||
|
Bei lese-Operationen wie `f_get_object_metadata` und `f_download_object` muss folgendes Recht geprüft werden:
|
||||||
|
|
||||||
|
`pck_mitarbeiterrecht.p_hat_recht('LESEN_ALLES');`
|
||||||
|
|
||||||
|
Schlägt eine Prüfung fehl, wirft die p_hat_recht Prozedur einen speziellen Application Error mit Fehlermeldung für den Benutzer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Private Helpers (nur Body)
|
||||||
|
|
||||||
|
| Name | Typ | Zweck |
|
||||||
|
|---|---|---|
|
||||||
|
| `f_get_cfg` | Funktion | Wrapper für `pck_system.f_get_par_wert_by_programmid` |
|
||||||
|
| `f_build_url` | Funktion | Baut vollständige OCI REST URL (`/n/{ns}/b/{bucket}/o/{key}`) |
|
||||||
|
| `p_assert_allowed` | Prozedur | Tenant-Prefix-Guard + Path-Traversal-Prüfung |
|
||||||
|
| `f_make_request` | Funktion | Wrapper für `apex_web_service.make_rest_request`, wertet HTTP-Status aus, gibt Response-Body zurück |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pagination (`f_list_objects`)
|
||||||
|
|
||||||
|
OCI liefert max. 1000 Objekte pro Request. `f_list_objects` ruft OCI intern in einer Schleife auf
|
||||||
|
(`nextStartWith`-Token aus JSON-Response) bis alle Seiten geladen sind. Ergebnis wird in einer
|
||||||
|
lokalen Collection gesammelt, am Ende als `sys_refcursor` geöffnet.
|
||||||
|
`i_limit = 0` → unbegrenzt, `i_limit > 0` → Abbruch wenn Limit erreicht.
|
||||||
|
|
||||||
|
OCI-Response trennt Unterordner (`prefixes`-Array) von Dateien (`objects`-Array) — beide werden
|
||||||
|
eingesammelt, Ordner mit `is_folder = 'Y'`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
|
||||||
|
`f_make_request` wirft bei HTTP-Fehler:
|
||||||
|
|
||||||
|
| HTTP-Status | Fehlercode | Meldung |
|
||||||
|
|---|---|---|
|
||||||
|
| 404 | -20001 | `Object not found: {key}` |
|
||||||
|
| 401 / 403 | -20002 | `OCI authentication failed` |
|
||||||
|
| 409 | -20007 | `Object already exists: {key}` |
|
||||||
|
| sonstige 4xx/5xx | -20006 | `OCI API error {status}: {response body}` |
|
||||||
|
|
||||||
|
`p_assert_allowed` wirft:
|
||||||
|
|
||||||
|
| Fall | Fehlercode | Meldung |
|
||||||
|
|---|---|---|
|
||||||
|
| `..` im Pfad | -20004 | `Path traversal attempt detected` |
|
||||||
|
| Pfad außerhalb Tenant-Prefix | -20005 | `Access denied: outside tenant scope` |
|
||||||
|
|
||||||
|
Bucket nicht erreichbar → -20003 via `f_make_request`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sicherheit / Tenant-Isolation
|
||||||
|
|
||||||
|
`p_assert_allowed` wird am Anfang jeder öffentlichen Funktion/Prozedur aufgerufen:
|
||||||
|
1. Tenant-Prefix laden via `f_get_cfg('NET_STORAGE_TENANT_ID')`
|
||||||
|
2. `..`-Sequenzen im übergebenen Key prüfen → -20004
|
||||||
|
3. Prüfen ob Key mit Tenant-Prefix beginnt → -20005
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dateieingang-Verarbeitung
|
||||||
|
|
||||||
|
### OCI-Struktur
|
||||||
|
|
||||||
|
n8n legt Dateien in einem Unterordner ab, der nach der ZIP-Datei benannt ist.
|
||||||
|
Unterordner innerhalb der ZIP werden beibehalten:
|
||||||
|
|
||||||
|
```
|
||||||
|
eingang/<zip-name>/datei1.csv
|
||||||
|
eingang/<zip-name>/unterordner/datei3.csv
|
||||||
|
eingang/<zip-name>/_READY_FOR_DB_PROCESSING_ ← Marker
|
||||||
|
```
|
||||||
|
|
||||||
|
### Marker-Datei
|
||||||
|
|
||||||
|
n8n legt nach erfolgreichem Upload aller Dateien eine leere Marker-Datei ab:
|
||||||
|
`eingang/<zip-name>/_READY_FOR_DB_PROCESSING_`
|
||||||
|
|
||||||
|
Die DB verarbeitet einen Unterordner **ausschließlich wenn der Marker vorhanden ist.**
|
||||||
|
Der Marker wird erst gelöscht wenn **alle** Dateien des Unterordners erfolgreich
|
||||||
|
verarbeitet wurden — so werden fehlgeschlagene Dateien beim nächsten Lauf erneut versucht.
|
||||||
|
|
||||||
|
### Prozedur `p_process_incoming_files` (in separatem Fachpackage, nicht in pck_net_storage)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Unterordner von eingang/ auflisten (f_list_objects mit delimiter='/')
|
||||||
|
2. Für jeden Unterordner:
|
||||||
|
a. Marker eingang/<zip-name>/_READY_FOR_DB_PROCESSING_ vorhanden?
|
||||||
|
→ nein: überspringen (unvollständiger Upload)
|
||||||
|
b. Alle Dateien in eingang/<zip-name>/ auflisten (ohne Marker)
|
||||||
|
c. Für jede Datei einzeln:
|
||||||
|
begin
|
||||||
|
Fachliche Verarbeitung (Import) — noch kein Commit
|
||||||
|
log_object_ref := 'eingang/<zip-name>/datei.csv' ← ZIP aus Pfad ableitbar
|
||||||
|
pck_net_storage.p_move_object(datei → Zielordner/<zip-name>/)
|
||||||
|
commit
|
||||||
|
pck_log.p_info(...)
|
||||||
|
exception when others then
|
||||||
|
rollback
|
||||||
|
pck_log.p_error(i_object_ref => 'eingang/<zip-name>/datei.csv', ...)
|
||||||
|
end
|
||||||
|
d. Noch Dateien in eingang/<zip-name>/ (außer Marker)?
|
||||||
|
→ nein: pck_net_storage.p_delete_object(Marker) — Batch abgeschlossen
|
||||||
|
→ ja: Marker bleibt — nächster Lauf versucht verbleibende Dateien
|
||||||
|
```
|
||||||
|
|
||||||
|
Rollback nach fehlgeschlagenem `p_move_object` macht auch den Import rückgängig.
|
||||||
|
Kein Verhalten wird aus `lg_app_log` abgeleitet — es dient ausschließlich dem Audit.
|
||||||
|
|
||||||
|
### ORDS-Endpunkt
|
||||||
|
|
||||||
|
`POST /ords/.../net_storage/process_incoming` → ruft `p_process_incoming_files` auf.
|
||||||
|
Wird von n8n nach erfolgreichem Upload aufgerufen.
|
||||||
|
Schlägt der Aufruf fehl: APEX Automation greift beim nächsten Stundenlauf.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## APEX REST Data Source (manuell einrichten)
|
||||||
|
|
||||||
|
Für das APEX Interactive Report im Dateimanager-Browser:
|
||||||
|
|
||||||
|
```
|
||||||
|
Name: NET_STORAGE_LIST_OBJECTS
|
||||||
|
Base-URL: https://objectstorage.{region}.oraclecloud.com
|
||||||
|
Endpoint: /n/{namespace}/b/{bucket}/o
|
||||||
|
Method: GET
|
||||||
|
Web Credential: OCI_OBJ_STORE_CRED
|
||||||
|
|
||||||
|
URL-Parameter:
|
||||||
|
prefix → aktueller Ordner-Prefix (Bind-Variable aus APEX)
|
||||||
|
delimiter → /
|
||||||
|
limit → 1000
|
||||||
|
start → für Pagination (optional)
|
||||||
|
|
||||||
|
Spalten-Mapping:
|
||||||
|
objects[*].name → OBJECT_NAME
|
||||||
|
objects[*].size → OBJECT_SIZE
|
||||||
|
objects[*].timeModified → LAST_MODIFIED
|
||||||
|
objects[*].etag → ETAG
|
||||||
|
```
|
||||||
|
|
||||||
|
Hinweis: `prefixes` (Unterordner) und `objects` (Dateien) sind zwei separate JSON-Arrays.
|
||||||
|
Für ein gemeinsames IR entweder zwei REST Sources mit UNION, oder `f_list_objects` über
|
||||||
|
eine APEX Collection als IR-Quelle nutzen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging: lg_app_log + pck_log
|
||||||
|
|
||||||
|
### Tabelle `lg_app_log`
|
||||||
|
|
||||||
|
Allgemeine Anwendungs-Log-Tabelle, modulübergreifend nutzbar. Keine Indizes (Log-Volumen
|
||||||
|
ist gering, Ad-hoc-Suche reicht ohne Index).
|
||||||
|
|
||||||
|
| Spalte | Typ | Pflicht | Beschreibung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `lg_app_log_id` | `number generated by default as identity` | ja | PK |
|
||||||
|
| `log_timestamp` | `timestamp` | ja | Zeitpunkt des Log-Eintrags |
|
||||||
|
| `log_level` | `varchar2(10 char)` | ja | `INFO`, `WARN`, `ERROR` |
|
||||||
|
| `log_module` | `varchar2(100 char)` | ja | z.B. `PCK_NET_STORAGE` |
|
||||||
|
| `log_action` | `varchar2(100 char)` | nein | z.B. `PROCESS_FILE` |
|
||||||
|
| `log_object_ref` | `varchar2(512 char)` | nein | z.B. `eingang/datei.csv` |
|
||||||
|
| `log_message` | `varchar2(4000 char)` | nein | Kurzmeldung |
|
||||||
|
| `log_detail` | `clob` | nein | Stack Trace, JSON, etc. |
|
||||||
|
| `log_user` | `varchar2(100 char)` | nein | APEX- oder DB-User |
|
||||||
|
| `log_session_id` | `number` | nein | Oracle Session-ID |
|
||||||
|
|
||||||
|
Keine Audit-Spalten (Log-Tabelle ist self-auditing durch `log_timestamp` + `log_user`).
|
||||||
|
|
||||||
|
### Package `pck_log`
|
||||||
|
|
||||||
|
Öffentliche Prozeduren:
|
||||||
|
|
||||||
|
| Name | Signatur |
|
||||||
|
|---|---|
|
||||||
|
| `p_info` | `(i_module, i_action, i_message, i_object_ref default null)` |
|
||||||
|
| `p_warn` | `(i_module, i_action, i_message, i_object_ref default null)` |
|
||||||
|
| `p_error` | `(i_module, i_action, i_message, i_detail default null, i_object_ref default null)` |
|
||||||
|
|
||||||
|
Intern immer `PRAGMA AUTONOMOUS_TRANSACTION` + `COMMIT` — Log-Einträge sind
|
||||||
|
transaktionsunabhängig und überleben Rollbacks der Haupttransaktion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OCI API Endpunkte (Referenz)
|
||||||
|
|
||||||
|
| Operation | Method | Pfad |
|
||||||
|
|---|---|---|
|
||||||
|
| List Objects | GET | `/n/{ns}/b/{bucket}/o?prefix=...&delimiter=/` |
|
||||||
|
| Get Object | GET | `/n/{ns}/b/{bucket}/o/{objectName}` |
|
||||||
|
| Put Object | PUT | `/n/{ns}/b/{bucket}/o/{objectName}` |
|
||||||
|
| Delete Object | DELETE | `/n/{ns}/b/{bucket}/o/{objectName}` |
|
||||||
|
| Head Object | HEAD | `/n/{ns}/b/{bucket}/o/{objectName}` |
|
||||||
|
| Rename Object | POST | `/n/{ns}/b/{bucket}/actions/renameObject` |
|
||||||
131
workflow_dateieingang.md
Normal file
131
workflow_dateieingang.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Workflow: Automatischer Dateieingang via n8n → OCI Object Storage → DB
|
||||||
|
|
||||||
|
**Stand:** 2026-04-07
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Beteiligte Systeme
|
||||||
|
|
||||||
|
| System | Rolle |
|
||||||
|
|---|---|
|
||||||
|
| **SFTP-Server** | Quelle — externer Lieferant legt ZIP-Dateien ab |
|
||||||
|
| **n8n** | Middleware — holt ZIP, entpackt, lädt Dateien + Marker in OCI hoch |
|
||||||
|
| **OCI Object Storage** | Zwischenspeicher — Eingangsordner, Zielordner nach Verarbeitung |
|
||||||
|
| **Oracle DB / APEX** | Verarbeitung — liest Dateien aus OCI, importiert Daten |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ APEX Automation (stündlich) │
|
||||||
|
│ │
|
||||||
|
│ 1. Unterordner in eingang/ mit Marker vorhanden? │
|
||||||
|
│ → ja: Dateien verarbeiten (gleiche Logik wie Schritt 4-6) │
|
||||||
|
│ │
|
||||||
|
│ 2. n8n Webhook auslösen (fire & forget) │
|
||||||
|
└────────────────────────────┬────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ n8n Workflow │
|
||||||
|
│ │
|
||||||
|
│ 3a. ZIP-Datei vom SFTP-Server herunterladen │
|
||||||
|
│ 3b. ZIP entpacken │
|
||||||
|
│ 3c. Alle Dateien in OCI eingang/<zip-name>/ hochladen │
|
||||||
|
│ (Unterordner aus der ZIP werden beibehalten) │
|
||||||
|
│ → "Continue on Error" AUS: ein Fehler stoppt alles │
|
||||||
|
│ 3d. Marker eingang/<zip-name>/_READY_FOR_DB_PROCESSING_ │
|
||||||
|
│ hochladen │
|
||||||
|
│ 3e. ZIP auf SFTP umbenennen / verschieben │
|
||||||
|
│ → erst NACH erfolgreichem Marker-Upload │
|
||||||
|
│ 3f. ORDS-Endpunkt aufrufen │
|
||||||
|
└────────────────────────────┬────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Oracle DB (via ORDS-Endpunkt oder APEX Automation) │
|
||||||
|
│ │
|
||||||
|
│ 4. Unterordner in eingang/ auflisten │
|
||||||
|
│ 5. Für jeden Unterordner mit Marker: │
|
||||||
|
│ Für jede Datei (außer Marker) einzeln: │
|
||||||
|
│ a. Daten importieren (noch kein Commit) │
|
||||||
|
│ → log_object_ref = eingang/<zip-name>/datei.csv │
|
||||||
|
│ b. Datei in Zielordner verschieben │
|
||||||
|
│ c. Commit │
|
||||||
|
│ d. Fehler → Rollback, ERROR in lg_app_log, nächste Datei │
|
||||||
|
│ 6. Keine Dateien mehr im Unterordner (außer Marker)? │
|
||||||
|
│ → Marker löschen (Batch abgeschlossen) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OCI Ordnerstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
bucket/
|
||||||
|
eingang/
|
||||||
|
export_2026-04-07/ ← Unterordner = ZIP-Name
|
||||||
|
datei1.csv
|
||||||
|
datei2.csv
|
||||||
|
unterordner/
|
||||||
|
datei3.csv
|
||||||
|
_READY_FOR_DB_PROCESSING_ ← Marker: Batch vollständig
|
||||||
|
<zielordner>/
|
||||||
|
export_2026-04-07/ ← gleiche Struktur nach Verarbeitung
|
||||||
|
datei1.csv
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Marker bleibt solange erhalten bis **alle** Dateien des Unterordners
|
||||||
|
erfolgreich verarbeitet wurden. So werden fehlgeschlagene Dateien beim
|
||||||
|
nächsten Lauf erneut aufgegriffen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fehlerfall-Verhalten
|
||||||
|
|
||||||
|
**n8n: Upload einer Datei schlägt fehl**
|
||||||
|
- Workflow stoppt sofort ("Continue on Error" ist aus)
|
||||||
|
- Kein Marker wird geschrieben, ZIP bleibt unverändert auf SFTP
|
||||||
|
- ORDS wird nicht aufgerufen
|
||||||
|
- Beim nächsten Stundenlauf lädt n8n die ZIP erneut herunter
|
||||||
|
- Bereits hochgeladene Dateien werden überschrieben (OCI PUT idempotent)
|
||||||
|
|
||||||
|
**n8n: ORDS-Aufruf schlägt fehl**
|
||||||
|
- Marker liegt in `eingang/<zip-name>/`, Dateien sind vollständig hochgeladen
|
||||||
|
- Beim nächsten Stundenlauf findet APEX Automation den Marker und verarbeitet
|
||||||
|
|
||||||
|
**DB: Verarbeitung einer einzelnen Datei schlägt fehl**
|
||||||
|
- Rollback — Datei bleibt in `eingang/<zip-name>/`
|
||||||
|
- ERROR in `lg_app_log` mit `log_object_ref = eingang/<zip-name>/datei.csv`
|
||||||
|
- Marker bleibt erhalten → nächste Dateien im Batch werden weiterverarbeitet
|
||||||
|
- Beim nächsten Lauf wird die fehlgeschlagene Datei erneut versucht
|
||||||
|
|
||||||
|
**DB: p_move_object schlägt nach erfolgreichem Import fehl**
|
||||||
|
- Rollback des Imports → sauberer Ausgangszustand
|
||||||
|
- Datei bleibt in `eingang/<zip-name>/`, Marker bleibt erhalten
|
||||||
|
- Nächster Lauf versucht es erneut
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Warum kein Fehlerordner, keine Status-Tabelle?
|
||||||
|
|
||||||
|
Der Zustand steckt im Dateisystem:
|
||||||
|
- Unterordner mit Marker = Batch bereit oder teilweise verarbeitet
|
||||||
|
- Unterordner ohne Marker = unvollständiger n8n-Upload, wird ignoriert
|
||||||
|
- Datei im Zielordner = erfolgreich verarbeitet
|
||||||
|
- Datei noch in `eingang/<zip-name>/` = noch ausstehend oder fehlgeschlagen
|
||||||
|
|
||||||
|
Fehlerdetails stehen in `lg_app_log`. Über `log_object_ref` ist jede Datei
|
||||||
|
eindeutig einer ZIP zugeordnet. Kein Verhalten wird aus dem Log abgeleitet —
|
||||||
|
es dient ausschließlich dem Audit-Trail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zeitplan
|
||||||
|
|
||||||
|
APEX Automation läuft **1x pro Stunde**. n8n-Workflow wird dabei neu angestoßen —
|
||||||
|
läuft also ebenfalls stündlich, zeitlich versetzt nach dem Automation-Start.
|
||||||
Reference in New Issue
Block a user