Bu sayfa İngilizceden çevrilmiştir. İngilizce sayfayı okuyun
İş uygulamaları için çapraz kesen özellikler: veri denetimi, kişisel veri koruması, güvenli dağıtımlar, sağlık kontrolleri, oturum güvenliği, CSV ve XLSX içe aktarma, arka plan işleri ve onay iş akışları. Her şey sunucuda denetlenir; tarayıcı çalışma zamanı yalnızca kullanımı rahatlatır. KVKK ve GDPR akılda tutulan gizlilik yasalarıdır, ancak buradaki hiçbir şey bir ülkeye veya sektöre özgü değildir.
Alan düzeyinde veri denetimi
AuditLog, her form gönderimi ve audit işaretli bir action'a yapılan her çağrı için bir AuditEntry yazar. Kaydetmelerde giriş ayrıca kaydı ve değişen her alanı adıyla belirtir:
{"kind":"form","screen":"customer","action":"save_customer","ok":true,"user":"ayse",
"model":"Customer","record":"2","changes":[{"field":"name","old":"Customer 2","new":"Renamed"}]}- Formlar bunu kendiliğinden yapar: eski değerler ekranın sunucuda yüklediği kayıttan, yeni değerler doğrulanmış
Valid<M>içinden gelir. İstemcinin gönderdiği hiçbir şey eski değer olarak kullanılmaz ve formun istemciden almadığı alanlar hiçbir zaman değişmiş görünmez. - API kayıtları, modeli ve anahtarı
Valid<M>girdisinden alır. Veritabanında ne olduğunu yalnızca action bilir, bu yüzden farkı kendisi bildirir:
#[action(permission = "crm.customer.write", audit)]
pub async fn save_customer(c: Valid<Customer>, store: Customers, ctx: Ctx) -> Result<Customer> {
let current = store.get(c.id)?;
ctx.record_changes(¤t, &c);
store.save(c.into_inner())
}Aynısını form kaydetme action'larında yapmak da uygundur: action'ın raporu formunkinin yerine geçer.
- Değerler düz, dilden bağımsız string'lerdir (
1234.56 TRY,2024-05-01, seçim değerleri). Satır sürümleri dışarıda bırakılır. Maskeli alanlar maskeli kalır (aşağıya bakın). - Başarısız bir kayıt yine de kaydı adıyla belirtir, değişiklik içermez.
Alan maskeleme
Kişisel veri tutan metin alanlarını #[mask] ile işaretleyin. Burada ulusal kimlik numarası •••• 8901, e-posta p••••@example.com ve notlar •••• olarak görünür:
#[entity(table = "people")]
pub struct Person {
#[key]
pub id: PersonId,
pub name: String,
#[mask(last4)]
pub national_id: String,
#[mask(email, permission = "hr.contact.unmask")]
pub email: Option<String>,
#[mask]
pub notes: Option<String>,
}- Maskeleme sunucuda gerçekleşir: grid hücrelerinde, formlarda, API çıktısında, ajan araçlarında ve dolayısıyla hücrelerden oluşturulan CSV dışa aktarımlarında. Ham değer, izin olmadan hiçbir tarayıcıya veya API istemcisine ulaşmaz.
- Maskeyi açma izni, öznitelik bir isim vermedikçe snake case biçiminde
<model>.<field>.unmaskolur (person.national_id.unmask). - İzni olan kullanıcılar yine de önce maskeli metni görür ve bir gösterme düğmesi bulunur. Göstermek
POST /_zeyn/revealçağrısı yapar; bu çağrı izni yeniden denetler ve bir denetim kaydı yazar (kindreveal, alanrevealediçinde). Reddedilen denemeler de denetlenir. - İzni olan API istemcileri ham değerleri alır; bu alanların her biri çağrının denetim kaydındaki
revealediçinde listelenir. - Formlar maskeli alanları salt okunur metin olarak gösterir. Saklanan değer, kullanıcı göstermeye yetkili değilse ve gösterdikten sonra değiştirmediyse, kaydetme sırasında korunur. Boş bir maskeli alan normal bir girdidir, böylece veri giriş personeli onu bir kez doldurabilir ama sonradan geri okuyamaz.
- Maskeli bir alan üzerindeki filtreler ve sıralamalar, onu gösterme yetkisi olmayan kullanıcılar için yok sayılır, böylece bir değer arama yoluyla tahmin edilemez. Bu, bir grid'in her okuması için (sayfalar, dışa aktarımlar, tüm satırlar üzerindeki toplu action'lar, kayıtlı görünümler, ajan araçları) ve bir
DataQueryalan API action'ları için geçerlidir. Maskeli sütunlara.filter()eklemeyin. - Sayfalama imleçleri hiçbir zaman maskeli bir alanın değerini tutmaz. Maskeli bir alana göre sıralanmış liste, anahtar yerine konuma göre sayfalanır.
- Bir grid hiçbir zaman maskeli bir alana göre gruplamaz; üzerindeki
group_byyok sayılır ve günlüğe yazılır. - Island ve diğer veri kaynağı çıktıları, API çıktısı gibi her derinlikte maskelenir: içerideki her modelin maskeli alanları maskeli metin olarak çıkar ve hassas modellerin anahtarları okuma olarak kaydedilir.
- Grid'de gösterme, satırı grid'in action'ı üzerinden anahtar üzerinde bir filtreyle arar, bu yüzden action kendi anahtar alanındaki filtrelere uymalıdır.
#[mask]yalnızcaStringveOption<String>üzerinde derlenir.
Hassas kayıt okumaları
Kimin hangi kişisel veriyi ne zaman gördüğü, kimin değiştirdiği kadar kaydedilir.
#[entity(table = "cases", sensitive)](veya#[entity]altında#[sensitive]) bir modeli işaretler. Böyle bir kayıt bir kullanıcıya her gösterildiğinde, çağrının denetim kaydı bunureadsiçinde listeler ({"model":"Case","id":"2"}): bir grid sayfasının satırları, bir form, bir ajan aracı sonucu ve API çıktısı. Okumalar verinin render edildiği yerde kaydedilir, bu yüzden layout'ların içindeki bileşenler de sayılır.#[screen(..., sensitive)], ekranın her açılışını,modeldeğeri ekran adı veiddeğeri parametreleri olan bir okuma olarak kaydeder. Bu, tek bir modele bağlı olmayan kişisel veriler gösteren ekranlar içindir.#[screen(..., reason)]ayrıca herhangi bir veri gösterilmeden önce kullanıcıya nedenini sorar. Sunucu, bir neden verilene kadar sayfaya, parçalarına, grid, form ve gösterme çağrılarına428vereason_requiredkoduyla (tam sayfa yüklemelerinde bir neden formu olan bir sayfa) yanıt verir. Çalışma zamanı bir iletişim kutusunda sorar,POST /_zeyn/reason {screen, reason}gönderir (3 ila 300 karakter) ve isteği tekrarlar. Neden,HttpOnly,SameSite=Strictbir çerezde 15 dakika saklanır, yalnızca verildiği ekranı açar ve o ekranın her denetim kaydındakireasonalanına yazılır. Ajanlar ve API istemcileri neden veremez, bu yüzden böyle ekranları açamaz.- Hassas bir model üzerindeki bir grid'in dışa aktarımları (CSV, XLSX, PDF),
exportayarlı olarak dosya başına bir denetim kaydı yazar: model, biçim, satır sayısı ve istenen sıralama ile filtreler. Her satırıreadsiçinde listelemezler, böylece büyük bir dışa aktarım ucuz kalır; diğer modellerin dışa aktarımları ek bir maliyet getirmez. Birsensitiveveyareasonekranından dışa aktarmak ekran okumasını da kaydeder. - Bir ekranın tembel (lazy) parçaları (tembel sekmelerin kullandığı
POST /_zeyn/node), interceptor'ları çalıştırır ve okumaları ekranı açmakla tamamen aynı şekilde kaydeder. ctx.record_read(&record), uygulama kodundan bir okuma kaydeder.- Okuma kayıtlarını
AuditLog, sayfa yüklemeleri dahil herhangi bir çağrı için, hassas bir kayıt gösterilir gösterilmez yazar. Bir denetim interceptor'ı olmadan hiçbir şey kaydedilmez; kişisel veri tutan uygulamalar her zaman bir tane kaydetmelidir.
Onay iş akışları
Bir iş akışı, küçük ve tipli bir durum makinesidir. Durumlar bir #[choice] enum'udur; her geçiş başlangıç ve bitiş durumunu ve ihtiyaç duyduğu izni belirtir:
#[choice]
pub enum LeaveState { Draft, Submitted, Approved, Rejected }
pub fn leave_flow() -> Workflow<LeaveState> {
Workflow::new()
.transition("submit", LeaveState::Draft, LeaveState::Submitted, "people.leave.request")
.transition("approve", LeaveState::Submitted, LeaveState::Approved, "people.leave.approve")
.transition("reject", LeaveState::Submitted, LeaveState::Rejected, "people.leave.approve")
}
#[action(permission = "people.leave.request", audit)]
pub async fn move_leave(req: TransitionRequest<LeaveRequestId>, store: LeaveStore, ctx: Ctx) -> Result<LeaveState> {
let mut leave = store.get(req.id)?;
leave.state = leave_flow().apply(&ctx, leave.state, &req.transition, req.comment.as_deref())?;
store.save(leave)
}apply, depodan okunan durumla sunucuda çalışır. Bilinmeyen bir geçiş404, artık başlangıç durumunda olmayan bir kayıt409(başka biri önce taşıdı), eksik bir izin403, 1000 karakteri aşan bir yorum422döndürür.- Her geçiş denetim kaydındaki
transitionalanına yazılır: ad, nereden, nereye ve yorum; kullanıcı ve zaman kaydın kendisindedir. Transitions::of(flow, state, id, action)geçerli durumu, isteğe bağlı bir yorumu ve kullanıcının o durumdan yapabileceği her geçiş için bir düğmeyi gösterir (Workflow::allowed). Kayıt anahtarı sunucuda ekrandan gelir, bu yüzden bir istemci başka bir kaydı taşıyamaz. Ajanlar bunu bir form aracı olarak görür.- Düğme etiketleri kataloglardaki
workflow.<name>anahtarından gelir;.label(...)birini elle ayarlar. Workflow::check, geçersiz izinleri ve aynı durumdan iki kez tanımlanmış geçişleri bildirir; bir testte çağırın.- Showcase'deki izin talepleri bunu kullanır: taslak olarak kaydet, gönder, sonra onayla veya reddet.
Güvenli dağıtımlar
- Her yanıt
x-zeyn-buildtaşır ve her sayfa<meta name="zeyn-build">taşır. Kimlik, ayarlıysaZEYN_BUILD_IDdeğeridir (harfler, rakamlar,.,_,-, en fazla 64), aksi halde çalıştırılabilir dosyanın bir karmasıdır (hash).zeyn::server::build_id()bunu günlükler için döndürür. - Çalışma zamanı başka bir derlemeden bir yanıt gördüğünde nazik bir bildirim gösterir: yeni bir sürüm var, hazır olunca yeniden yükleyin. Kendiliğinden hiç yeniden yüklemez.
- Kullanıcıların formlara yazdıkları, ekran, parametreler ve form başına
sessionStorageiçinde saklanır ve yeniden yüklemeden sonra bir not ve atma düğmesiyle geri konur. Maskeli, parola, dosya ve gizli girdiler hiçbir zaman saklanmaz. Taslak, yazıldığı satır sürümünü korur, bu yüzden bu arada başkasının değiştirdiği bir kaydın taslağını kaydetmek çakışma olarak reddedilir. Başarılı bir kayıt taslağı temizler; bir günden eski taslaklar atılır.sessionStorageyalnızca o tarayıcı sekmesinde yaşar.
Sağlık kontrolleri
GET /_zeyn/health, süreç çalıştığı sürece200 okyanıtı verir. Liveness için kullanın.GET /_zeyn/ready,App::readiness(name, check)ile eklenen her kontrolü aynı anda, her biri beş saniye ile çalıştırır ve200 {"status":"ready"}veya503 {"status":"not_ready","failed":["database"]}yanıtı verir. Yalnızca kontrol adları gösterilir, hatalar asla.- İkisi de
base_pathaltında yer alır (/billing/_zeyn/health),Cache-Control: no-storeile yanıt verir ve oturum açmayı gerektirmez. - Hazırlık sonucu bir saniye boyunca yeniden kullanılır (
READY_CACHE) ve aynı anda yalnızca bir istek kontrolleri çalıştırır, bu yüzden bir yoklama seli veritabanını yükleyemez. - Sağlık hiçbir zaman hız sınırına tabi değildir, bu yüzden yoğun bir yoklama kendini kilitleyip sağlıklı bir uygulamayı yeniden başlatamaz. Hazırlık, bir
probe_ipsadresinden gelmiyorsa hız sınırlarına sayılır. - IP kuralları yine de geçerlidir:
deny_ipsher zaman,allow_only_ipsde yoklama adresiApp::probe_ips([...])içinde listelenmedikçe geçerlidir; bu liste yalnızca bu iki uç noktayı o adrese açar.
Geliştirme gecikmesi
App::dev_latency(Duration::from_millis(400)), her çağrıyı (sayfalar, parçalar, grid'ler, formlar, API, MCP ve çalışma zamanı çağrıları; statik varlıklar veya sağlık yoklamaları değil) geciktirir, böylece geliştiriciler yükleme durumlarını ve çift tıklamaları yavaş bir ağdaki kullanıcılar gibi görür. Yalnızca debug derlemelerinde çalışır: release derlemelerinde yöntem hiçbir şey yapmaz ve gecikme kodu derlemeden çıkarılır, bu yüzden yanlışlıkla bırakılması üretimi yavaşlatamaz. Açıkken başlangıçta bir uyarı yazdırılır.
Oturum güvenliği
Oturum açma bir tek oturum açma (single sign on) proxy'sinden, yerleşik OpenID Connect oturum açmasından veya bearer token'lardan gelir (bkz. docs/auth.md), bu yüzden bu özellikler yalnızca kimlik doğrulayıcının kullanıcının kim olduğunu ve yeniden kimlik doğrulama için ne zaman oturum açtığını söylemesini gerektirir (Identity::authenticated_at; ForwardAuth::auth_time("x-auth-time") Unix saniyelerini veya RFC 3339'u okur; DevAuth her isteği yeni oturum açılmış olarak bildirir).
App::idle_timeout(Duration), çalışma zamanının kullanıcıyı sondanidle_warningönce (varsayılan bir dakika) geri sayımlı bir uyarı iletişim kutusunda ve "oturumu açık tut" düğmesiyle uyarmasını sağlar; düğmePOST /_zeyn/keepaliveçağrısı yapar, böylece öndeki bir proxy etkinlik görür. Uygulamanın herhangi bir sekmesindeki girdi sayılır. Sonunda kullanıcılogout_urladresine, yoksareturn_toilelogin_urladresine gider, o da yoksa oturumu kapatılmış bir ekran görür. Form taslakları saklanır. Boşta kalma zaman aşımı yalnızca unutulmuş ekranları kapatır; gerçek oturumu sonlandırmak kimlik sağlayıcının işidir.#[action(permission = "...", reauth)],App::reauth_withiniçinde (varsayılan beş dakika) bir oturum açma gerektirir. Aksi halde sunucu, istemci ne gönderirse göndersinreauth_requiredkoduyla401yanıtı verir (API'deurn:zeyn:problem:reauth_requiredproblem türü). Çalışma zamanı kullanıcıdan yeni bir sekmede yeniden oturum açmasını ister (reauth_url, yoksalogin_url) ve geri döndüğünde isteği tekrarlar, böylece yazılan hiçbir şey kaybolmaz. Oturum açma zamanı olmayan kimlikler asla geçemez.
CSV içe aktarma
#[action(permission = "people.leave.import", audit)]
pub async fn import_leaves(rows: Vec<Valid<LeaveRequest>>, store: LeaveStore) -> Result<usize> { ... }
Screen::new("Import").body(Import::of(import_leaves).max_rows(1000))- Kullanıcı bir CSV dosyası seçer. Çalışma zamanı onu UTF-8 olarak, geçerli UTF-8 değilse Windows-1254 olarak okur (Türkçe elektronik tablolar) ve metni bir JSON gövdesinde
POST /_zeyn/importadresine gönderir (multipart gerekmez; en fazla 8 MB). Sunucu bir özetle, ilk on satırın önizlemesiyle, satır başına hatalarla ve indirilebilir bir hata raporuyla yanıt verir. - Ayrıştırma: bayt sırası işareti atılır,
,,;veya sekme başlık satırından algılanır, ayırıcıları ve satır sonlarını içeren"alıntılama,\r\nveya\n. - Sınırlar dosya okunurken denetlenir, sonrasında değil: okuma satır sınırında durur, 512 hücreden fazla bir başlık reddedilir ve veri satırları başlığın en fazla bir hücre ötesini tutar (böylece fazla değerler yine de bildirilir). Bir XLSX dosyası en fazla başlığın sahip olduğu sayıda sütun okur, başlığı en fazla modelin içe aktarabileceği sayıda sütun adlandırabilir ve 16 MB'dan büyük bir paylaşılan string parçası hiçbir şey açılmadan önce reddedilir.
- Sütunlar alan adına veya uygulamanın herhangi bir dilindeki etikete göre eşleştirilir; büyük küçük harf ve Türkçe noktalı ve noktasız i yok sayılır. Bilinmeyen, salt okunur, anahtar ve satır sürümü sütunları reddedilir.
- Her satır, modelin bir formu gibi aynen okunur ve doğrulanır: o dilin kullanıcısının yazdığı gibi değerler (
1.234,56,05.12.2026), alan kuralları ve modelin kendivalidatefonksiyonu. - İçe aktarma action'a yalnızca geçerli satırları gönderir;
.all_or_nothing(), her satır geçerli olmadıkça hiçbir şey içe aktarmaz. İçe aktarırken sunucu dosyayı yeniden okur ve doğrular; önizlemeden hiçbir şeye güvenilmez. Her iki adım da action'ın iznini gerektirir ve içe aktarma denetlenir. .max_rows(n)satırları sınırlar (varsayılan 5000, hiçbir zaman 50000'den fazla olamaz). Hata raporu elektronik tablo formüllerini etkisiz hale getirir. Maskeli alanlar önizlemede maskeli gösterilir.
XLSX içe aktarma
xlsx Cargo feature'ı ile aynı bileşen Excel çalışma kitaplarını (.xlsx) da alır. Çalışma zamanı dosyayı base64 olarak gönderir (aynı JSON gövdesinde "csv" yerine "xlsx") ve sunucu ilk çalışma sayfasını calamine ile okuyup CSV ile aynı satır satır doğrulamaya, önizlemeye ve hata raporuna aktarır.
- Hücreler türlerini korur: bir sayı sayı, bir tarih hücresi tarih olarak okunur, kullanıcının dili ne olursa olsun, bu yüzden Türkçe bir oturumda
1234.5yine 1.234,50 olur. Bir tarih hücresindeki duvar saati zamanları, bir formun okuduğu gibi, kullanıcının saat diliminde okunur. Seçimler değere veya etikete göre eşleşir. - Sınırlar: en fazla 8 MB dosya ve arşiv en fazla 1000 parçada en fazla 64 MB'a açılabilir; bu, hiçbir şey açılmadan önce arşivin bildirdiği boyutlardan denetlenir. Hücreler tek tek okunur ve okuma satır sınırında durur, bu yüzden bir milyon satırlı bir sayfa sınırdan fazlaya mal olmaz. Çalışma kitabı olmayan, parola korumalı veya yarım kesilmiş bir dosya net bir hata alır.
- Yalnızca ilk çalışma sayfası okunur. Formüller önbelleğe alınmış değerleriyle sayılır.
Arka plan işleri
App::new("Billing")
.job_concurrency(2)
.job("send_reminders", Schedule::every(Duration::from_secs(3600)).retry(3, Duration::from_secs(30)), send_reminders)
.job("rebuild_index", Schedule::manual(), rebuild_index)
.jobs_permission("ops.jobs.read")
.serve_until("0.0.0.0:3000", shutdown_signal())- Bir iş
async fn(Ctx) -> Result<()>biçimindedir. Bağlamı uygulamanın servislerini, yapılandırmasını ve varsayılan dilini ve hiçbir izni olmayanjob:<name>kimliğini içerir. Schedule::every(d)her aralıkta çalışır;Schedule::manual()yalnızca tetiklendiğinde. Action'lar işleri enjekte edilebilirJobsservisi ile tetikler:jobs.trigger("rebuild_index"). Çalışırken tetiklenen bir iş sonrasında bir kez daha çalışır; bir iş hiçbir zaman aynı anda iki kez çalışmaz..retry(times, first_delay)başarısız bir çalıştırmayı yeniden dener, beklemeyi on dakikaya kadar ikiye katlayarak.- Aynı anda en fazla
job_concurrencyiş (varsayılan dört) çalışır. - Durum (state, çalıştırma sayısı, hata sayısı, son başlangıç ve bitiş, son hata kodu) kodda
jobs.status(),jobs_permissionsahibi kullanıcılar içinGET /_zeyn/jobsile alınır. Hata ayrıntıları yalnızca sunucu günlüğüne gider. App::serve_until(addr, signal), sinyal tamamlandığında istek almayı bırakır, çalışan isteklerin bitmesine izin verir, yeniden deneme beklemelerini iptal eder ve çalışan işlere 30 saniye tanır.- Bir depo olmadan işler bellektedir: süreç durduğunda bekleyen veya çalışan iş kaybolur ve birden çok sunucuyla her biri kendi zamanlamasını çalıştırır.
Kalıcı işler
App::new("Billing")
.job_store(SqlJobStore::new(db))
.queued_job("send_invoice", Schedule::manual().retry(5, Duration::from_secs(10)), send_invoice)
.job("close_day", Schedule::every(Duration::from_secs(86_400)), close_day)
async fn send_invoice(ctx: Ctx, invoice_id: String) -> Result<()> { ... }
jobs.enqueue("send_invoice", "42").await?;JobStoreçalıştırmaları tutar:enqueue,schedule, lease (worker kimliği ve bitiş zamanı) ileclaim,heartbeat,completevefail.MemoryJobStore(en fazla 10.000 çalıştırma, önce en eski başarısız çalıştırma atılır) varsayılandır;SqlJobStore(postgres,oraclefeature'ları) çalıştırmalarızeyn_jobsiçinde tutar.create_job_tables(dialect, prefix)bir migration için ifadeleri verir.- Bir depo ayarlıyken,
Schedule::everyişleri sonraki çalıştırmalarını depoda tutar, bu yüzden bir yeniden başlatma onu kaydırmaz ve her aralığı yalnızca bir sunucu çalıştırır.triggerorada da bir çalıştırma kuyruğa alır. queued_jobişleri her çalıştırmanın yükünü (payload) alır (en fazlaMAX_JOB_PAYLOAD, 4000 bayt); düz işler de kuyruğa alınabilir ve yükü yok sayar.enqueue_afterbir çalıştırmayı geciktirir.- Claim, bu sunucunun bildiği işlerin zamanı gelmiş çalıştırmalarını, en eskiden başlayarak alır ve onları
JOB_LEASE(60 saniye) boyunca tutar; çalıştırma sürerken her 20 saniyede yenilenir.completevefail, bir çalıştırmayı yalnızca worker onu hâlâ tutarken değiştirir. Worker'ı duran bir çalıştırma, lease'i bitince yeniden claim edilir; kaybolan deneme sayılır, bu yüzden sunucusunu çökerten bir çalıştırma başarısız olarak biter. - Başarısız bir deneme, deneme hakkı kaldıkça backoff süresini bekler (ikiye katlanır, en fazla
MAX_BACKOFF_MS, on dakika), sonra hata koduylafailedolarak kalır. Bir zamanlama sonraki aralığına geçer. - Worker'lar zamanı gelmiş çalıştırmaları her
JOB_POLL(bir saniye) aralığında ve aynı sunucuda birenqueuesonrasında hemen arar. Her claim, boş iş yuvası sayısı kadar çalıştırma alır, bu yüzden bellek kuyrukla birlikte büyümez. - PostgreSQL tek bir
UPDATE ... WHERE id IN (SELECT ... FOR UPDATE SKIP LOCKED) RETURNINGile claim eder. Oracle 11g ve sonrası, bir transaction içindeROWNUMveFOR UPDATE SKIP LOCKEDile kimlikleri seçer, günceller ve commit'ten önce geri okur. Oracle kilitleyen bir select'i sıralayamaz, bu yüzden orada zamanı gelmiş çalıştırmalar belirli bir sıra olmadan alınır. Zamanlar,BIGINTveyaNUMBER(19)sütunlarında Unix milisaniyesidir. - Bir depo varken, aynı işin zamanlanmış bir çalıştırması ile tetiklenmiş bir çalıştırması çakışabilir; aynı anda iki kez çalışmaması gereken işler kendilerini korumalıdır (örneğin bir satır kilidiyle).