@devoplus/one-sdk 0.0.0-stage → 0.2.0-next.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +268 -0
- package/README.md +894 -2
- package/bin/accessRequest.d.mts +96 -0
- package/bin/accessRequest.mjs +340 -0
- package/bin/appOps.d.mts +97 -0
- package/bin/appOps.mjs +336 -0
- package/bin/dataCommand.mjs +131 -0
- package/bin/devConfig.mjs +132 -0
- package/bin/one.mjs +341 -0
- package/bin/session.mjs +239 -0
- package/dist/agentInput.d.ts +9 -0
- package/dist/agentInput.d.ts.map +1 -0
- package/dist/agentInput.js +102 -0
- package/dist/agentInput.js.map +1 -0
- package/dist/agentTools.d.ts +83 -0
- package/dist/agentTools.d.ts.map +1 -0
- package/dist/agentTools.js +164 -0
- package/dist/agentTools.js.map +1 -0
- package/dist/ai.d.ts +67 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +129 -0
- package/dist/ai.js.map +1 -0
- package/dist/audit.d.ts +21 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +58 -0
- package/dist/audit.js.map +1 -0
- package/dist/backend.d.ts +27 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +63 -0
- package/dist/backend.js.map +1 -0
- package/dist/cache.d.ts +20 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +84 -0
- package/dist/cache.js.map +1 -0
- package/dist/client.d.ts +147 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +469 -0
- package/dist/client.js.map +1 -0
- package/dist/components.d.ts +235 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +1491 -0
- package/dist/components.js.map +1 -0
- package/dist/context.d.ts +110 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +126 -0
- package/dist/context.js.map +1 -0
- package/dist/data/crud.d.ts +74 -0
- package/dist/data/crud.d.ts.map +1 -0
- package/dist/data/crud.js +614 -0
- package/dist/data/crud.js.map +1 -0
- package/dist/data/ddl.d.ts +76 -0
- package/dist/data/ddl.d.ts.map +1 -0
- package/dist/data/ddl.js +362 -0
- package/dist/data/ddl.js.map +1 -0
- package/dist/data/fold.d.ts +14 -0
- package/dist/data/fold.d.ts.map +1 -0
- package/dist/data/fold.js +76 -0
- package/dist/data/fold.js.map +1 -0
- package/dist/data/model.d.ts +113 -0
- package/dist/data/model.d.ts.map +1 -0
- package/dist/data/model.js +408 -0
- package/dist/data/model.js.map +1 -0
- package/dist/data/values.d.ts +29 -0
- package/dist/data/values.d.ts.map +1 -0
- package/dist/data/values.js +152 -0
- package/dist/data/values.js.map +1 -0
- package/dist/dexa.d.ts +44 -0
- package/dist/dexa.d.ts.map +1 -0
- package/dist/dexa.js +74 -0
- package/dist/dexa.js.map +1 -0
- package/dist/dexaCommand.d.ts +116 -0
- package/dist/dexaCommand.d.ts.map +1 -0
- package/dist/dexaCommand.js +226 -0
- package/dist/dexaCommand.js.map +1 -0
- package/dist/export/columns.d.ts +62 -0
- package/dist/export/columns.d.ts.map +1 -0
- package/dist/export/columns.js +127 -0
- package/dist/export/columns.js.map +1 -0
- package/dist/export/csv.d.ts +10 -0
- package/dist/export/csv.d.ts.map +1 -0
- package/dist/export/csv.js +30 -0
- package/dist/export/csv.js.map +1 -0
- package/dist/export/report.d.ts +19 -0
- package/dist/export/report.d.ts.map +1 -0
- package/dist/export/report.js +91 -0
- package/dist/export/report.js.map +1 -0
- package/dist/export/response.d.ts +14 -0
- package/dist/export/response.d.ts.map +1 -0
- package/dist/export/response.js +70 -0
- package/dist/export/response.js.map +1 -0
- package/dist/export/xlsx.d.ts +15 -0
- package/dist/export/xlsx.d.ts.map +1 -0
- package/dist/export/xlsx.js +167 -0
- package/dist/export/xlsx.js.map +1 -0
- package/dist/export/zip.d.ts +7 -0
- package/dist/export/zip.d.ts.map +1 -0
- package/dist/export/zip.js +83 -0
- package/dist/export/zip.js.map +1 -0
- package/dist/files.d.ts +52 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +137 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs.d.ts +57 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +157 -0
- package/dist/jobs.js.map +1 -0
- package/dist/log.d.ts +22 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +107 -0
- package/dist/log.js.map +1 -0
- package/dist/notify.d.ts +41 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +11 -0
- package/dist/notify.js.map +1 -0
- package/dist/org.d.ts +184 -0
- package/dist/org.d.ts.map +1 -0
- package/dist/org.js +114 -0
- package/dist/org.js.map +1 -0
- package/dist/people.d.ts +39 -0
- package/dist/people.d.ts.map +1 -0
- package/dist/people.js +88 -0
- package/dist/people.js.map +1 -0
- package/dist/problem.d.ts +12 -0
- package/dist/problem.d.ts.map +1 -0
- package/dist/problem.js +33 -0
- package/dist/problem.js.map +1 -0
- package/dist/runtime.d.ts +92 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +8 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scenarios.d.ts +125 -0
- package/dist/scenarios.d.ts.map +1 -0
- package/dist/scenarios.js +251 -0
- package/dist/scenarios.js.map +1 -0
- package/package.json +55 -3
package/README.md
CHANGED
|
@@ -1,3 +1,895 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @devoplus/one-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Devoplus One** uygulamalarının (Devoplus Portal içinde iframe ile gösterilen kullanıcı uygulamaları) platform
|
|
4
|
+
servislerine **ince istemcisi**. SDK sır taşımaz, kiracı seçmez ve
|
|
5
|
+
belirteç üretmez: kimliği platform kapısının yazdığı başlıklardan **okur**, backend'e kullanıcının uygulama
|
|
6
|
+
belirteciyle gider, dosyayı kuruluma bağlı dosya deposuna yazar.
|
|
7
|
+
|
|
8
|
+
Platformun tamamı: `devoplus/devoplus-one` (mimari `docs/architecture.md`); kararlar
|
|
9
|
+
`devoplus-root/docs/devoplus-one-karar-defteri.md`; örnek uygulama `apps/hello`.
|
|
10
|
+
|
|
11
|
+
## Kurulum
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @devoplus/one-sdk@next # ön sürüm (test kanalı)
|
|
15
|
+
pnpm add @devoplus/one-sdk # kararlı sürüm (latest)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Kararlı sürüm (`latest`) yayımlanana kadar `@next` ile kurun; iki kanal Sürümleme bölümünde anlatılır.
|
|
19
|
+
|
|
20
|
+
Paket yalnız **ESM** dağıtılır (`dist/index.js` + `dist/index.d.ts`); Node ≥ 22. Çalışma ortamı tipleri (`OneDatabase`,
|
|
21
|
+
`OneFilesBinding`, `OneCacheBinding`, `OneApp` vb.) paketin içindedir; ek bir tip paketi gerekmez. `tsconfig` içinde
|
|
22
|
+
`"lib": ["ES2022", "WebWorker"]` ve `"types": []` yeterlidir (standart Web API'leri `WebWorker` kütüphanesinden gelir).
|
|
23
|
+
|
|
24
|
+
### Yerel geliştirme: `one` komutu
|
|
25
|
+
|
|
26
|
+
Paket `one` komutunu da getirir (D-72); uygulamanın kök dizininde çalışır ve `one.manifest.json`'u okur:
|
|
27
|
+
|
|
28
|
+
| Komut | Ne yapar |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `one dev [--port n] [--api-base url]` | Uygulamayı yerelde çalıştırır. Devoplus servisleri varsayılan olarak test ortamındadır (`ONE_API_BASE` ile değişir). İstekler Portal'dan geçmediği için kimlik başlıklarını (`X-One-*`) elle gönderirsiniz. |
|
|
31
|
+
| `one migrate` | `migrations/` altındaki SQL dosyalarını **yerel** veritabanına uygular (`one dev` aynı veriyi görür). |
|
|
32
|
+
| `one build` | Yükleme provası: kodu paketler, hiçbir yere göndermez (çıktı `.one/dist/`). |
|
|
33
|
+
| `one data generate [--check]` | `one.data.json` veri modelinden yeni migration + `src/data/types.ts` üretir; yalnız ekleyen değişiklik ([Veri modeli](#veri-modeli-ve-kayıt-işlemleri-d-84-️-geliştirici-onayı-bekliyor)). |
|
|
34
|
+
|
|
35
|
+
Yerel yapılandırma ve veri `.one/` altındadır, depoya girmez ve her komutta manifest'ten yeniden üretilir. Gerçek
|
|
36
|
+
yükleme Devoplus GitLab hattıyla yapılır; geliştirici makinesinden yükleme yoktur. Komutun ekrana yazdığı ayrıntılar
|
|
37
|
+
(araç adı, sürüm satırı) yerel çalışma ortamına aittir ve gizlenmez.
|
|
38
|
+
|
|
39
|
+
### Erişim talebi: `one login`, `one request`, `one requests` (D-75)
|
|
40
|
+
|
|
41
|
+
Uygulamanız bir kuruluma, DEXA sorgusuna, uygulama izni atamasına ya da yetki alanına ihtiyaç duyduğunda izni
|
|
42
|
+
yöneticiden komut satırından isteyebilirsiniz. Talep izni kendiliğinden VERMEZ; yönetici Admin Console'da onaylar ve
|
|
43
|
+
sonuç size e-postayla bildirilir.
|
|
44
|
+
|
|
45
|
+
| Komut | Ne yapar |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `one login [--env test\|uat\|prod]` | Tarayıcıda kod onayıyla giriş (portal orada seçilir). İlk girişte komut bu bilgisayarı kimlik sağlayıcıya kendisi kaydeder (yalnız erişim talebi, yayın ve kullanım panosu yetkisiyle) ve kimliği `~/.config/devoplus-one/clients.json`'da saklar; oturum `~/.config/devoplus-one/session.json`'da, ikisi de yalnız sizin okuyabileceğiniz izinle durur. Sabit bir istemci kullanmak için `--client-id` / `ONE_CLIENT_ID`. |
|
|
48
|
+
| `one request <tür> ...` | Talep gönderir; türler: `registration`, `installation`, `dexa-query`, `dexa-command`, `app-permission`, `scope`, `new-query`. `one request` yazınca türlerin alanlarını listeler. Kısa ad `one.manifest.json`'dan okunur. |
|
|
49
|
+
| `one requests [kimlik]` | Taleplerinizi ve durumlarını listeler. |
|
|
50
|
+
| `one context [--json]` | Oturumdaki portalın adını, kimliğini ve var olan kısa adını (portal anahtarı; yoksa yok der) gösterir. Kurulum talebinde portal anahtarı seçilmez; sunucu onu portalın adından belirler. |
|
|
51
|
+
| `one logout` | Oturumu kimlik sağlayıcıda iptal eder ve dosyayı siler. |
|
|
52
|
+
|
|
53
|
+
Uygulama içi izinler kayıt talebinde istenir: `one request registration --title "<ad>" --permission
|
|
54
|
+
One.Hello.Manage="Yönetir"` (tekrar edilebilir, en çok 16; İngilizce ad `--permission-en <kod>="<ad>"`). Kayıt talebini
|
|
55
|
+
portalın şirket yöneticisi, yetki alanı talebini platform yöneticisi onaylar. Kayıt talebi dışındaki türleri yalnız uygulamanın sahibi gönderebilir. Vekalet ya da Yerine Görüntüle oturumuyla talep
|
|
56
|
+
gönderilemez. Aynı istemci Devoplus One MCP sunucusunda da kullanılır (`@devoplus/one-sdk/access-request`).
|
|
57
|
+
|
|
58
|
+
### Yayın ve kullanım panosu: `one publish`, `one promote`, `one rollback`, `one deployments`, `one status` (D-86, D-87)
|
|
59
|
+
|
|
60
|
+
Kod yine bu bilgisayardan yüklenmez: komut Devoplus'a bir **yayın isteği** bırakır, Devoplus isteği kaydeder ve yayın
|
|
61
|
+
hattını kendisi başlatır; hattın kapılarının hiçbiri atlanmaz. Komutlar `one login` oturumuyla çalışır; uygulamanın
|
|
62
|
+
kısa adı `one.manifest.json`'dan okunur (`--app` ile değişir), `--json` ham yanıtı basar.
|
|
63
|
+
|
|
64
|
+
| Komut | Ne yapar |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `one publish [--portal <anahtar>]` | Test ortamına yayın. Hat bitince uygulama test Portal'ında açılır; adresi (`https://<kısa-ad>-<anahtar>.test-one.devoplus.dev`) paylaşılabilir önizlemedir. |
|
|
67
|
+
| `one promote [--portal <anahtar>]` | Sürümü bir sonraki ortama taşır (sıra: test, uat, canlı); terfi kapısı bir önceki ortamda doğrulanmamış sürümü durdurur. |
|
|
68
|
+
| `one rollback [--portal <anahtar>] [--to <x.y.z>] --yes` | Önceki sürüme (ya da verilen sürüme) döner. Kod `v<x.y.z>` git etiketinden alınır, bu yüzden yayına giren sürümleri etiketleyin; `--to` yoksa dalın ucundaki sürümden küçük en büyük etiket. Veritabanı da o sürümün yapısına dönmek zorundaysa dönülen noktadan sonra kaydedilen veriler **kaybolur**; komut `--yes` olmadan hiçbir istek göndermez. |
|
|
69
|
+
| `one deployments [--portal <anahtar>]` | Yayın isteklerinizi ve durumlarını listeler. |
|
|
70
|
+
| `one status [--portal <anahtar>] [--days 7]` | Kullanım ve hata özeti: istek ve hata sayısı, işlemci süresi, günlük dağılım, son hata kayıtları (`logError`), başarısız arka plan işleri. Kişisel veri ve ileti gövdesi döndürülmez. |
|
|
71
|
+
|
|
72
|
+
- Yalnız uygulamanın sahibi ya da o portalın şirket yöneticisi yayın isteyebilir ve panoyu görebilir; kararı sunucu verir.
|
|
73
|
+
- `--portal` verilmezse istek portal anahtarı taşımaz ve kurulumu sunucu seçer. Kısa ad, portal anahtarı, sürüm ve gün
|
|
74
|
+
sayısı istek gönderilmeden denetlenir; tanınmayan bir seçenek (`--portl`) açık hatadır.
|
|
75
|
+
- Yanıt *"Yayın hattı bu ortamda yapılandırılmamış"* ya da *"Kullanım ve hata panosu bu ortamda yapılandırılmamış"*
|
|
76
|
+
ise o ortamda bu özellik henüz kullanılamıyor demektir.
|
|
77
|
+
- Aynı istemci Devoplus One MCP sunucusunun `one_publish`, `one_deployments` ve `one_app_health` araçlarında da
|
|
78
|
+
kullanılır (`@devoplus/one-sdk/app-ops`).
|
|
79
|
+
|
|
80
|
+
## Hızlı başlangıç
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import {
|
|
84
|
+
BackendClient, OneFiles, problemResponse, readContext, requireScope, requireUser, sendMail,
|
|
85
|
+
type OneApp, type OneDatabase, type OneFilesBinding,
|
|
86
|
+
} from '@devoplus/one-sdk'
|
|
87
|
+
|
|
88
|
+
interface Env { DB: OneDatabase; FILES: OneFilesBinding; API_BASE: string }
|
|
89
|
+
|
|
90
|
+
export default {
|
|
91
|
+
async fetch(request, env): Promise<Response> {
|
|
92
|
+
try {
|
|
93
|
+
const ctx = readContext(request.headers) // platform kapısının yazdığı X-One-* başlıkları
|
|
94
|
+
const url = new URL(request.url)
|
|
95
|
+
|
|
96
|
+
if (url.pathname === '/api/me') {
|
|
97
|
+
requireUser(ctx) // oturum yoksa 401
|
|
98
|
+
const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
|
|
99
|
+
return Response.json(await api.get('Users/me'))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (url.pathname === '/api/notify' && request.method === 'POST') {
|
|
103
|
+
const user = requireScope(ctx, 'mail.execute') // scope manifest'te ve belirteçte yoksa 403
|
|
104
|
+
const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
|
|
105
|
+
return Response.json(await sendMail(api, { toSubjects: [user.sub], subject: 'Merhaba', text: 'Deneme' }))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (url.pathname === '/api/files' && request.method === 'POST') {
|
|
109
|
+
requireUser(ctx)
|
|
110
|
+
const files = new OneFiles(env.FILES)
|
|
111
|
+
const info = await files.put(`uploads/${crypto.randomUUID()}`, await request.arrayBuffer(), {
|
|
112
|
+
contentType: request.headers.get('content-type') ?? 'application/octet-stream',
|
|
113
|
+
})
|
|
114
|
+
return Response.json(info, { status: 201 })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return new Response('Bulunamadı', { status: 404 })
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return problemResponse(error) // OneError RFC 9457 yanıtına çevrilir; bilinmeyen hata 500
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
} satisfies OneApp<Env>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Kimlik bağlamı
|
|
126
|
+
|
|
127
|
+
| Üye | Açıklama |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `readContext(headers): OneContext` | `X-One-App`, `X-One-Portal-Key`, `X-One-Portal`, (`X-One-Auth=user` ya da kuyruk işinde `job` + `X-One-Sub`, `X-One-Scope`, `X-One-Global-Admin`, `X-One-Permissions`, `X-One-Admin`). Üç kurulum başlığından biri yoksa `ONE-SDK-E-001` (500): istek dispatch üzerinden gelmemiştir. |
|
|
130
|
+
| `OneContext` | `{ app, portalKey, portalId, user: OneUser \| null, actor?: OneActor \| null, mode?, admin?, jobServices? }`. `portalId` kiracıdır (24-hex); veri her zaman buna bağlıdır. `mode`: `'user'` (kullanıcının isteği), `'job'` (kuyruk / zamanlanmış iş), `'agent'` (Devoplus ajanının araç çağrısı). `admin` = `user.admin` (oturumsuzda ve işte `false`). `jobServices`: işte manifest `jobs.services` beyanı, diğer yollarda boş. |
|
|
131
|
+
| `OneActor`, `isActingSession(ctx)` | Vekalet / Yerine Görüntüle oturumu (D-76 U3; `X-One-Actor-Sub` + `X-One-Session-Kind`): `{ sub, kind: 'delegation' \| 'impersonation' }`. `user.sub` adına çalışılan kişidir (veri onundur), `actor.sub` oturumu kullanan kişidir; "kim yaptı" kaydına bu yazılır. Kendi oturumunda `null`. ⛔ Bu oturumlar varsayılan **salt-okunurdur**: platform GET/HEAD dışındaki isteği uygulamaya ulaşmadan **403 `ONE-DSP-E-009`** ile reddeder; uygulama manifest'te `"actingSessions": "write"` beyan ederse yazma isteği geçer. |
|
|
132
|
+
| `OneUser` | `{ sub, scope: readonly string[], globalAdmin, permissions: readonly string[], admin }`. `sub` devoplus-auth kimlik uzayıdır (`UserAccount._id`), Organization Schema `User._id` DEĞİLDİR. `permissions` yalnız bu uygulamanın ad alanındaki (`One.<Uygulama>.*`) izinlerdir (D-49); `admin` = şirket ya da küresel yönetici rolü. |
|
|
133
|
+
| `requireUser(ctx): OneUser` | anonimse `ONE-SDK-E-002` (401). |
|
|
134
|
+
| `requireScope(ctx, scope): OneUser` | belirteçte scope yoksa `ONE-SDK-E-003` (403). Onaya bağlı scope'ta metin kullanıcıya izni nereden vereceğini de söyler. |
|
|
135
|
+
| `requireServiceScope(ctx, scope)` | Devoplus servisinin yerel kapısı, üç yolu ayırır (bkz. "Devoplus servisleri arka plan işinde ve ajan aracında"): kullanıcıda `requireScope`; ajan aracında süreli onay gerektiren scope'lar hiç kullanılamaz; işte kullanıcı gerekmez, scope `jobs.services`'te beyanlı olmalı. İhlal `ONE-SDK-E-003` (403); asıl karar sunucudadır. |
|
|
136
|
+
| `CONSENT_SCOPES`, `awaitsUserConsent(ctx, scope): boolean` | Kullanıcının **ayrıca ve süreli** onay vermesi gereken scope'lar (D-59): `dexa.write`, `mail.execute`, `sms.execute` ve `push.execute`. `awaitsUserConsent` scope onaya bağlıysa ve belirteçte yoksa `true` döner; ilgili düğmeyi kapatıp kullanıcıya "portaldaki İzinleri incele düğmesiyle izin verin" demek için. ⚠️ Manifest'te hiç beyan edilmemiş scope için de `true` döner (beyan uygulamanın bilgisidir). |
|
|
137
|
+
| `hasPermission(ctx, code): boolean` | uygulama izni (D-49): kullanıcıda kod varsa ya da `admin` ise `true` (backend'in yönetici baypasıyla aynı); anonimde `false`. Kod manifest `permission`/`permissions` içinde beyan edilir, atamayı portal yöneticisi Yetki Yönetimi'nde kullanıcı gruplarına (org düğümlerine) yapar. ⚠️ Ad alanı süzgeci bir kolaylıktır, gizlilik sınırı değildir: `Authorization` belirteci kullanıcının bütün izinlerini taşır. |
|
|
138
|
+
| `requirePermission(ctx, code): OneUser` | oturum yoksa 401, izin yoksa `ONE-SDK-E-004` (403). |
|
|
139
|
+
| `OneError` | `status`, `code`, `message` (kullanıcıya görünen Türkçe metin). |
|
|
140
|
+
|
|
141
|
+
Uygulama kodu `X-One-*` başlıklarını **asla kendisi yazmaz**; dispatch istemciden gelenleri siler ve belirteçten
|
|
142
|
+
yeniden kurar.
|
|
143
|
+
|
|
144
|
+
**Kullanıcı onayı (D-59, 16.10.2026).** ERP'ye veri yazma (`dexa.write`) ve kullanıcı adına e-posta/SMS/mobil bildirim
|
|
145
|
+
(`mail.execute`, `sms.execute`, `push.execute`) manifest'te beyan edilmiş ve kullanıcının yetkisi olsa bile, kullanıcı bu uygulamaya
|
|
146
|
+
**o portalda** ayrıca onay vermeden belirtece girmez. Onay bir kez verilir, **30 gün** geçerlidir; portal uygulamanın
|
|
147
|
+
üstünde bir uyarı ve "İzinleri incele" düğmesi gösterir, onay devoplus-auth ekranında verilir ve oradaki "Yetkili
|
|
148
|
+
uygulamalarım" sayfasından kaldırılır. Uygulama onay beklerken de açılır ve onay gerektirmeyen işlevleri çalışır; bu
|
|
149
|
+
yüzden bu işleri yapan rotalar `requireScope` ile korunmalı, arayüz `awaitsUserConsent` ile ilgili düğmeyi kapatıp
|
|
150
|
+
yönlendirme yazabilir. Onay kaldırılınca sonraki her belirteç hassas scope'u taşımaz (devoplus-auth onayı güçlü tutarlı
|
|
151
|
+
bir Durable Object'te tutar); daha önce verilmiş belirteç iptal edilmez, en çok ömrü kadar (10 dk) geçerli kalır.
|
|
152
|
+
|
|
153
|
+
## Portal köprüsü ve görsel dil (D-76)
|
|
154
|
+
|
|
155
|
+
Uygulama sayfası Portal'da iframe içinde açılır. Sayfa üç dosyayı **kendi** adresinden yükler; satır içi betik
|
|
156
|
+
platformda kapalıdır (`script-src 'self'`) ve `one-ci validate` satır içi ya da harici (`https://...`) betiği yayından önce
|
|
157
|
+
durdurur:
|
|
158
|
+
|
|
159
|
+
```html
|
|
160
|
+
<link rel="stylesheet" href="/one-ui.css"> <!-- görsel dil -->
|
|
161
|
+
...
|
|
162
|
+
<script src="/one-bridge.js"></script> <!-- window.one -->
|
|
163
|
+
<script src="/shell.js"></script> <!-- sayfanın kendi betiği -->
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import manifest from '../one.manifest.json' with { type: 'json' }
|
|
168
|
+
import { clientAssetResponse, navigationFromManifest, scriptResponse } from '@devoplus/one-sdk'
|
|
169
|
+
|
|
170
|
+
const NAVIGATION = navigationFromManifest(manifest) // Portal'ın çizeceği sekmeler
|
|
171
|
+
// fetch içinde:
|
|
172
|
+
const asset = clientAssetResponse(url.pathname) // /one-bridge.js ve /one-ui.css
|
|
173
|
+
if (asset) return asset
|
|
174
|
+
if (url.pathname === '/shell.js') return scriptResponse(renderShellScript(NAVIGATION))
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| Sunucu üyesi | Açıklama |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `clientAssetResponse(pathname): Response \| null` | `/one-bridge.js` (`ONE_BRIDGE_SCRIPT`, `text/javascript`), `/one-components.js` (`ONE_COMPONENTS_SCRIPT`, hazır bileşenler, D-83) ve `/one-ui.css` (`ONE_UI_CSS`, `text/css`); başka yolda `null`. |
|
|
180
|
+
| `scriptResponse(body)` | Sayfa betiği için doğru içerik türüyle yanıt. |
|
|
181
|
+
| `navigationFromManifest(manifest)` | Manifest `navigation` alanı: bozuk/yinelenen öğe atlanır, en çok `NAVIGATION_MAX_ITEMS` (12). Yol kuralı `ROUTE_PATH_RE` / `isRoutePath` (manifest şemasıyla aynı). Sekme izni (`permission`, S10) korunur; biçimi bozuk izinli öğe atlanır. |
|
|
182
|
+
| `visibleNavigation(items, access)` | Kişinin görebildiği sekmeler (`access`: `{ permissions, admin }` ya da `null` = yalnız izinsiz sekmeler); dönen öğeler izin kodu taşımaz. |
|
|
183
|
+
|
|
184
|
+
Tarayıcıdaki `window.one` (tip: `OneBridge`):
|
|
185
|
+
|
|
186
|
+
| Üye | İleti | Açıklama |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| `one.start({ navigation })` | `one:ready` + `one:navigation` | Sayfa hazır olunca **bir kez**; yükseklik bildirimini de başlatır (`one:resize`). Menü sözleşmeye uymuyorsa fırlatır. İzinli sekmeler (`permission`) erişim bildirilene kadar gönderilmez. |
|
|
189
|
+
| `one.setNavigationAccess({ permissions, admin })` | `one:navigation` | Oturumdaki kişinin uygulama izinleri (S10): görebildiği izinli sekmeler menüye eklenir (menü değiştiyse ileti yeniden gönderilir; Portal son iletiyi geçerli sayar). Yönetici her sekmeyi görür. Oturum bilgisini okuyunca çağırın (`GET /api/session`). |
|
|
190
|
+
| `one.token`, `one.onToken(fn)`, `one.fetch(yol, init)` | `one:token` | Belirteç yalnız Portal'dan gelir; `one.fetch` `Authorization: Bearer ...` ekler. |
|
|
191
|
+
| `one.context`, `one.onContext(fn)`, `one.actor` | `one:context` | `{ theme, lang, autoHeight, actor: { kind } \| null, design }`. Köprü `data-theme`'i köke yazar, tasarım değerlerini (`primary`, `primaryContrast`, `fontFamily`, `radius`; biçimi tutmayan değer yazılmaz) `--one-primary` ve benzeri değişkenlere geçirir, `autoHeight` gelince sayfanın kendi dikey taşmasını gizler (yatay taşma kaydırılabilir kalır; yatay kaydırma çubuğu bildirilen yüksekliğe eklenir). `one.actor` yalnız gösterim içindir; yazma kapısı sunucudadır. |
|
|
192
|
+
| `one.onNavigate(fn)`, `one.path` | `one:navigate` | Portal sekmesine tıklandı; görünümü değiştirin. Adres geçmiş kaydı eklenmeden güncellenir; geri bildirim gönderilmez. |
|
|
193
|
+
| `one.setRoute(yol)` | `one:route` | Uygulama kendi içinde görünüm değiştirdi; Portal adresi güncellenir. Yol `/` ile başlar, sorgu/`#` yok. |
|
|
194
|
+
| `one.setTitle(metin \| null)` | `one:title` | Alt sayfa başlığı (en çok `TITLE_MAX_LENGTH` = 120, uzunu kesilir); `null` kaldırır. |
|
|
195
|
+
| `one.toast({ severity, summary, detail? })` | `one:toast` | `success`, `info`, `warn`, `error`; Portal çizer. Düz metin. |
|
|
196
|
+
| `await one.confirm({ message, title?, severity?, acceptLabel?, rejectLabel? })` | `one:confirm`, yanıt `one:confirm-result` | `true`/`false`. `severity`: `danger`, `warn`, `info`. Portal origin'i yoksa `false`. ⚠️ Portal bu iletiyi tanımıyorsa söz çözülmez. |
|
|
197
|
+
| `await one.download({ name, mime, data })` | `one:download` | `data` ArrayBuffer (Uint8Array, Blob ve metin çevrilir); en çok `DOWNLOAD_MAX_BYTES` (25 MB). iframe'de indirme kapalıdır; Portal kaydeder. |
|
|
198
|
+
| `one.openExternal(url)` | `one:open-external` | Yalnız `https://`; Portal kullanıcıya sorup yeni sekmede açar. |
|
|
199
|
+
| `one.setDirty(bool)` | `one:dirty` | Kaydedilmemiş değişiklik; Portal ayrılmadan önce sorar. Yalnız değişince gönderilir. |
|
|
200
|
+
| `one.viewport.current`, `one.viewport.onChange(fn)` | `one:viewport` | iframe'in ekranda görünen kısmı `{ top, height }` (iframe CSS pikseli). Kendi penceresini görünen alana yerleştirmek için kullanılır. |
|
|
201
|
+
|
|
202
|
+
Bütün iletiler yalnız iframe `src`'sindeki `parent` origin'lerine gönderilir (joker yok); gelen ileti origin ve üst
|
|
203
|
+
pencere doğrulanarak alınır. Portal dışında (yerel geliştirme) ileti gönderilmez: `confirm` tarayıcının kendi sorusuna,
|
|
204
|
+
`download` tarayıcı indirmesine, `openExternal` yeni pencereye, `toast` konsola düşer.
|
|
205
|
+
|
|
206
|
+
**Görsel dil (`/one-ui.css`):** açık/koyu değişkenler (`--one-bg`, `--one-fg`, `--one-muted`, `--one-surface`,
|
|
207
|
+
`--one-border`, `--one-primary`, `--one-primary-contrast`, `--one-font-family`, `--one-radius`, durum renkleri) ve
|
|
208
|
+
sınıflar: `one-btn` (`-primary`, `-danger`, `-text`), `one-field` + `one-label` + `one-input` / `one-select` /
|
|
209
|
+
`one-textarea`, `one-table` (+ `one-table-wrap`, `one-num`), `one-card`, `one-message` (`-success`, `-info`, `-warn`,
|
|
210
|
+
`-error`), içerik sekmeleri `one-tabs` + `one-tab` (`aria-selected`), `one-row`, `one-stack`, `one-muted`, `one-empty`,
|
|
211
|
+
`one-loading`, `one-sr-only`. ⚠️ `one-tabs` içerik içi gruplama içindir; uygulama menüsünü Portal çizer.
|
|
212
|
+
|
|
213
|
+
## Hazır arayüz bileşenleri (D-83)
|
|
214
|
+
|
|
215
|
+
Liste, form, süzgeç, kişi seçimi, dosya yükleme ve grafik için hazır bileşenler `/one-components.js`'tedir. Bileşenler
|
|
216
|
+
bağımlılıksızdır, uygulamanın **kendi** adresinden yüklenir (satır içi betik gerekmez), `/one-ui.css` görsel dilini
|
|
217
|
+
kullanır, Portal'ın tema ve dil değişimini (`one:context`) kendiliğinden izler ve bütün metinleri Türkçe + İngilizce
|
|
218
|
+
taşır. Tarih `gg.aa.yyyy`, sayı ve para `tr-TR` biçimindedir. İstekler yalnız uygulamanın kendi adresine gider ve
|
|
219
|
+
belirteci köprü ekler (`one.fetch`); başka adrese istek gönderilmez.
|
|
220
|
+
|
|
221
|
+
```html
|
|
222
|
+
<link rel="stylesheet" href="/one-ui.css">
|
|
223
|
+
...
|
|
224
|
+
<one-filter for="talepler"></one-filter>
|
|
225
|
+
<one-table id="talepler" src="/api/requests" export="xlsx csv" selectable clickable caption="Talepler"></one-table>
|
|
226
|
+
...
|
|
227
|
+
<script src="/one-bridge.js"></script>
|
|
228
|
+
<script src="/one-components.js"></script> <!-- köprüden SONRA -->
|
|
229
|
+
<script src="/shell.js"></script>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
// /shell.js: kolon ve alan tanımları JavaScript özelliğidir (öznitelik değil).
|
|
234
|
+
var table = document.getElementById('talepler')
|
|
235
|
+
table.columns = [
|
|
236
|
+
{ key: 'title', label: { tr: 'Başlık', en: 'Title' } },
|
|
237
|
+
{ key: 'amount', label: { tr: 'Tutar', en: 'Amount' }, type: 'money' },
|
|
238
|
+
{ key: 'createdAt', label: { tr: 'Tarih', en: 'Date' }, type: 'date' },
|
|
239
|
+
{ key: 'status', label: { tr: 'Durum', en: 'Status' }, type: 'choice',
|
|
240
|
+
options: [{ value: 'open', label: { tr: 'Açık', en: 'Open' } }, { value: 'closed', label: { tr: 'Kapalı', en: 'Closed' } }] },
|
|
241
|
+
]
|
|
242
|
+
document.querySelector('one-filter').fields = [
|
|
243
|
+
{ key: 'q', label: { tr: 'Ara', en: 'Search' }, type: 'text' },
|
|
244
|
+
{ key: 'status', label: { tr: 'Durum', en: 'Status' }, type: 'choice', options: table.columns[3].options },
|
|
245
|
+
{ key: 'createdAt', label: { tr: 'Oluşturma', en: 'Created' }, type: 'dateRange' },
|
|
246
|
+
]
|
|
247
|
+
table.addEventListener('rowclick', function (e) { openRequest(e.detail.row.id) })
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Sunucu tarafında bir şey eklemeniz gerekmez: `clientAssetResponse(pathname)` `/one-components.js`'i de döndürür.
|
|
251
|
+
Kişi seçici için `peopleSearchHandler` ucunu sunun (aşağıda).
|
|
252
|
+
|
|
253
|
+
| Bileşen | Özellikler (JS) | Öznitelikler | Olaylar |
|
|
254
|
+
|---|---|---|---|
|
|
255
|
+
| `<one-table>` | `columns` (`OneTableColumn[]`), `rows` (yerel veri), `filters` (ek sorgu parametreleri), `emptyText`, `selection` (salt okunur), `reload()`, `applyFilter(detail)`, `clearSelection()` | `src` (uzak liste), `page-size` (varsayılan 20, en çok 100), `paging="page"`, `selectable` (`single` ya da çoklu), `clickable`, `export="xlsx csv"`, `export-name`, `caption`, `row-key` (varsayılan `id`), `no-search`, `lang` | `rowclick` `{ row, index }`; `selectionchange` `{ rows, keys }`; `load` `{ items, nextCursor }` |
|
|
256
|
+
| `<one-form>` | `fields` (`OneFormField[]`), `value` (atamak "kaydedilmiş" hal), `submitLabel`, `dirty`, `validate()`, `setErrors(errors, detail?)`, `reset()` | `action` (aynı adres; JSON gönderir), `method` (`POST`/`PUT`/`PATCH`), `no-cancel`, `acting-write`, `lang` | `save` `{ value }` (iptal edilebilir); `saved` `{ value, response }`; `savefailed` `{ status, detail }`; `cancel`; `dirtychange` `{ dirty }` |
|
|
257
|
+
| `<one-filter>` | `fields` (`OneFilterField[]`), `value`, `clear()` | `for` (tablo kimliği), `lang` | `change` `{ values, params }` |
|
|
258
|
+
| `<one-person-picker>` | `value` (`OnePerson` / dizi / `null`), `label` | `src` (varsayılan `/api/one/people`), `multiple`, `required`, `disabled`, `label`, `lang` | `change` `{ value }` |
|
|
259
|
+
| `<one-file-upload>` | `value` (`OneUploadedFile` / dizi / `null`), `label` | `action` (varsayılan `/api/files`), `accept`, `max-size` (bayt, varsayılan 5 MB), `multiple`, `required`, `disabled`, `name-header` (varsayılan `x-file-name`), `label`, `lang` | `uploaded` `{ file }`; `change` `{ value }` |
|
|
260
|
+
| `<one-chart>` | `data` (`{ labels, series: [{ name, values }] }` ya da `[{ label, value }]`), `label` | `type` (`bar`, `line`, `pie`), `label`, `unit`, `lang` | Yok |
|
|
261
|
+
|
|
262
|
+
**Alan türleri** (`OneFieldType`, veri modeliyle aynı küme): `text`, `longText`, `number`, `money`, `date`,
|
|
263
|
+
`datetime`, `boolean`, `choice`, `person`, `file`. Etiketler ve seçenek adları `{ tr, en }` ya da düz metin olur;
|
|
264
|
+
hepsi DÜZ METİNDİR (HTML yorumlanmaz). Kolonun `format(value, row, lang)` işlevi de düz metin döndürür.
|
|
265
|
+
|
|
266
|
+
**Uzak liste sözleşmesi** (`<one-table src>`): `GET <src>?limit=&cursor=&sort=&order=asc|desc&q=&<alan>=...` isteğine
|
|
267
|
+
`{ items: [...], nextCursor: "..." | null }` biçiminde yanıt verilir. Sonraki sayfa `nextCursor`'la istenir, önceki sayfaya bileşen kendi
|
|
268
|
+
tuttuğu imleçle döner. `paging="page"` ile istek `page=` taşır ve yanıttaki `total` sayfa sayısını gösterir.
|
|
269
|
+
Süzgeç değerleri `<one-filter>`'dan parametre olarak gelir: metin için `q`, seçim için alan adı, tarih aralığı için
|
|
270
|
+
`<alan>From` / `<alan>To` (başka ad gerekiyorsa alanda `fromParam` / `toParam`). **Dışa aktarma** aynı adresi
|
|
271
|
+
`format=xlsx` ya da `format=csv` ile, sayfalama parametresi OLMADAN çağırır ve gelen dosyayı köprünün
|
|
272
|
+
`one.download`'ıyla indirir (ad `Content-Disposition`'dan, yoksa `export-name`).
|
|
273
|
+
|
|
274
|
+
**Form:** istemci doğrulaması (zorunlu, en az/en çok karakter, sayı aralığı, biçim, tarih, seçenek) Türkçe ve İngilizce
|
|
275
|
+
iletilerle; hatalı alan `aria-invalid` alır ve odak ilk hataya gider. Sunucu 4xx yanıtındaki RFC 9457 `errors` alanı
|
|
276
|
+
(`{ "alan": "ileti" | ["ileti"] }` ya da `[{ "field" | "pointer": "#/alan", "detail": "..." }]`) alanlara bağlanır;
|
|
277
|
+
eşleşmeyen iletiler ve `detail` formun üstünde gösterilir. Kaydedilmemiş değişiklik köprüye bildirilir
|
|
278
|
+
(`one.setDirty`); Vazgeç değişiklik varsa Portal'da onay sorar. Başkası adına açılmış oturumda (Vekalet / Yerine
|
|
279
|
+
Görüntüle) Kaydet kapalıdır; manifest `actingSessions: "write"` beyan eden uygulama forma `acting-write` yazar.
|
|
280
|
+
|
|
281
|
+
**Kişi araması:** bileşen uygulamanın kendi ucuna gider; ucu SDK hazırlar:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { PEOPLE_SEARCH_PATH, peopleSearchHandler } from '@devoplus/one-sdk'
|
|
285
|
+
// fetch içinde (manifest scopes: "orgschema.read"):
|
|
286
|
+
if (pathname === PEOPLE_SEARCH_PATH && method === 'GET') return peopleSearchHandler(request, env, ctx)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`GET /api/one/people?q=&lang=` yanıtı `{ items: [{ id, name, title, department }] }` biçimindedir: yalnız aktif kişiler, en az 2 en çok
|
|
290
|
+
100 karakterlik arama, en çok 20 sonuç, Türkçe harf duyarsız. Yanıt **yalnız** ad, unvan ve departman taşır; e-posta,
|
|
291
|
+
telefon, kullanıcı kodu ve fotoğraf DÖNMEZ (veri en azaltma). ⚠️ `id` Organization Schema kimliğidir (`sub` DEĞİL);
|
|
292
|
+
bildirim gönderirken `toOrgUserIds` alanına yazın. Şema her aramada okunur; `options.loadSchema` ile kendi önbelleğinizi
|
|
293
|
+
(`OneCache`) bağlayabilirsiniz.
|
|
294
|
+
|
|
295
|
+
**Dosya yükleme** iskeletin dosya ucuyla uyumludur (`POST /api/files`, gövde dosyanın kendisi, ad `x-file-name`
|
|
296
|
+
başlığında; başlık yalnız ASCII taşıdığı için Türkçe harfler karşılığına çevrilir). Tür ve boyut sınırı yüklemeden önce
|
|
297
|
+
tarayıcıda denetlenir; sunucu yine kendi sınırını uygular. Kaldır düğmesi yalnız değeri değiştirir, dosyayı silmez.
|
|
298
|
+
|
|
299
|
+
**Grafik** SVG'dir: `role="img"` + `aria-label`, ekran okuyucu için gizli veri tablosu, renkler `--one-chart-1` ile
|
|
300
|
+
`--one-chart-6` arasındaki değişkenlerden (açık/koyu).
|
|
301
|
+
|
|
302
|
+
| Sabit | Değer |
|
|
303
|
+
|---|---|
|
|
304
|
+
| `COMPONENTS_SCRIPT_PATH` | `/one-components.js` (`ONE_COMPONENTS_SCRIPT`) |
|
|
305
|
+
| `ONE_COMPONENT_TAGS` | altı etiket adı |
|
|
306
|
+
| `PEOPLE_SEARCH_PATH`, `PEOPLE_SEARCH_MIN_QUERY`, `PEOPLE_SEARCH_MAX_QUERY`, `PEOPLE_SEARCH_MAX_RESULTS` | `/api/one/people`, 2, 100, 20 |
|
|
307
|
+
| `FILE_UPLOAD_DEFAULT_ACTION`, `FILE_UPLOAD_DEFAULT_MAX_BYTES` | `/api/files`, 5 MB |
|
|
308
|
+
| `TABLE_DEFAULT_PAGE_SIZE`, `TABLE_MAX_PAGE_SIZE` | 20, 100 |
|
|
309
|
+
|
|
310
|
+
Tipler: `OneTableColumn`, `OneTableElement`, `OneListResponse`, `OneFormField`, `OneFormElement`, `OneFormErrors`,
|
|
311
|
+
`OneFilterField`, `OneFilterElement`, `OneFilterChange`, `OnePerson`, `OnePersonPickerElement`, `OneUploadedFile`,
|
|
312
|
+
`OneFileUploadElement`, `OneChartData`, `OneChartSeries`, `OneChartElement`, `OneChoiceOption`, `OneLocalizedText`,
|
|
313
|
+
`OneFieldType`, `OneComponentEventMap`; sunucu: `peopleSearchHandler`, `searchPeople` (saf), `foldPersonText`,
|
|
314
|
+
`PeopleSearchItem`, `PeopleSearchEnv`, `PeopleSearchOptions`.
|
|
315
|
+
|
|
316
|
+
## Backend istemcisi
|
|
317
|
+
|
|
318
|
+
`new BackendClient({ baseUrl, authorization })`: `baseUrl` ortamın API tabanıdır (uygulama `vars`'ından gelir ve
|
|
319
|
+
outbound allowlist'inde olmak zorundadır); `authorization` tarayıcıdan gelen `Authorization: Bearer <app_token>`
|
|
320
|
+
başlığıdır ve olduğu gibi iletilir.
|
|
321
|
+
|
|
322
|
+
| Metot | Davranış |
|
|
323
|
+
|---|---|
|
|
324
|
+
| `request<T>(path, init?)` | `api/v1/<path>`; `accept: application/json`; gövde varsa `content-type: application/json`; `init.headers` olduğu gibi eklenir (outbound yalnız `X-One-*` başlıklarını siler); `204` yanıtında `undefined` döner; `!ok` olursa `OneError(status, problem.code ?? HTTP-<status>, problem.detail ?? title)` fırlatır. Yanıtta `Retry-After` (saniye) ya da gövdede `retryAfterSeconds` varsa `OneError.retryAfterSeconds` olarak taşınır. |
|
|
325
|
+
| `get<T>(path)`, `post<T>(path, body)` | kısayollar. |
|
|
326
|
+
|
|
327
|
+
Backend "kullanıcı yetkisi ∩ scope" kuralını kendisi uygular; SDK ek bir yetki vermez.
|
|
328
|
+
|
|
329
|
+
## Dosya servisi
|
|
330
|
+
|
|
331
|
+
Manifest'te `services.files: true` ise platform kurulum başına bir dosya deposu açar ve `env.FILES`'a bağlar (D-32).
|
|
332
|
+
İzolasyon depo sınırındadır: anahtara `portalId` eklenmez, başka kurulumun deposu görünmez.
|
|
333
|
+
|
|
334
|
+
| Üye | Açıklama |
|
|
335
|
+
|---|---|
|
|
336
|
+
| `new OneFiles(env.FILES)` | |
|
|
337
|
+
| `put(key, body, { contentType?, maxBytes?, metadata? })` | `FileInfo`; gövde `ArrayBuffer \| ArrayBufferView \| string \| Blob` (akış KABUL EDİLMEZ); tavan `FILE_MAX_BYTES` (25 MiB), aşımda `ONE-SDK-E-014` (413). |
|
|
338
|
+
| `get(key)` | `OneStoredFileBody \| null`; dosyayı uygulamanın kendisi sunar (`writeHttpMetadata(headers)` + `httpEtag`). |
|
|
339
|
+
| `head(key)`, `delete(key)`, `list({ prefix?, limit?, cursor? })` | `limit` 1-1000. |
|
|
340
|
+
| `normalizeFileKey(raw)` | 1-512 karakter **ve** UTF-8 ile en çok 1.024 bayt (depo sınırı, `FILE_KEY_MAX_BYTES`), baştaki `/` atılır; boş / `.` / `..` parçası ve kontrol karakteri reddedilir (`ONE-SDK-E-010...012`). |
|
|
341
|
+
| `sanitizeFileName(raw, maxLength = 120)` | kullanıcı dosya adını tek parçaya indirger (`ONE-SDK-E-013`). |
|
|
342
|
+
| `readBoundedBody(request, maxBytes)` | HTTP isteğinin gövdesini **sınırlı** okur: `Content-Length` varsa tek bayt okunmadan sınanır, yoksa (parçalı aktarım) akış parça parça okunur ve sınır aşıldığı an kesilir (`ONE-SDK-E-014`, 413); bozuk `Content-Length` için hata `ONE-SDK-E-016` (400) olur. `request.arrayBuffer()` gövdenin tamamını belleğe aldıktan SONRA sınar, bu da uygulamanın belleğini taşırabilir; yükleme ucunda bunu kullanın. |
|
|
343
|
+
|
|
344
|
+
## Önbellek servisi
|
|
345
|
+
|
|
346
|
+
Manifest'te `services.cache: true` ise platform kurulum başına bir önbellek açar ve `env.CACHE`'e bağlar (D-35). İzolasyon
|
|
347
|
+
önbellek sınırındadır. Bu bir **önbellektir**: nihai tutarlıdır (yazma başka bölgelerde 60 sn'ye kadar gecikebilir).
|
|
348
|
+
Sayaç, kilit ya da kalıcı kayıt için kullanılmaz; kalıcı veri veritabanındadır.
|
|
349
|
+
|
|
350
|
+
| Üye | Açıklama |
|
|
351
|
+
|---|---|
|
|
352
|
+
| `new OneCache(env.CACHE)` | |
|
|
353
|
+
| `get<T>(key)` | `T \| null`; değer yazıldığı JSON şeklinde döner (bozuk kayıt fırlatmaz, `null`). |
|
|
354
|
+
| `put(key, value, { ttlSeconds })` | TTL **zorunlu**, tam sayı, `CACHE_MIN_TTL_SECONDS` (60) ile `CACHE_MAX_TTL_SECONDS` (30 gün) arasında; sınır dışı değer `ONE-SDK-E-022`, sessizce kırpılmaz. Değer `JSON.stringify` ile yazılır; `undefined`, üst düzey `null` (okumada "anahtar yok" ile ayırt edilemezdi) ve çevrilemeyen değer `ONE-SDK-E-023`; `CACHE_VALUE_MAX_BYTES` (1 MiB) üstü `ONE-SDK-E-024` (413). |
|
|
355
|
+
| `delete(key)` | |
|
|
356
|
+
| `normalizeCacheKey(raw)` | kırpılır; boş / 512 bayttan uzun / tam olarak `.` ya da `..` (önbelleğin ayırdığı anahtarlar; `ONE-SDK-E-020`) ya da kontrol karakterli (`ONE-SDK-E-021`) anahtar reddedilir. |
|
|
357
|
+
|
|
358
|
+
## İletişim servisi (e-posta / SMS / mobil bildirim)
|
|
359
|
+
|
|
360
|
+
`sendMail(api, { toSubjects, subject, text })` `POST One/services/mail` ucunu, `sendSms(api, { toSubjects, text })`
|
|
361
|
+
`POST One/services/sms` ucunu, `sendPush(api, { toSubjects, title, text })` `POST One/services/push` ucunu çağırır (aşağıda). Alıcılar devoplus-auth `sub` değerleridir; adres/telefon **sunucuda** ve yalnız aynı portalın
|
|
362
|
+
aktif üyeleri için çözülür; serbest adres/numara kabul edilmez. Scope `mail.execute` / `sms.execute` hem kimlik
|
|
363
|
+
sağlayıcı kaydında hem platform kayıt defterinde tanımlı olmalıdır. Yanıt `NotifyResult { queued, skipped[{ sub,
|
|
364
|
+
reason }] }`; `reason` değerleri: `invalid-subject`, `not-in-portal`, `inactive`, `no-address`, `no-phone`,
|
|
365
|
+
`preference`, `rate-limited`, `invalid-org-user`, `not-deliverable` (yalnız push). Kurulum başına saatlik tavan vardır (varsayılan e-posta 50, SMS 20, push 100). Backend **429**
|
|
366
|
+
yalnız hiçbir ileti kuyruğa girmez **ve** atlananların tamamı `rate-limited` ise döner; başka gerekçeyle hiç kuyruğa
|
|
367
|
+
girmeyen istek **200** + `queued: 0` alır. Sonucu `skipped[].reason` ile denetleyin (backend `OneServiceHandlers`,
|
|
368
|
+
okundu 15.10.2026).
|
|
369
|
+
|
|
370
|
+
**Org kimliğiyle alıcı (D-47):** `toOrgUserIds` alanı org şemasındaki kullanıcı kimliğini (legacy `User._id`, 24-hex) kabul eder. Yönetici zinciri ve grup üyeleri bu uzayda geldiği için onlara `sub` bulmadan bildirim gönderilebilir. `toSubjects` ile birlikte verilebilir; toplam alıcı en çok 50'dir, aynı kişi iki kez sayılmaz. Biçimi bozuk org kimliği `invalid-org-user`, portalın aktif kullanıcısı olmayan kimlik `not-in-portal` ile atlanır. ⛔ İki kimlik uzayını karıştırmayın: `ctx.user.sub` org kimliği DEĞİLDİR (ikisi de 24-hex; karışırsa hata vermez, kişi `not-in-portal` ile sessizce atlanır).
|
|
371
|
+
|
|
372
|
+
### Mobil bildirim (D-55; ⚠️ geliştirici onayı bekliyor)
|
|
373
|
+
|
|
374
|
+
`sendPush(api, { toSubjects, toOrgUserIds?, title, text })`, `POST One/services/push` ucunu çağırır; scope `push.execute`. Devoplus
|
|
375
|
+
Portal mobil uygulamasına **yalnız başlık + metin** gider (başlık 1-100, metin 1-300 karakter; bu sınırı sunucu uygular, SDK
|
|
376
|
+
`sendPush` istemci denetimi yapmaz, hazır senaryolar `toPush` ile kırpar). Derin bağlantı ya da veri yükü **yoktur**
|
|
377
|
+
(mobil uygulamanın okuduğu veri anahtarı ölçülmedi). Alıcı kuralları e-posta/SMS ile aynıdır; yeni atlama gerekçesi
|
|
378
|
+
`not-deliverable` = kişinin bildirim tercihi kapalı **ya da** mobil kimliği çözülemedi (sunucu ikisini ayırt edemez).
|
|
379
|
+
⚠️ Push bugün **hiçbir ortamda açık değildir**: uç kuyruğa almadan `OneError` `ONE-APP-E-029` ile reddeder (kapalı
|
|
380
|
+
kanal sessiz başarı gibi görünmesin). Kodunuz bu hatada çalışmayı sürdürmeli; `notify` kanalları bağımsız gönderir.
|
|
381
|
+
|
|
382
|
+
### Hazır bildirim senaryoları (D-47)
|
|
383
|
+
|
|
384
|
+
`approvalRequest`, `approvalResult`, `reminder`, `systemNotice`, `operationResult`, `verificationCode` aynı girdiden e-posta konusu + gövdesi ve SMS metni üretir (`ScenarioMessage { subject, text, smsText }`; Türkçe resmi "siz" ya da `lang: 'en'`). Metinler backend sınırlarına göre kırpılır (konu ≤ 200, SMS ≤ 300; e-posta gövdesi 10.000'i aşarsa `ONE-SDK-E-062`). Bağlantı yalnız `https` olabilir ve kullanıcı adı/parola taşıyamaz (`normalizeLink`, `ONE-SDK-E-061`). `notify(api, message, { toSubjects?, toOrgUserIds?, channels })` kanalları (`mail`, `sms`, `push`) **bağımsız** gönderir: SMS ya da push düşse de e-posta gider; sonuç kanal adıyla döner, düşen kanal `{ error }` taşır. `toPush(message, to)` başlığı e-posta konusundan (≤ 100, `PUSH_TITLE_MAX_LENGTH`), metni SMS metninden (≤ 300, `PUSH_TEXT_MAX_LENGTH`) kurar; kırpma bir emojiyi ortasından bölmez.
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import { approvalChain, approvalRequest, getMyOrgContext, getOrgUsersWithManagers, notify } from '@devoplus/one-sdk'
|
|
388
|
+
|
|
389
|
+
const me = await getMyOrgContext(api) // null: kişinin bu portalda org kaydı yok
|
|
390
|
+
if (me) {
|
|
391
|
+
const users = await getOrgUsersWithManagers(api, { includeInactive: true }) // pasifler de gelsin ki atlanabilsin
|
|
392
|
+
const [first] = approvalChain(users, me.orgUserId) // işlevsel yönetici zinciri, pasifler atlanır
|
|
393
|
+
if (first) {
|
|
394
|
+
const message = approvalRequest({ appName: 'İzin Talepleri', item: '3 günlük yıllık izin', requester: me.fullName ?? '', link })
|
|
395
|
+
await notify(api, message, { toOrgUserIds: [first], channels: ['mail'] })
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Doğrulama kodu (D-47)
|
|
401
|
+
|
|
402
|
+
`generateVerificationCode(digits = 6)` (4-10 hane, yanlılıksız); `generateSalt()`; `hashVerificationCode({ code, salt, purpose, sub })`; `verificationCodeMatches({ ..., hash })` (sabit süreli karşılaştırma). Özet **amaca ve kişiye** bağlıdır: başka bir işlem ya da başka bir kişi için üretilmiş kod kabul edilmez. Kodun kendisini değil `salt + hash + son kullanma + deneme sayısı`nı uygulamanın **veritabanında** saklayın (önbellek nihai tutarlıdır; deneme sayacı ve tek kullanım orada güvenilir değildir). Süreyi ve deneme tavanını (ör. 5) uygulama uygular; doğru koddan sonra kaydı silin.
|
|
403
|
+
|
|
404
|
+
## Organizasyon şeması (D-47)
|
|
405
|
+
|
|
406
|
+
Scope `orgschema.read` (salt okuma) + kullanıcının kendi `Portal.Generic.Read` izni. Veri aktif portalın aktif kayıtlarıdır.
|
|
407
|
+
|
|
408
|
+
| İşlev | Uç | Not |
|
|
409
|
+
|---|---|---|
|
|
410
|
+
| `getOrgSchema(api)` | `GET OrganizationManagement/organization-schema` | şirket, iş yeri, departman, pozisyon, ünvan, çalışan türü, kullanıcı grubu, kullanıcı |
|
|
411
|
+
| `getOrgUsersWithManagers(api, { includeInactive? })` | `GET OrganizationManagement/users` (`includeInactive` karşılığı `?hideInactiveDocuments=false`) | yönetici alanlarını (`functionalManager` / `operationalManager`) taşıyan TEK uç; boş metin = yönetici yok. Uç varsayılan olarak yalnız aktifleri döner; onay zinciri kurarken `includeInactive: true` verin, yoksa zincir pasif yöneticide kesilir. ⚠️ Yanıt iletişim bilgisi de taşır, saklamayın |
|
|
412
|
+
| `getMyOrgContext(api)` | `GET Users/me` | oturumdaki kişinin org kimliği (`orgUserId`); org kaydı yoksa `null` |
|
|
413
|
+
| `getUserGroupMembers(api, groupId)` | `GET OrganizationManagement/user-groups/{id}/users` | üyelik SUNUCUDA çözülür (yedi boyutun VEYA'sı + iç içe gruplar); istemcide türetmeyin |
|
|
414
|
+
| `getUserGroupsOfUser(api, orgUserId)` | `GET OrganizationManagement/users/{id}/user-groups` | |
|
|
415
|
+
|
|
416
|
+
Saf yardımcılar (ağ yok): `managerOf(users, orgUserId, kind?)`; `approvalChain(users, orgUserId, { kind?, maxDepth?, skipInactive? })` (döngüye ve kendine yöneticiye karşı güvenli; pasif yönetici varsayılan olarak atlanır ve zincir onun yöneticisinden sürer; derinlik varsayılan 5, en çok 20); `approvalChainReport(...)` (aynı zincir ve durma nedeni: `top`, `depth`, `unknown` ya da `cycle`; boş zinciri "onaycı yok" diye okumadan önce bakın); `orgUsersMatching(schema, criteria)` (verilen boyutların hepsine uyan aktif kullanıcılar; ölçütsüz çağrı `ONE-SDK-E-051`); `localizedText(text, lang?)`. Şemada departman/pozisyon hiyerarşisi alanı **yoktur**; zincir yalnız kişi kayıtlarındaki yönetici alanlarından kurulur.
|
|
417
|
+
|
|
418
|
+
## DEXA (ERP okuma) servisi
|
|
419
|
+
|
|
420
|
+
`queryDexa(api, key, parameters?)`, `POST One/services/dexa/query` ucunu çağırır. Uygulama ERP verisini **yalnız önceden tanımlı
|
|
421
|
+
QueryStore anahtarlarıyla** okur (D-09; uç D-42, ⚠️ geliştirici onayı bekliyor): serbest SQL, ERP'ye yazan anahtar ya
|
|
422
|
+
da desteklenmeyen sorgu türü çalıştırılamaz. Hangi anahtarın çalışabileceğinin **tek kaynağı platform kayıt
|
|
423
|
+
defteridir**: anahtarları Admin Console'da uygulamanın kaydına yönetici ekler; kayıtta olmayan anahtar 403
|
|
424
|
+
`ONE-APP-E-024` alır. Manifest `services.dexa.queryKeys` yalnız **beyandır** (çalışma anında okunmaz) ve `scopes`
|
|
425
|
+
içinde `dexa.read` ile birlikte yazılır; biri eksikse manifest doğrulaması düşer. Belirteçte `dexa.read` yoksa
|
|
426
|
+
backend 403 `ONE-APP-E-017` döner.
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { BackendClient, queryDexa, requireScope } from '@devoplus/one-sdk'
|
|
430
|
+
|
|
431
|
+
requireScope(ctx, 'dexa.read')
|
|
432
|
+
const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
|
|
433
|
+
// Anahtar ve parametre adları sorgu tanımından (QueryStore) gelir; buradakiler yalnız örnektir.
|
|
434
|
+
const result = await queryDexa(api, 'Ornek.Rapor', { StartDate: '2026-10-01' })
|
|
435
|
+
|
|
436
|
+
if (result.unavailable) {
|
|
437
|
+
// Sorgu ÇALIŞAMADI (DEXA tüneli kurulmamış / ERP'ye ulaşılamıyor / sorgu düştü): boş liste "kayıt yok" DEĞİLDİR.
|
|
438
|
+
return Response.json({ status: 'unavailable', message: 'Veriler şu anda getirilemiyor; lütfen daha sonra deneyin.' }, { status: 503 })
|
|
439
|
+
}
|
|
440
|
+
return Response.json({
|
|
441
|
+
columns: result.columns,
|
|
442
|
+
rows: result.rows, // boşsa gerçekten kayıt yoktur
|
|
443
|
+
truncated: result.truncated, // true: sunucu sonucu 5.000 satırda kesti, süzgeci daraltın
|
|
444
|
+
})
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
| Alan | Anlam |
|
|
448
|
+
|---|---|
|
|
449
|
+
| `columns: string[]` | Kolon adları (sıra korunur). |
|
|
450
|
+
| `rows: Array<Record<string, unknown>>` | Satırlar (kolon adı: değer). |
|
|
451
|
+
| `unavailable: boolean` | `true` = sorgu çalışamadı; kullanıcıya "şu anda getirilemiyor" gösterin, "kayıt yok" değil. Sunucu **200** döner, SDK **fırlatmaz**. Yanıt gövdesi hiç nesne değilse (boş/bozuk) SDK da `true` sayar. |
|
|
452
|
+
| `truncated: boolean` | `true` = sunucu sonucu 5.000 satırda kesti; liste eksiktir. |
|
|
453
|
+
|
|
454
|
+
Eksik dizi `[]`, eksik bayrak `false` olarak okunur (`normalizeDexaQueryResult`). İstemci tarafı kurallar sunucuyla
|
|
455
|
+
birebirdir ve istek **gönderilmeden** fırlatır (`buildDexaQueryRequest`; programlama hatası):
|
|
456
|
+
|
|
457
|
+
| Kural | Sabit | Hata |
|
|
458
|
+
|---|---|---|
|
|
459
|
+
| Anahtar `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`, harf duyarlı | `DEXA_KEY_RE` | `ONE-SDK-E-040` |
|
|
460
|
+
| Parametreler `{ ad: metin }` nesnesi, en çok 32 | `DEXA_MAX_PARAMETERS` | `ONE-SDK-E-041` |
|
|
461
|
+
| Ad `^[A-Za-z_][A-Za-z0-9_]{0,63}$` | `DEXA_PARAMETER_NAME_RE` | `ONE-SDK-E-042` |
|
|
462
|
+
| `LangCode` gönderilemez: platformun bağlam parametresidir, sunucu doldurur; denetim **harf duyarsızdır** (`langcode` da reddedilir) | `DEXA_RESERVED_PARAMETERS` | `ONE-SDK-E-043` |
|
|
463
|
+
| `UserCode`, `CompanyCode` ve `CostCenterCode` gönderilebilir ama **boş olamaz** (boş dize ya da yalnız boşluk; ad harf duyarsız tanınır) | `DEXA_APP_CONTEXT_PARAMETERS` | `ONE-SDK-E-140` |
|
|
464
|
+
| Değer metin, en çok 2.000 karakter (sayı/tarih `String(...)` ile) | `DEXA_PARAMETER_VALUE_MAX_LENGTH` | `ONE-SDK-E-044` |
|
|
465
|
+
|
|
466
|
+
Sunucu hataları `OneError.code` ile taşınır: 403 `ONE-APP-E-011...017` (kapı; `-017` = `dexa.read` yok); 403
|
|
467
|
+
`ONE-APP-E-024` (anahtar kayıt defterinde yok); 400 `ONE-APP-E-025` (anahtar çalıştırılamaz: katalogda yok / ERP'ye
|
|
468
|
+
yazan anahtar / desteklenmeyen sorgu türü); 400 `ONE-APP-E-026` (parametre izinli değil: sorgu tanımlamıyor,
|
|
469
|
+
`LangCode`, ya da kurulumda onay yokken gönderilen bağlam kodu); 403 `ONE-APP-E-027` (sorgu kişiye bağlı bir bağlam,
|
|
470
|
+
`UserCode` ya da `CompanyCode`, istiyor ve oturumdaki kişiden çözülemedi; sorgu ÇALIŞTIRILMAZ, boş değerle "tüm
|
|
471
|
+
kayıtlar"a dönmez; oturumdan çözülen bağlam değeri boş kaldığında da bu kod döner); 429 `ONE-APP-E-018` (kurulum
|
|
472
|
+
başına saatte 600 sorgu); 400 `ONE-APP-E-076` (gönderilen bağlam kodu geçersiz: boş, 64 karakterden uzun ya da kontrol
|
|
473
|
+
karakteri içeriyor); 403 `ONE-APP-E-077` (gönderilen `UserCode` ya da `CompanyCode` kurulumun portalında tanımlı değil);
|
|
474
|
+
400 `ONE-APP-E-078` (arka plan işinde sorgu bir bağlam kodu istiyor ve uygulama onu göndermedi).
|
|
475
|
+
|
|
476
|
+
### Bağlam kodları: `UserCode`, `CompanyCode`, `CostCenterCode` (C7)
|
|
477
|
+
|
|
478
|
+
Sorgu bu kodlardan birini bildiriyorsa değer şu sırayla belirlenir:
|
|
479
|
+
|
|
480
|
+
| Durum | Uygulama kodu gönderdi | Uygulama kodu göndermedi |
|
|
481
|
+
|---|---|---|
|
|
482
|
+
| Kurulumda yönetici **"kodları uygulama belirler"** onayını vermedi | 400 `ONE-APP-E-026` ile reddedilir | kişi / ajan kipinde oturumdaki kişiden çözülür; arka plan işinde reddedilir |
|
|
483
|
+
| Onay verildi, kişi (Portal) ya da ajan kipi | sunucu doğrular, gönderilen kullanılır | oturumdaki kişiden çözülür |
|
|
484
|
+
| Onay verildi, arka plan işi | sunucu doğrular, gönderilen kullanılır | çözülecek oturum olmadığı için **reddedilir** |
|
|
485
|
+
|
|
486
|
+
- **Portal doğrulaması:** `UserCode` kurulumun portalındaki **aktif** bir kişinin kodu, `CompanyCode` aynı portaldaki bir
|
|
487
|
+
şirketin kodu olmalıdır; değilse sorgu ERP'ye gitmeden reddedilir (`ONE-APP-E-077`). `CostCenterCode` için portalda
|
|
488
|
+
karşılaştırılacak bir kayıt yoktur: yalnız biçimi denetlenir (boş değil, en çok 64 karakter, kontrol karakteri yok). Uygulamanın gönderdiği kodla yapılan her çağrı
|
|
489
|
+
denetim günlüğüne yazılır.
|
|
490
|
+
- **Boş değer ERP'ye gitmez:** gönderilen ya da oturumdan çözülen değer boşsa sorgu çalışmaz. SDK boş dizeyi ve yalnız
|
|
491
|
+
boşluğu istek göndermeden reddeder (`ONE-SDK-E-140`). Kodu bilmiyorsanız parametreyi **hiç göndermeyin**.
|
|
492
|
+
- **`CostCenterCode` artık açıktır:** onu bildiren sorgular çalışır. Uygulama göndermezse değeri sunucu kişiden çözer;
|
|
493
|
+
çözülemezse sorgu reddedilir.
|
|
494
|
+
- **`LangCode` platformundur:** uygulama hiçbir kipte gönderemez (`ONE-SDK-E-043`).
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
// Arka plan işinde (oturum yok) kişiye bağlı bir sorgu: kodu uygulama verir. Kurulumda onay yoksa sunucu reddeder.
|
|
498
|
+
const stock = await queryDexa(api, 'Ornek.MagazaStok', { CompanyCode: '01', StoreCode: storeCode })
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
## DEXA işlemleri: ERP'ye veri yazma (D-56; ⚠️ geliştirici onayı bekliyor)
|
|
502
|
+
|
|
503
|
+
`runDexaCommand(api, key, parameters, { idempotencyKey })`, `POST One/services/dexa/command` ucunu `Idempotency-Key`
|
|
504
|
+
başlığıyla çağırır; `getDexaCommandStatus(api, idempotencyKey)` ise `GET One/services/dexa/command/{anahtar}` ucunu. Scope **`dexa.write`**.
|
|
505
|
+
Okuma sorgularından (`queryDexa`) ayrıdır: bu uç ERP'ye **VERİ YAZAR**. Çalışması dört kapının kesişimidir:
|
|
506
|
+
|
|
507
|
+
1. Belirteçte `dexa.write` (yoksa 403 `ONE-APP-E-017`).
|
|
508
|
+
2. Anahtar uygulamanın kayıt defterindeki işlem izin listesinde (`dexaCommandKeys`, Admin Console; yoksa 403 `ONE-APP-E-030`).
|
|
509
|
+
3. Anahtar koddaki ERP komut kataloğunda **One uygulamalarına açık** (yoksa 404 `ERPCMD-APP-E-006`; açmak backend kod
|
|
510
|
+
değişikliğidir; ⚠️ bugün açık hiçbir işlem yoktur).
|
|
511
|
+
4. Kullanıcının o işleme özgü izni (yoksa 403 `ERPCMD-APP-E-0xx`).
|
|
512
|
+
|
|
513
|
+
Manifest `services.dexa.commandKeys` yalnız **beyandır** ve `scopes` içinde `dexa.write` ile birlikte yazılır.
|
|
514
|
+
|
|
515
|
+
⛔ **Idempotency (kod yazmadan önce okuyun):**
|
|
516
|
+
|
|
517
|
+
| Durum | Ne yapılır |
|
|
518
|
+
|---|---|
|
|
519
|
+
| Yeni kullanıcı eylemi ("Kaydet"e basış) | `newIdempotencyKey()` ile anahtar üretin ve isteği göndermeden ÖNCE veritabanına yazın |
|
|
520
|
+
| Ağ hatası / zaman aşımı | AYNI anahtarla tekrar deneyin ya da `getDexaCommandStatus` ile okuyun; ERP'ye ikinci kez yazılmaz (`replayed: true`) |
|
|
521
|
+
| 409 `ERPCMD-APP-E-016` (aynı işlem sürüyor) | `error.retryAfterSeconds` kadar bekleyin, durumu okuyun ya da AYNI anahtarla deneyin. Hatayı `problemResponse(error)` ile tarayıcıya döndürürseniz yanıt bu süreyi `Retry-After` başlığında ve gövdedeki `retryAfterSeconds` alanında taşır (yalnız pozitif tam sayıysa) |
|
|
522
|
+
| 424 `ERPCMD-APP-E-013` (ERP bağlantısı kurulamadı) | hiçbir şey gönderilmedi; AYNI anahtarla tekrar denenebilir |
|
|
523
|
+
| 409 `ERPCMD-APP-E-015` (anahtar farklı içerikle kullanılmış) | programlama hatası: anahtar başka bir eylemle karışmış |
|
|
524
|
+
| 200 `outcome: 'succeeded'` | ERP kaydı oluştu (`erp.headerId` / `erp.documentNumber`) |
|
|
525
|
+
| 200 `outcome: 'rejected'` | ERP reddetti (`erp.exceptionMessage`); aynı içerikle tekrar yine reddedilir |
|
|
526
|
+
| 200 `outcome: 'inDoubt'` | ERP'ye gidip gitmediği **BİLİNMİYOR**: YENİ anahtarla **ASLA** yeniden göndermeyin (çift kayıt); durumu okuyun, kullanıcıya "sonuç doğrulanamadı" deyin |
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
import { BackendClient, getDexaCommandStatus, newIdempotencyKey, requireScope, runDexaCommand } from '@devoplus/one-sdk'
|
|
530
|
+
|
|
531
|
+
requireScope(ctx, 'dexa.write')
|
|
532
|
+
const api = new BackendClient({ baseUrl: env.API_BASE, authorization: request.headers.get('authorization') })
|
|
533
|
+
// Anahtar eylem başına BİR KEZ üretilir ve göndermeden önce saklanır (tablo ve sütun adları örnektir).
|
|
534
|
+
const idempotencyKey = newIdempotencyKey()
|
|
535
|
+
await env.DB.prepare('INSERT INTO erp_ops (id, sub, state) VALUES (?, ?, ?)').bind(idempotencyKey, user.sub, 'sent').run()
|
|
536
|
+
// İşlem anahtarı ve parametre adları QueryStore tanımından gelir; buradakiler yalnız örnektir.
|
|
537
|
+
const result = await runDexaCommand(api, 'Ornek.Islem', { Body: JSON.stringify(model) }, { idempotencyKey })
|
|
538
|
+
if (result.outcome === 'inDoubt') {
|
|
539
|
+
// Yeni anahtarla TEKRAR GÖNDERMEYİN. Durum daha sonra getDexaCommandStatus(api, idempotencyKey) ile okunur.
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
`DexaCommandResult` alanları backend `ErpCommandResponse` ile birebirdir: `queryStoreKey`, `idempotencyKey` (sunucunun
|
|
544
|
+
küçük harf biçimi), `replayed`, `outcome`, `correlationId`, `erp { shape, isSuccessful, headerId, documentNumber,
|
|
545
|
+
exceptionMessage, lines, raw }`. SDK sözleşmeye uymayan her 200 yanıtını **`inDoubt`** sayar: nesne olmayan gövde,
|
|
546
|
+
bilinmeyen `outcome`, backend kaydında nullable olmayan bir alanın eksik olması ya da herhangi bir alanın yanlış tipte
|
|
547
|
+
olması, **gönderilenden farklı `idempotencyKey`** ya da farklı `queryStoreKey` (yalnız ASCII büyük/küçük harf farkı
|
|
548
|
+
aynı anahtardır: backend katalogdaki yazımı döner). O durumda gövdeden hiçbir alan sonuca taşınmaz (anahtarlar
|
|
549
|
+
gönderilenlerdir, `correlationId: null`, `erp.shape: 'unrecognized'`); doğru sonuç AYNI anahtarla durum sorgusundan
|
|
550
|
+
okunur.
|
|
551
|
+
`getDexaCommandStatus` backend `ErpCommandStatusResponse`'u döner (`outcome` ayrıca `pending` ya da `notSent` olabilir;
|
|
552
|
+
`attemptCount`, `createdAtUtc`, `completedAtUtc`, `erp`, `steps`, `kind`, `warnings`) ya da kayıt yoksa
|
|
553
|
+
(`404 ERPCMD-APP-E-017`) **`null`** döner; o anahtarla hiçbir şey kaydedilmemiştir ve AYNI anahtarla gönderilebilir. Durum
|
|
554
|
+
yanıtı da aynı kuralla doğrulanır (komut anahtarı istekte olmadığı için `queryStoreKey` yalnız tipiyle); ayrıca
|
|
555
|
+
`succeeded`/`rejected` `erp` görünümü olmadan kabul edilmez.
|
|
556
|
+
|
|
557
|
+
İstemci tarafı kurallar istek **gönderilmeden** fırlatır (`buildDexaCommandRequest` / `normalizeIdempotencyKey`):
|
|
558
|
+
|
|
559
|
+
| Kural | Sabit | Hata |
|
|
560
|
+
|---|---|---|
|
|
561
|
+
| Anahtar `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`, harf duyarlı | `DEXA_KEY_RE` | `ONE-SDK-E-045` |
|
|
562
|
+
| Parametreler `{ ad: metin }` nesnesi, en çok 32 | `DEXA_MAX_PARAMETERS` | `ONE-SDK-E-046` |
|
|
563
|
+
| Ad `^[A-Za-z_][A-Za-z0-9_]{0,63}$` | `DEXA_PARAMETER_NAME_RE` | `ONE-SDK-E-047` |
|
|
564
|
+
| Değer metin (null olamaz), en çok 65.536 karakter (`Body` model JSON'u taşır) | `DEXA_COMMAND_PARAMETER_VALUE_MAX_LENGTH` | `ONE-SDK-E-048` |
|
|
565
|
+
| `Idempotency-Key` kanonik UUID (8-4-4-4-12), boş UUID değil; SDK küçük harfe çevirir | `IDEMPOTENCY_KEY_RE` | `ONE-SDK-E-049` |
|
|
566
|
+
|
|
567
|
+
Sorgulardaki ayrılmış bağlam parametresi yasağı (`LangCode` vb.) işlemlerde **yoktur**, çünkü sunucu böyle bir kural
|
|
568
|
+
uygulamıyor. 4xx yanıtların hepsi ERP'ye **hiçbir şey gönderilmediği** anlamına gelir; ayrıca 429 `ONE-APP-E-018`
|
|
569
|
+
(kurulum başına saatte 300 işlem).
|
|
570
|
+
|
|
571
|
+
## Zamanlanmış görev ve kuyruk (D-80)
|
|
572
|
+
|
|
573
|
+
Uygulama işi manifest `jobs` ile **beyan eder** ve işleyicisini yazar; işi platform tetikler. Ayrıntı ve örnek:
|
|
574
|
+
ajan belgesi `/docs/jobs.md`; manifest kuralları `docs/manifest.md` içindeki `jobs` bölümünde.
|
|
575
|
+
|
|
576
|
+
| Üye | Açıklama |
|
|
577
|
+
|---|---|
|
|
578
|
+
| `defineJobs({ schedules?, queues? }): OneJobsDefinition` | İşleyicileri ada göre tanımlar; ad manifest beyanıyla aynı olmalı (`JOB_NAME_RE`; geçersiz ad hemen `ONE-SDK-E-070`). Son tanım `handleJobRequest`'in varsayılanıdır. |
|
|
579
|
+
| `OneJobHandler` | `(job: OneJob, env, ctx: OneContext, execution) => unknown`; `ctx` platform bağlamıdır (`readContext`). |
|
|
580
|
+
| `OneJob` | `{ id, name, kind: 'schedule' \| 'queue', attempt, body, scheduledTime?, enqueuedAt? }`. `id` her denemede AYNIDIR (tekrar eleme anahtarı); `attempt` 1'den başlar; tarih alanları ISO 8601 (UTC). |
|
|
581
|
+
| `handleJobRequest(request, env, execution, definition?): Promise<Response> \| null` | Yol `/__one/jobs/...` değilse `null` (uygulama kendi yönlendirmesine geçer). Bu yolda yalnız platform işaretli (`X-One-Auth: job`) `POST` işlenir, başka her şey `404 ONE-SDK-E-075`. İşleyici dönerse 200; 4xx `OneError` fırlatırsa o durum (kalıcı hata, yeniden denenmez); başka hata 500 (platform geri çekilmeyle yeniden dener). |
|
|
582
|
+
| `enqueue(name, body, { delaySeconds? }): Promise<{ id }>` | Kuyruğa ileti bırakır. Ad manifest `jobs.queues` içinde beyanlı olmalı (yoksa platform 404 `ONE-OUT-E-011`); gövde JSON ≤ 64 KiB (`JOB_BODY_MAX_BYTES`), gecikme 0-86.400 sn (`JOB_DELAY_MAX_SECONDS`). Kimlik istekten damgalanır; Vekalet / Yerine Görüntüle oturumunda, uygulama yazmaya açık değilse 403 `ONE-OUT-E-015`. |
|
|
583
|
+
|
|
584
|
+
⛔ Aynı iş birden çok kez gelebilir: kalıcı yazımı `job.id` ile tekrarsız yapın (birincil anahtar + `INSERT OR IGNORE`).
|
|
585
|
+
Kuyruk işi iletiyi bırakanın `sub`'unu bilgi olarak taşır (`ctx.user`); uygulama izinleri ve yönetici bilgisi işe
|
|
586
|
+
taşınmaz. Zamanlanmış iş kullanıcısızdır (`ctx.user` null). İkisinde de Devoplus servisleri **uygulamanın kimliğiyle**
|
|
587
|
+
çağrılır (bkz. aşağıdaki bölüm). `/__one/` yolu platforma ayrılmıştır ve internetten kapalıdır. Yerelde (`one dev`) zamanlama
|
|
588
|
+
tetiklenmez ve `enqueue` 503 döner.
|
|
589
|
+
|
|
590
|
+
## Devoplus servisleri arka plan işinde ve ajan aracında (C6)
|
|
591
|
+
|
|
592
|
+
İşte ve ajan aracında tarayıcıdan gelen bir belirteç yoktur; servis istemcileri (`BackendClient`) yine de çağrı yapar:
|
|
593
|
+
platform çıkış katmanı isteği doğru kimlikle Devoplus'a taşır (`new BackendClient({ baseUrl, authorization: null })` ya
|
|
594
|
+
da `request.headers.get('authorization')`, ikisi de olur). Uygulamanın yazdığı `Authorization` başlığı bu iki yolda
|
|
595
|
+
Devoplus'a **iletilmez**.
|
|
596
|
+
|
|
597
|
+
| Yol | Kimin adına | Kullanılabilen servisler |
|
|
598
|
+
|---|---|---|
|
|
599
|
+
| Kuyruk / zamanlanmış iş (`ctx.mode === 'job'`) | **Uygulamanın** kendi kimliği; kişi yoktur | Yalnız manifest `jobs.services` ile beyan edilip **kurulumda portal yöneticisinin onayladığı** servisler: `mail.execute`, `sms.execute`, `push.execute`, `dexa.read`, `ai.use` (her biri manifest `scopes`'ta da olmalı). Onaysız servis 403 alır. ⛔ DEXA'ya yazma (`dexa.write`) bu yolla **açılmaz**. "Kendime gönder" yoktur: alıcıyı açıkça verin. Denetim kaydında aktör uygulamadır. |
|
|
600
|
+
| Ajan aracı (`ctx.mode === 'agent'`) | Aracı çağıran **kişi** | Kişinin bu uygulamada yapabildikleri; süreli onay gerektiren scope'lar (`CONSENT_SCOPES`: e-posta, SMS, mobil bildirim ve DEXA'ya yazma) bu yolda **kullanılamaz** (kişi onay ekranını göremez). `ctx.admin`: kişi kurulumun portalının şirket yöneticisi; `ctx.actor`: Vekalet / Yerine Görüntüle oturumunda işlemi yapan kişi. |
|
|
601
|
+
|
|
602
|
+
```jsonc
|
|
603
|
+
// one.manifest.json
|
|
604
|
+
"scopes": ["ai.use", "mail.execute"],
|
|
605
|
+
"jobs": { "schedules": [{ "name": "weekly-summary", "cron": "0 8 * * 1" }], "services": ["ai.use", "mail.execute"] }
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
## Günlük
|
|
609
|
+
|
|
610
|
+
`logEvent(ctx, level, message, data?)` sistem/çalışma zamanı olayı; `logError(ctx, message, error, data?)` beklenmeyen
|
|
611
|
+
hata (ad, ileti ve en çok 4.000 karakterlik yığın; `OneError` kodu ve durumu korunur); `logAudit(ctx, action, target?,
|
|
612
|
+
data?)` kritik kullanıcı işlemi; aktör zorunludur (anonim bağlamda fırlatır). Hepsi tek satır JSON olarak yazılır ve
|
|
613
|
+
**platform günlüğünde saklanır**: platform her kurulumda günlüğü açar (D-45), örnekleme manifest
|
|
614
|
+
`observability.headSamplingRate` (varsayılan 1). Satırları platform ekibi kurulum (uygulama × portal) süzgeciyle
|
|
615
|
+
görür; uygulama geliştiricisine açık bir görüntüleme ekranı bu sürümde yoktur.
|
|
616
|
+
|
|
617
|
+
Koruma: `data` içinde adı parola, token, secret, authorization, cookie, API anahtarı, private key, credential ya da
|
|
618
|
+
parola/şifre geçen alanların değeri `"[gizli]"` olur (her derinlikte, girdi değişmez; `redact`); JSON'u 8 KiB'ı aşan
|
|
619
|
+
`data` yerine `{ truncated, bytes }` yazılır (platform günlüğü çağrı başına en çok 256 KB tutar, aşan kısım düşer;
|
|
620
|
+
`safeData`); ileti 1.000 karakterde kırpılır. Maskeleme bir emniyet ağıdır, izin değildir: kişisel veri ve sır yazmayın.
|
|
621
|
+
`problemResponse(error, ctx?)` bilinmeyen hatayı bu kayda (`ctx` verilirse bağlamıyla) düşer; yanıtta yalnız sabit
|
|
622
|
+
metin döner.
|
|
623
|
+
|
|
624
|
+
## Denetim günlüğü (kalıcı)
|
|
625
|
+
|
|
626
|
+
`recordAudit(api, ctx, action, target?, data?)`, `logAudit` izini yazar **ve** `POST One/services/audit` ile
|
|
627
|
+
Devoplus denetim günlüğüne kaydeder (scope `audit.write`; D-36). Kayıt Admin Console Denetim Günlüğü'nde "Devoplus
|
|
628
|
+
One uygulaması" türüyle, `reason = {slug}:{action}`, `targetLabel = target`, `details = data` olarak görünür; aktör
|
|
629
|
+
ve portal istekle gönderilmez, backend doğrulanmış bağlamdan alır. Kurallar sunucuyla birebir: `action`
|
|
630
|
+
`^[a-z0-9][a-z0-9._:-]*$` ≤ 64, `target` ≤ 200, `data` JSON nesnesi ≤ 4 KiB (`buildAuditRequest`; ihlal
|
|
631
|
+
`ONE-SDK-E-030...032` ile FIRLATIR, çünkü programlama hatasıdır). Uzak yazma **best-effort**: ağ/kapı hatası
|
|
632
|
+
`{ recorded: false }` + uyarı olayı döner, isteğiniz düşmez. ⛔ `target`/`data`ya kişisel veri ya da sır yazmayın
|
|
633
|
+
(365 gün saklanır).
|
|
634
|
+
|
|
635
|
+
## Yapay zeka servisi (D-88 a; ⚠️ öneri, geliştirici onayı bekliyor)
|
|
636
|
+
|
|
637
|
+
`completeAi(api, ctx, istek)` (eşdeğeri `ai.complete`), `POST One/services/ai` ucunu çağırır (scope `ai.use`; kullanıcıdan ayrıca
|
|
638
|
+
onay istenmez). Diğer `One/services/*` uçlarıyla aynı kapıdan geçer; kullanıcı, portal ve uygulama istekle GÖNDERİLMEZ.
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
const { output } = await completeAi(api, ctx, { task: 'summarize', input: metin, instructions: 'üç madde', lang: 'tr' })
|
|
642
|
+
const { label } = await completeAi(api, ctx, { task: 'classify', input: talep, labels: ['acil', 'normal'] })
|
|
643
|
+
const { json } = await completeAi(api, ctx, { task: 'extract', input: fatura, schema: { type: 'object', properties: { tutar: { type: 'number' } } } })
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
| Alan | Kural (istemcide de denetlenir; ihlal istek gönderilmeden FIRLATIR) |
|
|
647
|
+
|---|---|
|
|
648
|
+
| `task` | `summarize`, `classify`, `extract`, `generate`, `chat` (`ONE-SDK-E-080`) |
|
|
649
|
+
| `input` | 1-24.000 karakter (`ONE-SDK-E-081`) |
|
|
650
|
+
| `instructions`, `lang` | ≤ 4.000 karakter; `tr` / `en` (`ONE-SDK-E-082`) |
|
|
651
|
+
| `labels` | yalnız `classify`; 2-50 tekil etiket, her biri ≤ 100 karakter (`ONE-SDK-E-083`) |
|
|
652
|
+
| `schema` | yalnız `extract`; JSON nesnesi ≤ 8 KiB (`ONE-SDK-E-084`) |
|
|
653
|
+
| `messages` | yalnız `chat`; ≤ 20 ileti, `role` = `user` ya da `assistant`, `content` dolu metin (`ONE-SDK-E-085`) |
|
|
654
|
+
|
|
655
|
+
Yanıt `OneAiResult`: `output` (metin), `json?` (alan çıkarma), `label?` (sınıflandırma), `model`, `usage`
|
|
656
|
+
(`inputTokens`, `outputTokens`). Tanınmayan yanıt `ONE-SDK-E-086` fırlatır. Model, kota ve portalda yapay zekanın açık
|
|
657
|
+
olup olmadığı Devoplus'tadır: kapalı/yapılandırılmamış portalda 503, kota dolunca 429 (`OneError`). ⛔ Model **sayı
|
|
658
|
+
hesaplamaz** ve **kayıt yazmaz**: tutar/toplam/tarih farkı kodda hesaplanıp `input`'a hazır verilir; çıktı bir öneridir,
|
|
659
|
+
veritabanına yazmadan önce kod doğrular. `input`'a sır ya da işin gerektirmediği kişisel veri konmaz.
|
|
660
|
+
|
|
661
|
+
## Devoplus yapay zekasına açılan araçlar (D-88 b; ⚠️ öneri, geliştirici onayı bekliyor)
|
|
662
|
+
|
|
663
|
+
Devoplus yapay zekası, kullanıcı Devoplus'ta bir soru sorduğunda uygulamanın verisine ve işlemlerine yalnız manifest
|
|
664
|
+
`agent.tools[]` ile beyan edilen **araçlarla**, kullanıcının kendi kimliğiyle ulaşır. Her araç üç yerde durur: manifest
|
|
665
|
+
beyanı (ad, TR/EN başlık + açıklama, `readOnly`, `permission?`, `input` şeması) + işleyici + `AGENT_GUIDE.md`.
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
import guide from '../AGENT_GUIDE.md' with { type: 'text' } // derlemede metin olarak gömülür
|
|
669
|
+
import manifest from '../one.manifest.json' with { type: 'json' }
|
|
670
|
+
|
|
671
|
+
const tools = defineAgentTools<Env>({
|
|
672
|
+
'list-notes': { handler: async ({ input, ctx, env }) => ({ notes: await notlariGetir(env, ctx.user!.sub, input.limit) }) },
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
export default {
|
|
676
|
+
async fetch(request, env) {
|
|
677
|
+
const agent = (await handleAgentRequest(request, env, tools, manifest)) ?? agentManifestResponse(request, manifest, guide)
|
|
678
|
+
if (agent) return agent
|
|
679
|
+
// ... uygulamanın kendi yönlendirmesi
|
|
680
|
+
},
|
|
681
|
+
} satisfies OneApp<Env>
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
`handleAgentRequest` yalnız `POST /__one/agent/<ad>` için yanıt üretir (başka yolda `null`). Kapılar sırasıyla:
|
|
685
|
+
`X-One-Invoked-By: devoplus-agent` (yalnız platformun imzalı çağrısı yazar; tarayıcıdan gelen 404); araç manifest'te
|
|
686
|
+
VE `defineAgentTools`'ta (beyansız 404 `ONE-SDK-E-091`, işleyicisiz 501 `ONE-SDK-E-092`); oturum; `readOnly: false`
|
|
687
|
+
araç Vekalet / Yerine Görüntüle oturumunda 403 (`ONE-SDK-E-095`); `permission` varsa `hasPermission` (403
|
|
688
|
+
`ONE-SDK-E-004`); gövde JSON ve ≤ 256 KiB (`ONE-SDK-E-093`); girdi şemaya uygun (400 `ONE-SDK-E-094` + `errors:
|
|
689
|
+
[{ path, message }]`). İşleyici `{ input, ctx, env }` alır, JSON'a çevrilebilir bir değer döndürür; fırlattığı `OneError`
|
|
690
|
+
ProblemDetails olur. `ctx.admin` çağıran kişinin kurulumun portalının şirket yöneticisi olduğunu, `ctx.actor` Vekalet /
|
|
691
|
+
Yerine Görüntüle oturumunu bildirir. İşleyici Devoplus servislerini kişi adına çağırabilir; süreli onay gerektiren
|
|
692
|
+
scope'lar (e-posta, SMS, mobil bildirim ve DEXA'ya yazma) bu yolda kullanılamaz.
|
|
693
|
+
|
|
694
|
+
Girdi şeması JSON Schema'nın dar bir alt kümesidir: `type` (`object`, `string`, `number`, `integer`, `boolean`,
|
|
695
|
+
`array`), `properties`, `required`, `enum`, `minimum`, `maximum`, `minLength`, `maxLength`, `items`,
|
|
696
|
+
`description` (`AGENT_INPUT_KEYWORDS`). ⛔ Nesneler kapalıdır: şemada olmayan alan reddedilir. Doğrulayıcı
|
|
697
|
+
`validateAgentInput(şema, girdi)` olarak da dışa açıktır.
|
|
698
|
+
|
|
699
|
+
`agentManifestResponse(request, manifest, guide)` oturum istemeden `GET /.well-known/one-agent.json` (yalnız
|
|
700
|
+
`app: { slug, name }` + `agent`) ve `GET /.well-known/one-agent.md` (`AGENT_GUIDE.md`, ≤ 256 KiB) sunar; manifest'te
|
|
701
|
+
`agent` yoksa 404. Rehbere kullanıcı verisi, sır ya da iç adres yazılmaz.
|
|
702
|
+
|
|
703
|
+
## Veri modeli ve kayıt işlemleri (D-84; ⚠️ geliştirici onayı bekliyor)
|
|
704
|
+
|
|
705
|
+
Kayıt tutan uygulamada tabloyu, tipleri ve uçları elle yazmak yerine `one.data.json`'da **varlıkları ve alanları**
|
|
706
|
+
tanımlayın; gerisini `one data generate` ve `createCrudHandler` yapar.
|
|
707
|
+
|
|
708
|
+
```json
|
|
709
|
+
{
|
|
710
|
+
"version": 1,
|
|
711
|
+
"entities": {
|
|
712
|
+
"talepler": {
|
|
713
|
+
"title": { "tr": "Talepler", "en": "Requests" },
|
|
714
|
+
"fields": {
|
|
715
|
+
"baslik": { "type": "text", "title": { "tr": "Başlık", "en": "Title" }, "required": true, "maxLength": 120 },
|
|
716
|
+
"tutar": { "type": "money", "title": { "tr": "Tutar", "en": "Amount" }, "min": 0 },
|
|
717
|
+
"durum": { "type": "choice", "title": { "tr": "Durum", "en": "Status" }, "required": true, "default": "acik", "index": true,
|
|
718
|
+
"choices": [ { "value": "acik", "title": { "tr": "Açık", "en": "Open" } },
|
|
719
|
+
{ "value": "kapali", "title": { "tr": "Kapalı", "en": "Closed" } } ] },
|
|
720
|
+
"sorumlu": { "type": "person", "title": { "tr": "Sorumlu", "en": "Owner" } }
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
| Tür | Saklanan | API'de | Kural |
|
|
728
|
+
|---|---|---|---|
|
|
729
|
+
| `text` / `longText` | metin | metin | `maxLength` (varsayılan 200 / 10.000; en çok 1.000 / 50.000); `text` tek satır |
|
|
730
|
+
| `number` | ondalıklı sayı | sayı | `min` / `max` |
|
|
731
|
+
| `money` | **kuruş, tam sayı**; kolon `<ad>_minor` | `"1234.50"` (metin) | en çok iki ondalık; **yuvarlanmaz, reddedilir**; binlik ayırıcı yok; `min` / `max` lira |
|
|
732
|
+
| `date` | `YYYY-AA-GG` | aynı | gerçek takvim günü |
|
|
733
|
+
| `datetime` | UTC ISO (`...Z`) | aynı | gönderilirken saat dilimi zorunlu (`+03:00` ya da `Z`) |
|
|
734
|
+
| `boolean` | 0/1 | `true`/`false` | |
|
|
735
|
+
| `choice` | `value` kodu (kısıtsız; seçenek denetimi kayıt işlemlerinde) | kod | kod `^[a-z0-9][a-z0-9_-]{0,39}$`; görünen ad `title`; yeni değer migration'sız eklenir |
|
|
736
|
+
| `person` | kullanıcı kimliği (`sub`) | aynı | |
|
|
737
|
+
| `file` | dosya anahtarı (`OneFiles`) | aynı | |
|
|
738
|
+
|
|
739
|
+
Her alan: `required`, `unique` (silinmemiş kayıtlar arasında), `index`, `default`. Ad kuralı `^[a-z][a-z0-9_]{0,39}$`
|
|
740
|
+
(çift alt çizgi, sondaki alt çizgi, `_minor` / `_fold` ekleri ve `order`, `group`, `not` gibi SQL anahtar sözcükleri
|
|
741
|
+
reddedilir); otomatik kolonlar `id` (uuid), `created_at`, `created_by`, `updated_at`, `updated_by`, `deleted_at` alan
|
|
742
|
+
adı olamaz. En çok 30 varlık, varlık başına 60 alan ve tabloda en çok 100 kolon (her metin alanının bir arama kolonu
|
|
743
|
+
`<alan>_fold` vardır, aşağıya bakın). `softDelete` varsayılan `true`.
|
|
744
|
+
|
|
745
|
+
**`one data generate`** şunları üretir: `migrations/NNNN_one_data.sql` (bir sonraki numara) + `src/data/types.ts` (`<Varlık>Record`,
|
|
746
|
+
`<Varlık>Input`, `dataModel`) + `migrations/.one-data-snapshot.json` (bir sonraki farkın tabanı; elle düzenlenmez).
|
|
747
|
+
`--check` hiçbir şey yazmaz, dosyalar güncel değilse 1 ile çıkar. **Yalnız ekleyen** değişiklik üretilir: yeni varlık,
|
|
748
|
+
yeni alan (zorunluysa `default` şart), yeni dizin. Seçim alanına **yeni değer eklemek** migration gerektirmez (yalnız
|
|
749
|
+
tipler ve anlık görüntü yenilenir); seçim değeri **kaldırmak** reddedilir (var olan kayıtlar o değeri taşıyabilir).
|
|
750
|
+
Alan/varlık silme, ad değiştirme, tür değiştirme, zorunluluk değişikliği, tekillik değişikliği ve dizin kaldırma
|
|
751
|
+
algılanırsa komut hiçbir dosya yazmadan nedenini söyleyip durur (görünen adlar serbesttir). Üretilmiş migration
|
|
752
|
+
uygulandıktan sonra değiştirilmez. ⚠️ Bu kurallardan önceki bir SDK ile kurulmuş tablolarda seçim değerleri veritabanında
|
|
753
|
+
sabitlenmiştir (anlık görüntüde `choiceChecks`); o alanlara değer eklemek yine reddedilir. Eski anlık görüntüyle ilk
|
|
754
|
+
üretim metin alanlarına arama kolonlarını ekleyen yeni bir migration yazar.
|
|
755
|
+
|
|
756
|
+
```ts
|
|
757
|
+
import { createCrudHandler, problemResponse, readContext } from '@devoplus/one-sdk'
|
|
758
|
+
import { dataModel } from './data/types.ts'
|
|
759
|
+
|
|
760
|
+
const talepler = createCrudHandler({ model: dataModel, entity: 'talepler', records: 'shared', canWrite: 'One.Talep.Write' })
|
|
761
|
+
// fetch içinde: const res = await talepler.handle(request, env.DB); if (res) return res
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
| Yöntem | Yol | İş |
|
|
765
|
+
|---|---|---|
|
|
766
|
+
| GET | `/api/<varlık>` | `{ items, page, pageSize, total }` döner; parametreler `_page`, `_size` (≤ 200), `_sort=alan` / `-alan`, `alan=değer`, `alan.contains=metin` (metin alanları), `alan.from` / `alan.to` (sayı, para, tarih; dahil); en çok 20 süzgeç |
|
|
767
|
+
| POST | `/api/<varlık>` | 201 + kayıt |
|
|
768
|
+
| GET | `/api/<varlık>/<id>` | kayıt |
|
|
769
|
+
| PATCH | `/api/<varlık>/<id>` | gönderilen alanlar |
|
|
770
|
+
| DELETE | `/api/<varlık>/<id>` | 204 (yumuşak silme) |
|
|
771
|
+
| GET | `/api/<varlık>/export.csv`, `export.xlsx`, `report.html` | aynı süzgeçlerle dışa aktarma / yazdırma sayfası (`_lang=tr|en`) |
|
|
772
|
+
|
|
773
|
+
**Hazır liste bileşeniyle doğrudan bağlanır** (`<one-table src="/api/<varlık>" paging="page" export="xlsx csv">`): uçlar
|
|
774
|
+
bileşenin sorgu lehçesini de kabul eder: `limit` (= `_size`), `page` (= `_page`), `cursor` (sayfa numarası; yanıttaki
|
|
775
|
+
`nextCursor`, son sayfada `null`), `sort` + `order=asc|desc` (= `_sort`), `q` (metin alanlarının **herhangi birinde**
|
|
776
|
+
içerir), `<alan>From` / `<alan>To` (= `.from` / `.to`; `<one-filter>` tarih aralığının varsayılan adları), `lang`
|
|
777
|
+
(= `_lang`), `format=xlsx|csv` (listeyle aynı adreste dışa aktarma). Lehçe adı modeldeki bir alanın adıysa (ör. alan
|
|
778
|
+
`page`) o parametre alan süzgecidir.
|
|
779
|
+
|
|
780
|
+
Seçenekler: `records` (**zorunlu**: `shared` ile herkes görür, `own` ile herkes yalnız kendi oluşturduğunu), `canRead` /
|
|
781
|
+
`canWrite` (uygulama izni kodu ya da `(ctx) => boolean`), `actingSessions` (`read` varsayılan; `write` Vekalet / Yerine
|
|
782
|
+
Görüntüle'de yazmayı açar; manifest'te de `actingSessions: "write"` gerekir), `basePath`, `pageSize`, `exportLimit`
|
|
783
|
+
(varsayılan 10.000), `timeZone`. Okuma ve yazma oturum ister. Gövde yalnız `application/json`, ≤ 1 MB; sistem alanı
|
|
784
|
+
gönderilemez (`created_by` oturumdan yazılır). Hata gövdesi RFC 9457 + `errors: [{ field, message }]` (Türkçe,
|
|
785
|
+
kullanıcıya gösterilebilir). Değerler SQL'e her zaman bağlı parametre olarak gider; sıralama ve süzgeç adları yalnız
|
|
786
|
+
modelden seçilir.
|
|
787
|
+
|
|
788
|
+
**Arama Türkçe katlanmıştır:** `alan.contains` ve `q` büyük/küçük harfe ve Türkçe harflere duyarsızdır ("istanbul"
|
|
789
|
+
"İSTANBUL"u, "cay" "Çay"ı bulur; İ ı I i aynı sayılır). Her metin alanının yanında `<alan>_fold` arama kolonu durur;
|
|
790
|
+
`createCrudHandler` onu yazar, API yanıtında görünmez. Kolonu boş olan kayıt (eski kayıt ya da elle yazılmış SQL) da
|
|
791
|
+
bulunur. ⚠️ Bir metin alanını **elle yazdığınız SQL ile güncelliyorsanız** arama kolonunu da yazın
|
|
792
|
+
(`"<alan>_fold" = ?` + `foldSearchText(değer)`) ya da `NULL` yapın; yoksa arama eski metne göre bulur.
|
|
793
|
+
|
|
794
|
+
## Rapor ve dışa aktarma (D-85; ⚠️ geliştirici onayı bekliyor)
|
|
795
|
+
|
|
796
|
+
`toCsv(rows, columns, { lang, delimiter, timeZone })`; `toXlsx(rows, columns, { lang, sheetName, timeZone })`;
|
|
797
|
+
`exportResponse(içerik, { fileName, format: 'csv' | 'xlsx' | 'html' })`; `renderPrintableReport({ title, subtitle,
|
|
798
|
+
columns, rows, totals, lang })`. Sütun: `{ key, title: { tr, en }, type?: 'text' | 'number' | 'money' | 'date' |
|
|
799
|
+
'datetime' | 'boolean', labels?, width? }`.
|
|
800
|
+
|
|
801
|
+
- **CSV:** UTF-8 + BOM (Excel Türkçe karakteri bozmasın), Türkçede `;` ayırıcı ve `1.234,50` sayı (Türkçe bölge ayarlı
|
|
802
|
+
Excel'in liste ayırıcısı `;`'dir), İngilizcede `,`. `=`, `+`, `-`, `@` ile başlayan metin hücresine `'` eklenir
|
|
803
|
+
(formül enjeksiyonu).
|
|
804
|
+
- **XLSX:** bağımlılıksız; sayı/para/tarih hücreleri sayı olarak yazılır ve biçim alır (Excel'de toplanabilir), metin
|
|
805
|
+
hücresi formül olarak çalışmaz; ilk satır sabitlenir. En çok 50.000 satır, 100 sütun.
|
|
806
|
+
- **İndirme:** arayüz dosyayı belirteçli istekle alır ve `one.download({ name, mime, data })` ile Portal'a verir (en çok
|
|
807
|
+
25 MB). Dosya adı `Content-Disposition`'dadır (`filename*`, Türkçe karakterli; `downloadFileName(header)`).
|
|
808
|
+
- **PDF:** ayrı motor yok. `renderPrintableReport` betiksiz bir yazdırma sayfası üretir; arayüz onu gizli bir
|
|
809
|
+
çerçevede açıp `print()` çağırır, kullanıcı "PDF olarak kaydet"i seçer.
|
|
810
|
+
|
|
811
|
+
## Hata kodları
|
|
812
|
+
|
|
813
|
+
| Kod | Durum | Anlam |
|
|
814
|
+
|---|---|---|
|
|
815
|
+
| `ONE-SDK-E-001` | 500 | Platform bağlamı eksik (dispatch dışından çağrı) |
|
|
816
|
+
| `ONE-SDK-E-002` | 401 | Oturum gerekli |
|
|
817
|
+
| `ONE-SDK-E-003` | 403 | Scope yok (onaya bağlı scope'ta kullanıcı henüz izin vermemiş olabilir, D-59) |
|
|
818
|
+
| `ONE-SDK-E-004` | 403 | Uygulama izni yok (`requirePermission`, D-49) |
|
|
819
|
+
| `ONE-SDK-E-010...013` | 400 | Dosya anahtarı / adı geçersiz |
|
|
820
|
+
| `ONE-SDK-E-014` | 413 | Dosya boyut tavanı (`put` ve `readBoundedBody`) |
|
|
821
|
+
| `ONE-SDK-E-015` | 500 | Dosya yazılamadı |
|
|
822
|
+
| `ONE-SDK-E-016` | 400 | `Content-Length` başlığı geçersiz |
|
|
823
|
+
| `ONE-SDK-E-020...021` | 400 | Önbellek anahtarı geçersiz |
|
|
824
|
+
| `ONE-SDK-E-022` | 400 | Önbellek TTL sınır dışı ya da eksik |
|
|
825
|
+
| `ONE-SDK-E-023` | 400 | Önbellek değeri JSON'a çevrilemiyor |
|
|
826
|
+
| `ONE-SDK-E-024` | 413 | Önbellek değeri 1 MiB tavanı |
|
|
827
|
+
| `ONE-SDK-E-040` | 400 | DEXA sorgu anahtarı geçersiz |
|
|
828
|
+
| `ONE-SDK-E-041` | 400 | DEXA parametre kümesi nesne değil ya da 32'den fazla |
|
|
829
|
+
| `ONE-SDK-E-042` | 400 | DEXA parametre adı geçersiz |
|
|
830
|
+
| `ONE-SDK-E-043` | 400 | DEXA `LangCode` gönderilemez; platformun bağlam parametresidir (harf duyarsız) |
|
|
831
|
+
| `ONE-SDK-E-044` | 400 | DEXA parametre değeri metin değil ya da 2.000 karakteri aşıyor |
|
|
832
|
+
| `ONE-SDK-E-045` | 400 | DEXA işlem anahtarı geçersiz |
|
|
833
|
+
| `ONE-SDK-E-046` | 400 | DEXA işlem parametre kümesi nesne değil ya da 32'den fazla |
|
|
834
|
+
| `ONE-SDK-E-047` | 400 | DEXA işlem parametre adı geçersiz |
|
|
835
|
+
| `ONE-SDK-E-048` | 400 | DEXA işlem parametre değeri metin değil ya da 65.536 karakteri aşıyor |
|
|
836
|
+
| `ONE-SDK-E-049` | 400 | `Idempotency-Key` kanonik UUID değil ya da boş UUID |
|
|
837
|
+
| `ONE-SDK-E-050` | 400 | Org kimliği 24 karakterlik onaltılık değil (istek gönderilmez) |
|
|
838
|
+
| `ONE-SDK-E-051` | 400 | `orgUsersMatching` ölçütsüz çağrıldı |
|
|
839
|
+
| `ONE-SDK-E-060` | 400 | Bildirim senaryosu alanı boş / tanınmayan sonuç / geçersiz doğrulama kodu ya da süresi |
|
|
840
|
+
| `ONE-SDK-E-061` | 400 | Bildirim bağlantısı https değil ya da kimlik bilgisi taşıyor |
|
|
841
|
+
| `ONE-SDK-E-062` | 400 | Bildirim metni backend sınırını aşıyor |
|
|
842
|
+
| `ONE-SDK-E-063` | 400 | Alıcı ya da kanal verilmedi |
|
|
843
|
+
| `ONE-SDK-E-070` | 400 | İş / kuyruk adı geçersiz |
|
|
844
|
+
| `ONE-SDK-E-071` | 400 | Kuyruk iletisi gövdesi JSON'a çevrilemiyor |
|
|
845
|
+
| `ONE-SDK-E-072` | 413 | Kuyruk iletisi gövdesi 64 KiB tavanı |
|
|
846
|
+
| `ONE-SDK-E-073` | 400 | `delaySeconds` 0-86.400 tam sayı değil |
|
|
847
|
+
| `ONE-SDK-E-074` | 503 | İleti kuyruğa bırakılamadı (ağ hatası ya da beklenmeyen yanıt) |
|
|
848
|
+
| `ONE-SDK-E-075` | 404 | İş bulunamadı (`handleJobRequest`: bilinmeyen tür/ad ya da platform dışı çağrı) |
|
|
849
|
+
| `ONE-SDK-E-076` | 400 | İş çağrısının gövdesi JSON değil |
|
|
850
|
+
| `ONE-SDK-E-080...085` | 400 / 413 | Yapay zeka isteği geçersiz (iş, metin, talimat/dil, etiketler, şema, sohbet) |
|
|
851
|
+
| `ONE-SDK-E-086` | 502 | Yapay zeka yanıtı tanınmadı |
|
|
852
|
+
| `ONE-SDK-E-090` | 500 | `defineAgentTools`: araç adı geçersiz ya da işleyici yok |
|
|
853
|
+
| `ONE-SDK-E-091` | 404 | Araç yok / bu yoldan çağrılamaz (beyansız araç, imzasız çağrı, yanlış yöntem) |
|
|
854
|
+
| `ONE-SDK-E-092` | 501 | Araç manifest'te beyanlı ama işleyicisi tanımlanmamış |
|
|
855
|
+
| `ONE-SDK-E-093` | 400 / 413 | Araç girdisi geçerli JSON değil ya da 256 KiB'ı aşıyor |
|
|
856
|
+
| `ONE-SDK-E-094` | 400 | Araç girdisi şemaya uymuyor (`errors` alan listesi) |
|
|
857
|
+
| `ONE-SDK-E-095` | 403 | Değiştiren araç Vekalet / Yerine Görüntüle oturumunda çağrılamaz |
|
|
858
|
+
| `ONE-SDK-E-096` | 500 | Ajan rehberi 256 KiB'ı aşıyor |
|
|
859
|
+
| `ONE-SDK-E-100` | 500 | Veri modeli ya da `createCrudHandler` yapılandırması geçersiz (`one data generate` ile düzeltin) |
|
|
860
|
+
| `ONE-SDK-E-101` | 400 | Kayıt doğrulanamadı (`errors: [{ field, message }]`) |
|
|
861
|
+
| `ONE-SDK-E-102` | 404 | Kayıt bulunamadı (silinmiş ya da başkasının, `records: 'own'`) |
|
|
862
|
+
| `ONE-SDK-E-103` | 409 | Tekil alan değeri başka bir kayıtta kullanılıyor |
|
|
863
|
+
| `ONE-SDK-E-104` | 400 | Liste sorgu parametresi geçersiz (bilinmeyen alan, uygunsuz süzgeç, sayfa boyu) |
|
|
864
|
+
| `ONE-SDK-E-105` | 405 | Bu adreste bu yöntem kullanılamaz |
|
|
865
|
+
| `ONE-SDK-E-106` | 403 | Vekalet / Yerine Görüntüle oturumunda kayıt değiştirilemez |
|
|
866
|
+
| `ONE-SDK-E-107` | 403 | `canRead` / `canWrite` izni yok |
|
|
867
|
+
| `ONE-SDK-E-108` | 413 | Kayıt gövdesi 1 MB'ı aşıyor |
|
|
868
|
+
| `ONE-SDK-E-109` | 415 | Kayıt gövdesi `application/json` değil |
|
|
869
|
+
| `ONE-SDK-E-110` | 400 | Dışa aktarılacak kayıt sayısı sınırı aşıyor (süzgeci daraltın) |
|
|
870
|
+
| `ONE-SDK-E-120` | 400 | Dışa aktarma sütunu / saat dilimi / toplam sütunu geçersiz |
|
|
871
|
+
| `ONE-SDK-E-121` | 400 | Dışa aktarma satır sınırı (50.000) ya da 25 MB indirme sınırı aşıldı |
|
|
872
|
+
| `ONE-SDK-E-130` | 400 | Kişi araması 2-100 karakter olmalı (`peopleSearchHandler`) |
|
|
873
|
+
| `ONE-SDK-E-131` | 405 | Kişi araması yalnız `GET` |
|
|
874
|
+
| `ONE-SDK-E-140` | 400 | DEXA bağlam kodu (`UserCode`, `CompanyCode`, `CostCenterCode`) boş ya da yalnız boşluk (istek gönderilmez) |
|
|
875
|
+
| `ONE-SDK-E-999` | 500 | Beklenmeyen hata (`problemResponse`) |
|
|
876
|
+
|
|
877
|
+
Backend'den dönen kodlar (`ONE-APP-E-*`, `USRDIR-*` vb.) `OneError.code` ile olduğu gibi taşınır.
|
|
878
|
+
|
|
879
|
+
## Sürümleme
|
|
880
|
+
|
|
881
|
+
SemVer; `0.x` boyunca küçük sürüm artışı kırıcı olabilir; `CHANGELOG.md` **Kırıcı** başlığına bakın. Paket iki
|
|
882
|
+
kanaldan otomatik yayımlanır (`Publish SDK (npm)` iş akışı, D-60 ve D-94):
|
|
883
|
+
|
|
884
|
+
| Kanal | Ne zaman | Sürüm | npm etiketi |
|
|
885
|
+
|---|---|---|---|
|
|
886
|
+
| Ön sürüm | Platform deposunun `test` dalına birleşmede | `<package.json sürümü>-next.<n>` (ör. `0.2.0-next.512`) | `next` |
|
|
887
|
+
| Kararlı | `main` dalına birleşmede | `package.json` sürümü | `latest` |
|
|
888
|
+
|
|
889
|
+
Sürüm npm'de zaten varsa hiçbir şey yapılmaz; etiketi geriye çekecek sürüm (npm'dekilerden düşük) reddedilir. Sürüm
|
|
890
|
+
SemVer 2.0'dır; derleme üstverisi (`+...`) karşılaştırmada sayılmaz. Yayımlanan her zaman dalın ucundaki sürümdür. Ön sürüm
|
|
891
|
+
numarası `n` dalın ucundaki commit sayısıdır, dolayısıyla her yeni ön sürüm bir öncekinden büyüktür. Bir sürüm kararlı
|
|
892
|
+
olarak çıktıktan sonra `test`'teki `package.json` sürümü yükseltilir; aksi halde ön sürüm kanalı durur.
|
|
893
|
+
|
|
894
|
+
⚠️ `^0.2.0` aralığı ön sürümleri KAPSAMAZ (SemVer kuralı). Ön sürüm de kurulsun isteniyorsa aralık `^0.2.0-next.0`
|
|
895
|
+
yazılır: bu aralık `0.2.0-next.*` ve `0.2.x` kararlı sürümlerini kapsar, `0.3.0`'ı kapsamaz.
|