Kompletny, sprawdzony przepis na aplikację wg wzorca “pobieraj z zewnętrznego API (ręcznie
i cyklicznie), zapisuj każde pobranie do bazy, pokazuj historię w apce”. Składniki: custom
connector (Open-Meteo), baza Noe, konektor zapisu, jeden custom flow wspólny dla crona
i przycisku w apce, cykliczna akcja konta (cron) i aplikacja Noe (Svelte) z historią.
Każdy rekord ma źródło: manual (kto i z jakiego IP kliknął) albo cron.
Autoryzacja: Authorization: Bearer TOKEN
Content-Type: application/json; charset=utf-8
Krok 1 - custom connector do zewnętrznego API
POST /connect/connectors.json
{
"connector": {
"name": "Open-Meteo API (example1)",
"code": "open-meteo-example1",
"kind": "noe/custom_connector",
"url": "https://api.open-meteo.com",
"active": true,
"methods": [
{
"method": "get_weather",
"object_in": "json_object",
"object_out": "json_object",
"http_method": "GET",
"path": "/v1/forecast?latitude={latitude}&longitude={longitude}¤t_weather=true",
"description": "Pobiera aktualną pogodę dla współrzędnych"
}
]
}
}
Pułapki:
-
methodsto top-level pole konektora (kolumna jsonb), NIEfields.methods - bazowy
urljest normalizowany do samego hosta - ścieżkę (np./v1) podawaj wpathmetody - klucz API zewnętrznego serwisu (jeśli wymagany) idzie w
secret_token(wysyłany jako Bearer)
Krok 2 - baza Noe na historię pobrań
POST /noe/dbs.json
{
"db": {
"name": "Pogoda - historia (example1)",
"code": "weather-example1",
"schema": {
"fields": {
"temperature": { "type": "number" },
"windspeed": { "type": "number" },
"alert": { "type": "string" },
"source": { "type": "string" },
"fetched_by": { "type": "string" },
"ip": { "type": "string" }
},
"indexes": { "idx_str_1": "source" }
},
"public_methods": { "list": true }
}
}
-
list: true- frontend czyta historię bez autoryzacji:POST /noe/db/weather-example1/list(rekordy{id, data, created_at}, najnowsze pierwsze; czas pobrania =created_at) - zapisy robi wyłącznie flow (niżej), więc publiczne
createnie jest wystawiane
Krok 3 - konektor zapisu do bazy (prywatne API + token)
Program flow nie ma wbudowanej akcji “zapisz do bazy Noe” - zapis to krok connector na endpoint
bazy. Utwórz token API (/account/api_tokens -> “Nowy”) i wstaw go w secret_token:
{
"connector": {
"name": "Zapis pogody do bazy Noe (example1)",
"code": "weather-db-example1",
"kind": "noe/custom_connector",
"url": "http://intum1.intum.test",
"secret_token": "TOKEN_API",
"active": true,
"methods": [
{ "method": "save_weather", "object_in": "json_object", "object_out": "json_object",
"http_method": "POST", "path": "/noe/dbs/weather-example1/records.json",
"description": "Zapisuje rekord pogody przez prywatne API rekordów (Bearer = secret_token)" }
]
}
}
url = adres konta (na produkcji domena konta, na dev np. http://intum1.intum.test).
Endpoint prywatnego API czyta z body record.data - nadmiarowe klucze obiektu ignoruje.
Krok 4 - JEDEN custom flow dla crona i przycisku
POST /connect/flows.json
{
"flow": {
"name": "Get Weather (example1)",
"code": "get-weather-example1",
"kind": "noe/custom_flow",
"active": true,
"fields": {
"env": { "latitude": "52.23", "longitude": "21.01" },
"program": [
{
"action": "mutation",
"in": "json_object",
"mutation": {
"json_object.latitude": "{env.latitude}",
"json_object.longitude": "{env.longitude}"
},
"out": "json_object"
},
{
"action": "connector",
"in": "json_object",
"kind": "noe/custom_connector",
"code": "open-meteo-example1",
"method": "get_weather",
"result_key": "weather",
"out": "json_object"
},
{
"action": "mutation",
"in": "json_object",
"mutation": {
"json_object.record.data.temperature": "{json_object.weather.current_weather.temperature}",
"json_object.record.data.windspeed": "{json_object.weather.current_weather.windspeed}"
},
"out": "json_object"
},
{
"in": "json_object",
"out": "json_object",
"if": {
"{json_object.record.data.temperature} > 30": {
"then": [
{ "action": "mutation", "in": "json_object",
"mutation": { "json_object.record.data.alert": "'true'" }, "out": "json_object" }
],
"else": [
{ "action": "mutation", "in": "json_object",
"mutation": { "json_object.record.data.alert": "'false'" }, "out": "json_object" }
]
}
}
},
{
"in": "json_object",
"out": "json_object",
"if": {
"{json_object.kind} == 'cron'": {
"then": [
{ "action": "mutation", "in": "json_object",
"mutation": {
"json_object.record.data.source": "'cron'",
"json_object.record.data.fetched_by": "'cron'",
"json_object.record.data.ip": "'-'"
}, "out": "json_object" }
],
"else": [
{ "action": "mutation", "in": "json_object",
"mutation": {
"json_object.record.data.source": "'manual'",
"json_object.record.data.fetched_by": "{json_object._user_name}",
"json_object.record.data.ip": "{json_object._ip}"
}, "out": "json_object" }
]
}
}
},
{
"action": "connector",
"in": "json_object",
"kind": "noe/custom_connector",
"code": "weather-db-example1",
"method": "save_weather",
"out": "json_object"
}
]
}
}
}
Kluczowe elementy:
-
result_key: "weather"na kroku Open-Meteo - bez tego odpowiedź API ZASTĄPIŁABY cały obiekt i kontekst wywołania (kind,_user_name,_ip) przepadłby przed krokiem rozpoznającym źródło -
skąd flow wie, kto go odpalił: cron przekazuje na wejściu
{ "kind": "cron", "cron_id": ... }, a akcja aplikacji Noe dokłada_action,_app_idi kontekst wywołującego:_user_name,_user_id,_ip- warunek{json_object.kind} == 'cron'rozróżnia źródła -
współrzędne w
fields.env- program czyta je jako{env.latitude}/{env.longitude}przy każdym uruchomieniu (cron i akcja); zmiana lokalizacji = edycjafields.envflow, bez dotykania programu (w apce robi to sekcja “Lokalizacja” PATCH-em na flow) - payload zapisu budowany pod zagnieżdżonym kluczem
record.data(mutation umie tworzyć zagnieżdżone ścieżki) - dokładnie w formacie prywatnego API rekordów - konektory muszą istnieć PRZED flow (walidacja programu sprawdza metody)
- test (dev):
POST /connect/flows/get-weather-example1/start.json?perform_now=1z body{}
Krok 5 - cykliczna akcja konta (cron)
POST /account/crons.json
{ "cron": { "kind": "flow", "target_code": "get-weather-example1", "schedule": "0 6 */3 * *", "name": "Pogoda - odświeżanie", "active": true } }
Ręczne odpalenie: POST /account/crons/:id/run.json albo “Uruchom teraz” w /account/crons.
Krok 6 - aplikacja Noe (Svelte) z historią pobrań
POST /noe/apps.json
{
"app": {
"name": "Pogoda (example1)",
"url_code": "pogoda-example1",
"description": "Przykładowa apka: custom connector + custom flow + cron + baza Noe z historią pobrań",
"app_engine": "svelte",
"css_engine": "tailwind_all",
"public": true,
"db_code": "weather-example1",
"actions": [
{ "action": "get_weather", "flow_code": "get-weather-example1", "description": "Pobiera pogodę i zapisuje do bazy", "public": true }
],
"source_code": "...kod Svelte (opis niżej)..."
}
}
Frontend (Svelte 5): karta z gradientem pokazuje najnowszy pomiar (duża temperatura, wiatr,
badge alertu), przycisk “Pobierz pogodę”, a pod nim historia pobrań - każdy wiersz z datą
i czasem, temperaturą, wiatrem i badge źródła (fioletowy cron, niebieski ręcznie · kto (ip)).
Pod historią sekcja Lokalizacja (widoczna dla userów z dostępem do flows): dwa pola lat/lng,
przycisk “Użyj mojej lokalizacji” (navigator.geolocation - wymaga HTTPS, na dev po HTTP
przeglądarka może odmówić) oraz “Zapisz” - GET flow, merge fields.env i PATCH
/connect/flows/<code>.json (z nagłówkiem X-CSRF-Token z meta). Domyślnie Warszawa;
zmiana obowiązuje od następnego pobrania.
Przepływ po kliknięciu (frontend niczego nie zapisuje - zapis robi flow):
-
POST /noe/apps/APP_ID/action/get_weather.json(POST akcji musi mieć.json) - przy
status: "pending"pollingGET /noe/apps/APP_ID/action/get_weather/status?flow_process_id=... - po zakończeniu przeładowuje listę
POST /noe/db/weather-example1/list
Po utworzeniu appki:
- podmień placeholder
APP_IDw kodzie:PATCH /noe/apps/:id/replace.json{ "field": "source_code", "old_string": "APP_ID", "new_string": "PRAWDZIWE_ID", "replace_all": true } - powiąż bazę z appką:
PATCH /noe/dbs/weather-example1.jsonz{ "db": { "app_id": "PRAWDZIWE_ID" } }- baza będzie widoczna w “Powiązaniach” na show appki
Pułapka przy wgrywaniu kodu przez curl: NIE wklejaj source_code w heredocu bez cudzysłowów -
shell zje backticki i ${...} z template literals oraz runy $state/$derived. Zapisz kod do
pliku i buduj JSON np. ruby/pythonem z odczytu pliku.
Krok 7 - weryfikacja
-
lokalnie musi działać worker SolidQueue (
./solid_queue_local_sugester2) - akcje appki i cron wykonują flow asynchronicznie; bez workera przycisk wisi na “Pobieram…”, a joby czekają w kolejce - ręczne pobranie: klik w appce -> rekord
source: manualz nazwą użytkownika i jego IP - cron:
POST /account/crons/:id/run.json(albo czekaj na harmonogram) -> rekordsource: cron - historia:
POST /noe/db/weather-example1/list, widok appki/noe/pogoda-example1albo rekordy bazy w/noe/dbs/weather-example1/records - po zmianie kodu appki zrób w przeglądarce twarde odświeżenie (stary bundle potrafi zostać w cache)
-
perform_now=1wykonuje flow synchronicznie w środku requestu - krok zapisu strzela wtedy HTTP do tego samego serwera dev i może dostać timeout (408), mimo że rekord się zapisze; normalna ścieżka (worker) nie ma tego problemu -
publiczny dostęp (
public: true, bez logowania) działa dopiero na koncie zweryfikowanym - na niezweryfikowanym system blokuje publiczne appki (403, aktywnośćnoe_app_blocked_unverified) - powiązania (appka -> baza/flow -> cron/konektory) widać na show appki w
/noe/appsi na/connect/flows/:id
Powiązane
- noe_api - pełne API aplikacji Noe
- noe_db_api - API baz danych Noe (schema, rekordy, metody publiczne)
- connector_api - konektory, w tym noe/custom_connector
- connector_flow_api - program flow (connector/mutation/if/case, result_key)
- account_crons_api - cykliczne akcje konta
- common_api - wspólne zasady API