Model danych i generowanie kodu
Plik:
herd/_model.goatopisuje strukturę aplikacji i jest głównym źródłem informacji wykorzystywanym przez generator Goat.
Model definiuje między innymi:
- metadane aplikacji,
- obsługiwane języki,
- role użytkowników,
- encje,
- właściwości encji,
- relacje,
- ograniczenia danych,
- uprawnienia do odczytu i zapisu,
- moduły przypisane do encji,
- zachowanie CRUD,
- elementy SEO i SSR.
Na podstawie tych informacji Goat może wygenerować spójne elementy wielu warstw aplikacji, między innymi:
- modele Go,
- DAO i repozytoria,
- DTO,
- API,
- komendy CRUD,
- migracje SQL,
- formularze,
- listy i widoki administracyjne,
- elementy frontendu.
Najważniejszą korzyścią nie jest samo generowanie plików. Model pozwala opisać daną strukturę raz, a następnie wykorzystać tę definicję w wielu częściach aplikacji.
Zamiast ręcznie synchronizować backend, bazę danych, API i frontend, możesz zmienić model i pozwolić generatorowi przygotować wynikające z tej zmiany modyfikacje.
Model jako opis aplikacji
herd/_model.goat powinien opisywać przede wszystkim intencję i strukturę aplikacji, a nie szczegóły implementacyjne każdego wygenerowanego pliku.
W uproszczeniu:
_model.goat
↓
model domeny
↓
generator
↓
backend + baza + API + frontendPrzykładowo definicja pola:
add --name=title --type=web_titlemoże wpływać nie tylko na model backendu, ale również na sposób przechowywania danych, generowane DTO, formularze i widoki.
Dzięki temu ta sama informacja nie musi być wielokrotnie deklarowana w różnych technologiach.
Metadane aplikacji
Model rozpoczyna się od podstawowej konfiguracji aplikacji:
app --name goat \
--version 0.0.1 \
--path app \
--go-prefix code.pozoga.eu/spozoga/goatcms.com \
--terminal-prefix APP \
--default-language=en \
--supported-languages=pl,en,deDefinicja określa między innymi:
- nazwę aplikacji,
- wersję,
- ścieżkę projektu,
- prefiks modułów Go,
- prefiks używany przez terminal,
- domyślny język,
- listę obsługiwanych języków.
Te informacje mogą być później wykorzystywane przez generator oraz poszczególne moduły aplikacji.
Role i uprawnienia
Role definiowane są bezpośrednio w modelu.
Przykład:
role:add --name=admin --super
role:add --name=manager
role:add --name=userW tym przypadku aplikacja posiada trzy role:
admin,manager,user.
Flaga:
--superoznacza rolę o rozszerzonych uprawnieniach systemowych.
Role mogą być później wykorzystywane podczas definiowania dostępu do właściwości encji.
Przykład:
def --write=admin,manager --read=userpozwala określić, które role mogą modyfikować dane, a które mogą je odczytywać.
Dzięki temu podstawowe reguły dostępu mogą zostać zdefiniowane już na poziomie modelu zamiast być powtarzane niezależnie w API, formularzach i backendzie.
Encje bazowe
Jeżeli kilka encji posiada wspólną strukturę, warto wydzielić ją do encji bazowej.
Przykładem jest content:
entity:base:add --name content \
--label slug \
--base base_entityEncja bazowa może definiować wspólne:
- właściwości,
- relacje,
- ograniczenia,
- zasady dostępu.
W projekcie content reprezentuje podstawową strukturę treści publikowanej w aplikacji.
Może być następnie wykorzystany przez:
- strony,
- dokumentację,
- artykuły,
- inne typy treści.
Właściwości encji
Właściwości definiowane są wewnątrz sekcji --properties.
Przykład:
--properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=title \
--type=web_title \
--doc="Title is a short heading that describes the content."
add --name=slug \
--type=web_slug \
--required \
--doc="Slug is a URL-friendly name for the content."
add --name=lang \
--type=language \
--required \
--doc="Language of the current content."
PROPERTIESEOFKażda właściwość może określać między innymi:
- nazwę,
- typ,
- obowiązkowość,
- sposób prezentacji,
- dostęp,
- dokumentację,
- dodatkowe zachowanie generatora.
Typ właściwości niesie więcej informacji niż sam typ kolumny w bazie.
Przykładowo:
web_title
web_slug
language
seo_description
block_content
file_setmogą określać również sposób walidacji, serializacji i prezentacji danych w interfejsie.
Dziedziczenie encji
Encje mogą rozszerzać encje bazowe.
Przykładowa encja dokumentacji:
entity:extended --name doc --base contentoznacza, że doc dziedziczy właściwości i zachowania zdefiniowane dla content.
Pozwala to uniknąć powtarzania takich pól jak:
title
slug
lang
description
bodyw każdej encji reprezentującej treść.
Encja może jednocześnie dodać własne właściwości.
Przykład:
--properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=ver \
--type=short_text \
--doc="Goat CLI version number for the page"
PROPERTIESEOFW ten sposób doc zachowuje cały model content, ale dodatkowo przechowuje wersję dokumentacji.
Relacje
Relacje pomiędzy encjami można definiować bezpośrednio w modelu.
Przykład:
--relations=<<RELATIONSEOF
def --onremove
add --name=owner \
--to=user \
--doc="Owner is the author of the article."
RELATIONSEOFDefinicja określa relację owner prowadzącą do encji user.
Generator może wykorzystać taką informację w wielu miejscach:
- modelu danych,
- schemacie SQL,
- API,
- DAO,
- formularzach,
- widokach administracyjnych.
Dzięki temu relacja jest opisana w jednym miejscu zamiast niezależnie w każdej warstwie aplikacji.
Ograniczenia danych
Model może również definiować ograniczenia dotyczące danych.
Przykład:
--constraints=<<CONSTRAINTSEOF
unique:add --fields=lang,slug
CONSTRAINTSEOFoznacza, że kombinacja:
lang + slugmusi być unikalna.
Pozwala to przechowywać ten sam slug dla różnych języków:
pl / architektura
en / architecture
de / architekturprzy jednoczesnym zabezpieczeniu przed utworzeniem dwóch rekordów o tym samym języku i slugu.
Moduły encji
Encja może otrzymywać dodatkowe zachowanie za pomocą modułów.
Przykładowo encja doc korzysta z:
module:add --name=seo
module:add --name=ssr
module:add --name=crudModuły pozwalają rozszerzać encję bez konieczności powtarzania całej implementacji.
W praktyce model encji może więc opisywać nie tylko jej dane, ale również sposób, w jaki ma być używana przez aplikację.
Moduł SEO
Moduł:
module:add --name=seododaje zachowanie związane z metadanymi strony.
W przypadku encji dziedziczącej po content może korzystać między innymi z:
title
descriptiondo przygotowania informacji potrzebnych przez wyszukiwarki i mechanizmy udostępniania treści.
Dzięki temu podstawowa obsługa SEO może być wynikiem modelu zamiast ręcznie implementowaną dla każdej strony.
Renderowanie SSR
Moduł:
module:add --name=ssrpozwala powiązać rekord encji z publiczną trasą aplikacji.
Przykład dla dokumentacji:
module:add \
--name=ssr \
--slug=doc \
--route="/doc/{lang}/{slug}" \
--property:list:list="title,description" \
--property:list:details="title,body"Definicja wskazuje między innymi:
- ścieżkę publiczną,
- sposób identyfikacji rekordu,
- pola potrzebne na liście,
- pola potrzebne w widoku szczegółowym.
Dla przykładowego dokumentu wynikowa ścieżka może wyglądać tak:
/doc/pl/architekturaModuł SSR może następnie wykorzystać dane encji do przygotowania strony renderowanej po stronie serwera.
Moduł CRUD
Moduł:
module:add --name=crudokreśla sposób obsługi encji przez wygenerowane operacje administracyjne.
Przykład:
module:add \
--name=crud \
--property:list:list="ver,lang,title" \
--property:list:persist="title,slug,lang,description,body,ver"property:list:list określa właściwości wykorzystywane podczas prezentowania listy rekordów.
property:list:persist określa pola używane podczas tworzenia i aktualizowania rekordu.
Moduł crud definiuje kontrakt WebUI/API. Pola komend konsolowych konfiguruje się osobno za pomocą crud_cli:
module:add \
--name=crud_cli \
--property:list:list="username,email" \
--property:list:persist="password,role,username,email"Wygenerowane komendy zachowują namespace crud:<entity>:*. crud_cli.list określa pola zwracane przez crud:<entity>:list, a crud_cli.persist określa flagi tworzenia i aktualizacji. Komenda list wypisuje po jednym obiekcie JSON w wierszu i obsługuje --limit, --order-by, --order-direction oraz --search.
Rozszerzalne moduły
Goat umożliwia również definiowanie własnych typów modułów.
Przykład:
entity:module:def \
--type=top_box_counter \
--query:count=<<QUERYDEFEOF
def --required
QUERYDEFEOF \
--short_text:label=<<LABELDEFEOF
def --required
LABELDEFEOFDefinicja tworzy typ modułu top_box_counter, który wymaga:
- zapytania
count, - etykiety
label.
Na jego podstawie można następnie tworzyć konkretne moduły.
Przykład:
module:add \
--name=admin_counter \
--type=top_box_counter \
--label=Administrators \
--query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=admin
WHEREEOF
QUERYEOFPozwala to budować powtarzalne elementy aplikacji na wyższym poziomie abstrakcji.
Zamiast każdorazowo implementować licznik użytkowników w backendzie i frontendzie, można zdefiniować jego strukturę jako moduł modelu.
Przykład modułu dashboardu
Kilka modułów może zostać połączonych w większą strukturę.
Przykład:
entity:module:def \
--type=summary_cards \
--modules:list=<<MODULESLISTEOF
def --types=top_box_counter --required
MODULESLISTEOFNastępnie encja user może otrzymać zestaw liczników:
module:add \
--name=summary_cards \
--type=summary_cards \
--modules:list=<<SUMMARYCARDSOF
module:add \
--name=admin_counter \
--type=top_box_counter \
--label=Administrators \
--query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=admin
WHEREEOF
QUERYEOF
module:add \
--name=manager_counter \
--type=top_box_counter \
--label=Managers \
--query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=manager
WHEREEOF
QUERYEOF
module:add \
--name=user_counter \
--type=top_box_counter \
--label=Users \
--query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=user
WHEREEOF
QUERYEOF
SUMMARYCARDSOFModel opisuje tutaj nie tylko strukturę danych, ale również element dashboardu i zapytania potrzebne do jego zasilenia.
To pokazuje, że herd/_model.goat nie jest wyłącznie odpowiednikiem definicji schematu bazy danych. Może opisywać także wyższy poziom zachowania aplikacji.
Encje niezależne
Nie każda encja musi dziedziczyć po content.
Przykładem jest galeria:
entity:add \
--name gallery \
--label title \
--base base_entityJej właściwości mogą wyglądać następująco:
--properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=title \
--type=nice_name \
--doc="Title is a short heading that describes the content."
def --write=admin,manager --read=user
add --name=slug \
--type=web_slug \
--unique \
--doc="Slug is a URL-friendly name for the content."
add --name=files \
--type=file_set \
--doc="Files for the gallery"
PROPERTIESEOFPokazuje to, że model może opisywać zarówno encje oparte na wspólnej bazie, jak i całkowicie niezależne struktury domenowe.
Generowanie kodu
Po zmianie:
herd/_model.goaturuchom generator:
goat relub bezpośrednio:
go run ./scripts reGenerator odczytuje aktualny model i przygotowuje wynikające z niego zmiany w aplikacji.
Proces można przedstawić w uproszczeniu tak:
herd/_model.goat
↓
herd/gen.goat
↓
szablony
↓
generator
↓
kod aplikacjiGenerowane mogą być między innymi:
- modele,
- repozytoria,
- API,
- komendy CLI,
- frontend,
- migracje SQL.
Podstawowy schemat bazy jest generowany również do:
app/static/raw/data/sql/migrations/0001_schema.sqlKonfiguracja generatora
Przed większymi zmianami sposobu generowania warto sprawdzić:
herd/gen.goatPlik opisuje proces generowania, wykorzystywane szablony oraz miejsca, do których trafiają ich rezultaty.
Jeżeli chcesz zmienić sposób generowania wielu podobnych elementów, zwykle lepiej zmodyfikować odpowiedni szablon niż ręcznie powtarzać tę samą zmianę w wielu plikach.
W uproszczeniu:
_model.goatokreśla co ma zostać wygenerowane,
natomiast:
herd/gen.goat
herd/gen/templates/określają jak ma wyglądać wynik.
Wygenerowany kod można edytować
Kod utworzony przez Goat pozostaje zwykłym kodem źródłowym projektu.
Możesz go:
- modyfikować,
- refaktoryzować,
- rozszerzać,
- uzupełniać o własną logikę,
- commitować jak każdy inny kod.
Generator ma przyspieszać pracę, a nie zamykać użytkownika w swoim modelu generowania.
Nie każda zmiana musi więc zostać odwzorowana w herd/_model.goat.
Jeżeli potrzebujesz jednorazowej, specyficznej dla danej funkcjonalności modyfikacji, bezpośrednia edycja wygenerowanego kodu może być najprostszym rozwiązaniem.
Jeżeli jednak zauważysz, że wykonujesz tę samą zmianę wielokrotnie, warto przenieść ją poziom wyżej — do modelu lub szablonu generatora.
Kiedy zmienić model?
Zmień herd/_model.goat, gdy modyfikacja dotyczy struktury lub zachowania domeny.
Przykłady:
- dodanie encji,
- dodanie właściwości,
- zmiana relacji,
- dodanie ograniczenia,
- zmiana uprawnień,
- dodanie modułu,
- zmiana konfiguracji CRUD,
- zmiana publicznej trasy encji.
Przykład:
add \
--name=published_at \
--type=datetimeJeżeli published_at jest elementem modelu domeny, powinien zostać opisany właśnie tutaj.
Kiedy zmienić szablon?
Zmodyfikuj szablon generatora, gdy zmiana powinna dotyczyć wszystkich elementów określonego typu.
Przykłady:
- wszystkie formularze mają otrzymać dodatkową strukturę,
- każdy wygenerowany endpoint ma korzystać z nowego mechanizmu,
- wszystkie encje mają otrzymywać dodatkowy kod pomocniczy,
- chcesz zmienić konwencję wygenerowanego frontendu.
Takie zmiany najlepiej wprowadzać w:
herd/gen/templates/zamiast poprawiać wiele wygenerowanych plików osobno.
Kiedy edytować kod bezpośrednio?
Bezpośrednia edycja jest odpowiednia, gdy zmiana jest specyficzna dla konkretnego przypadku.
Przykładowo:
- nietypowa logika biznesowa,
- dodatkowa integracja,
- specjalny endpoint,
- wyjątkowe zachowanie jednego formularza,
- złożone zapytanie,
- ręczna optymalizacja,
- niestandardowy element UI.
Nie ma potrzeby komplikowania generatora tylko po to, aby obsłużyć pojedynczy wyjątek.
Dobrym kryterium jest pytanie:
Czy ta zmiana opisuje regułę projektu, czy wyjątek charakterystyczny dla jednej funkcjonalności?
Reguły warto przenosić do modelu lub generatora. Wyjątki mogą pozostać bezpośrednio w kodzie.
Bezpieczny proces zmian
Typowa zmiana modelu wygląda następująco:
zmiana _model.goat
↓
goat re
↓
git diff
↓
ręczne dopracowanie kodu
↓
testy
↓
commitW praktyce:
# zmień model
vim herd/_model.goat
# wygeneruj zmiany
goat re
# sprawdź rezultat
git diff
# uruchom testy
goat run:script --path=herd/test.goatgit diff jest szczególnie ważny, ponieważ pozwala zobaczyć rzeczywisty zakres zmian przygotowanych przez generator.
Nie traktuj generowania jako operacji, której rezultat należy przyjmować bez kontroli.
Generator przygotowuje kod, ale ostateczna decyzja o zmianie pozostaje po stronie programisty.
Model a istniejąca baza danych
Zmiana modelu może powodować zmianę wygenerowanego schematu SQL.
Nie oznacza to automatycznie, że jest ona bezpieczna dla istniejącej bazy danych.
Przykładowo:
zmiana _model.goat
↓
zmiana docelowego schematunie jest tym samym co:
bezpieczna migracja istniejących danychSzczególnej uwagi wymagają między innymi:
- usuwanie pól,
- zmiany typów,
- dodawanie wymaganych pól,
- zmiany relacji,
- nowe ograniczenia,
- zmiany indeksów.
Przed wdrożeniem takiej zmiany przejrzyj wygenerowany SQL i przygotuj odpowiednią ścieżkę migracji.
Model jako warstwa dla AI
Deklaratywny model aplikacji ma dodatkową zaletę podczas pracy z AI.
W wielu przypadkach model nie musi analizować osobno:
modelu Go
DAO
DTO
API
migracji
formularza
widoku AngularaMoże zamiast tego zmodyfikować jedną definicję:
herd/_model.goata następnie pozwolić Goat wygenerować konsekwencje tej zmiany.
Przykładowo:
"Dokument powinien posiadać datę publikacji"
↓
AI zmienia _model.goat
↓
goat re
↓
aktualizacja wymaganych warstwZmniejsza to ilość kodu potrzebnego do analizy oraz ogranicza ryzyko pominięcia jednej z warstw.
Nie każda zmiana powinna jednak odbywać się przez model. Jeżeli zadanie dotyczy pojedynczego fragmentu implementacji, bardziej efektywna może być bezpośrednia edycja kodu.
Przykładowy model
Poniższy fragment pokazuje kilka podstawowych możliwości modelu Goat:
# App metadata
app --name goat \
--version 0.0.1 \
--path app \
--go-prefix code.pozoga.eu/spozoga/goatcms.com \
--terminal-prefix APP \
--default-language=en \
--supported-languages=pl,en,de
# Roles
role:add --name=admin --super
role:add --name=manager
role:add --name=user
# Dashboard module types
entity:module:def --type=top_box_counter --query:count=<<QUERYDEFEOF
def --required
QUERYDEFEOF --short_text:label=<<LABELDEFEOF
def --required
LABELDEFEOF
entity:module:def --type=summary_cards --modules:list=<<MODULESLISTEOF
def --types=top_box_counter --required
MODULESLISTEOF
# User
entity:overwrite --name=user --base=user_base_entity --system=<<ENTITYSYSTEMEOF
module:add --name=summary_cards --type=summary_cards --modules:list=<<SUMMARYCARDSOF
module:add --name=admin_counter --type=top_box_counter --label=Administrators --query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=admin
WHEREEOF
QUERYEOF
module:add --name=manager_counter --type=top_box_counter --label=Managers --query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=manager
WHEREEOF
QUERYEOF
module:add --name=user_counter --type=top_box_counter --label=Users --query:count=<<QUERYEOF
where:and --conditions=<<WHEREEOF
eq --field=role --value=user
WHEREEOF
QUERYEOF
SUMMARYCARDSOF
module:add --name=crud \
--property:list:list="username,email" \
--property:list:persist="role,username,email"
module:add --name=crud_cli \
--property:list:list="username,email" \
--property:list:persist="password,role,username,email"
ENTITYSYSTEMEOF
# Base content entity
entity:base:add --name content --label slug --base base_entity --properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=title \
--type=web_title \
--doc="Title is a short heading that describes the content."
add --name=slug \
--type=web_slug \
--required \
--doc="Slug is a URL-friendly name for the content."
add --name=lang \
--type=language \
--required \
--doc="Language of the current content."
def --write=admin,manager --read=user
add --name=description \
--type=seo_description \
--doc="Short description for SEO and social media."
add --name=body \
--type=block_content \
--doc="Body contains the content data structured into blocks."
PROPERTIESEOF --relations=<<RELATIONSEOF
def --onremove
add --name=owner \
--to=user \
--doc="Owner is the author of the article."
RELATIONSEOF --constraints=<<CONSTRAINTSEOF
unique:add --fields=lang,slug
CONSTRAINTSEOF
# Page
entity:extended --name page --base content --doc=<<DOCEOF
Pages are the main sections of the website.
They contain essential content that rarely changes and
plays an important role in the overall structure and context of the website.
DOCEOF --system=<<ENTITYSYSTEMEOF
module:add --name=seo
module:add --name=ssr \
--slug=page \
--property:list:list="title,description" \
--property:list:details="title,body"
module:add --name=crud \
--property:list:list="title,description" \
--property:list:persist="title,slug,lang,description,body"
module:add --name=crud_cli \
--property:list:list="title,description" \
--property:list:persist="title,slug,lang,description,body"
ENTITYSYSTEMEOF
# Documentation
entity:extended --name doc --base content --doc=<<DOCEOF
The document contains comprehensive project documentation,
including available commands, configuration details,
usage examples and other information required to work
with the project effectively.
DOCEOF --properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=ver \
--type=short_text \
--doc="Goat CLI version number for the page"
PROPERTIESEOF --system=<<ENTITYSYSTEMEOF
module:add --name=seo
module:add --name=ssr \
--slug=doc \
--route="/doc/{lang}/{slug}" \
--property:list:list="title,description" \
--property:list:details="title,body"
module:add --name=crud \
--property:list:list="ver,lang,title" \
--property:list:persist="title,slug,lang,description,body,ver"
module:add --name=crud_cli \
--property:list:list="ver,lang,title" \
--property:list:persist="title,slug,lang,description,body,ver"
ENTITYSYSTEMEOF
# Gallery
entity:add --name gallery --label title --base base_entity --properties=<<PROPERTIESEOF
def --write=admin,manager --read=user --list
add --name=title \
--type=nice_name \
--doc="Title is a short heading that describes the content."
def --write=admin,manager --read=user
add --name=slug \
--type=web_slug \
--unique \
--doc="Slug is a URL-friendly name for the content."
add --name=files \
--type=file_set \
--doc="Files for the gallery"
PROPERTIESEOFTen przykład pokazuje najważniejszą ideę modelu Goat: jedna deklaratywna definicja może opisywać nie tylko bazę danych, lecz również uprawnienia, relacje, CRUD, routing, SEO, SSR i elementy interfejsu.
Najważniejsza zasada
Model powinien przejmować te elementy aplikacji, które są powtarzalne i mogą zostać opisane deklaratywnie.
Generator przekłada je na konkretną implementację, a wygenerowany kod pozostaje pod pełną kontrolą programisty.
W praktyce możesz pracować na trzech poziomach:
model — gdy zmiana dotyczy struktury i reguł aplikacji,
generator i szablony — gdy chcesz zmienić sposób tworzenia całej klasy elementów,
kod aplikacji — gdy potrzebujesz indywidualnej implementacji.
Nie chodzi o wygenerowanie jak największej części projektu za wszelką cenę.
Goat ma generować to, co powtarzalne, aby więcej czasu można było poświęcić na kod, który rzeczywiście wyróżnia aplikację.