create-devoplus-one-app 0.0.0-stage → 0.1.0-next.158
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/README.md +62 -2
- package/dist/cli.mjs +17 -0
- package/package.json +35 -3
- package/templates/0001_init.sql.tpl +10 -0
- package/templates/AGENTS.md.tpl +286 -0
- package/templates/AGENT_GUIDE.md.tpl +36 -0
- package/templates/README.md.tpl +58 -0
- package/templates/_shared/AGENTS.writes.md.tpl +38 -0
- package/templates/_shared/README.tail.md.tpl +20 -0
- package/templates/_shared/files/src/app.test.ts.tpl +36 -0
- package/templates/_shared/files/src/index.ts.tpl +8 -0
- package/templates/_shared/files/src/lib/client.ts.tpl +213 -0
- package/templates/_shared/files/src/lib/files.ts.tpl +37 -0
- package/templates/_shared/files/src/lib/http.ts.tpl +101 -0
- package/templates/_shared/files/src/lib/mail.ts.tpl +26 -0
- package/templates/_shared/files/src/lib/model.ts.tpl +31 -0
- package/templates/_shared/files/src/lib/page.ts.tpl +71 -0
- package/templates/_shared/files/src/lib/record.ts.tpl +246 -0
- package/templates/_shared/files/src/lib/server.ts.tpl +132 -0
- package/templates/_shared/files/src/shell.test.ts.tpl +56 -0
- package/templates/agent.ts.tpl +23 -0
- package/templates/bakim-takvimi/AGENTS.template.md.tpl +40 -0
- package/templates/bakim-takvimi/README.md.tpl +45 -0
- package/templates/bakim-takvimi/files/AGENT_GUIDE.md.tpl +52 -0
- package/templates/bakim-takvimi/files/one.data.json +45 -0
- package/templates/bakim-takvimi/files/src/agent.ts.tpl +22 -0
- package/templates/bakim-takvimi/files/src/app.ts.tpl +259 -0
- package/templates/bakim-takvimi/files/src/jobs.ts.tpl +170 -0
- package/templates/bakim-takvimi/files/src/names.ts.tpl +5 -0
- package/templates/bakim-takvimi/files/src/queries.ts.tpl +71 -0
- package/templates/bakim-takvimi/files/src/schedule.test.ts.tpl +58 -0
- package/templates/bakim-takvimi/files/src/schedule.ts.tpl +106 -0
- package/templates/bakim-takvimi/files/src/shell.ts.tpl +299 -0
- package/templates/bakim-takvimi/manifest.json.tpl +64 -0
- package/templates/egitim-sertifika/AGENTS.template.md.tpl +41 -0
- package/templates/egitim-sertifika/README.md.tpl +46 -0
- package/templates/egitim-sertifika/files/AGENT_GUIDE.md.tpl +53 -0
- package/templates/egitim-sertifika/files/one.data.json +43 -0
- package/templates/egitim-sertifika/files/src/agent.ts.tpl +22 -0
- package/templates/egitim-sertifika/files/src/app.ts.tpl +312 -0
- package/templates/egitim-sertifika/files/src/certificate.test.ts.tpl +83 -0
- package/templates/egitim-sertifika/files/src/certificate.ts.tpl +163 -0
- package/templates/egitim-sertifika/files/src/jobs.ts.tpl +219 -0
- package/templates/egitim-sertifika/files/src/names.ts.tpl +5 -0
- package/templates/egitim-sertifika/files/src/queries.ts.tpl +115 -0
- package/templates/egitim-sertifika/files/src/shell.ts.tpl +345 -0
- package/templates/egitim-sertifika/manifest.json.tpl +57 -0
- package/templates/envanter/AGENTS.template.md.tpl +28 -0
- package/templates/envanter/README.md.tpl +40 -0
- package/templates/envanter/files/AGENT_GUIDE.md.tpl +47 -0
- package/templates/envanter/files/one.data.json +62 -0
- package/templates/envanter/files/src/agent.ts.tpl +20 -0
- package/templates/envanter/files/src/app.ts.tpl +204 -0
- package/templates/envanter/files/src/jobs.ts.tpl +30 -0
- package/templates/envanter/files/src/queries.ts.tpl +53 -0
- package/templates/envanter/files/src/shell.ts.tpl +265 -0
- package/templates/envanter/files/src/stock.test.ts.tpl +31 -0
- package/templates/envanter/files/src/stock.ts.tpl +33 -0
- package/templates/envanter/manifest.json.tpl +56 -0
- package/templates/hedef-takibi/AGENTS.template.md.tpl +37 -0
- package/templates/hedef-takibi/README.md.tpl +52 -0
- package/templates/hedef-takibi/files/AGENT_GUIDE.md.tpl +48 -0
- package/templates/hedef-takibi/files/one.data.json +76 -0
- package/templates/hedef-takibi/files/src/agent.ts.tpl +26 -0
- package/templates/hedef-takibi/files/src/app.ts.tpl +740 -0
- package/templates/hedef-takibi/files/src/progress.test.ts.tpl +53 -0
- package/templates/hedef-takibi/files/src/progress.ts.tpl +81 -0
- package/templates/hedef-takibi/files/src/queries.ts.tpl +83 -0
- package/templates/hedef-takibi/files/src/shell.ts.tpl +455 -0
- package/templates/hedef-takibi/manifest.json.tpl +38 -0
- package/templates/index.ts.tpl +262 -0
- package/templates/markdown.d.ts.tpl +6 -0
- package/templates/musteri-ziyaret/AGENTS.template.md.tpl +47 -0
- package/templates/musteri-ziyaret/README.md.tpl +48 -0
- package/templates/musteri-ziyaret/files/AGENT_GUIDE.md.tpl +44 -0
- package/templates/musteri-ziyaret/files/one.data.json +53 -0
- package/templates/musteri-ziyaret/files/src/agent.ts.tpl +21 -0
- package/templates/musteri-ziyaret/files/src/app.ts.tpl +340 -0
- package/templates/musteri-ziyaret/files/src/customers.ts.tpl +85 -0
- package/templates/musteri-ziyaret/files/src/queries.ts.tpl +67 -0
- package/templates/musteri-ziyaret/files/src/shell.ts.tpl +272 -0
- package/templates/musteri-ziyaret/files/src/visit.test.ts.tpl +65 -0
- package/templates/musteri-ziyaret/files/src/visit.ts.tpl +98 -0
- package/templates/musteri-ziyaret/manifest.json.tpl +37 -0
- package/templates/saha-denetimi/AGENTS.template.md.tpl +32 -0
- package/templates/saha-denetimi/README.md.tpl +44 -0
- package/templates/saha-denetimi/files/AGENT_GUIDE.md.tpl +55 -0
- package/templates/saha-denetimi/files/one.data.json +87 -0
- package/templates/saha-denetimi/files/src/agent.ts.tpl +41 -0
- package/templates/saha-denetimi/files/src/app.ts.tpl +444 -0
- package/templates/saha-denetimi/files/src/queries.ts.tpl +143 -0
- package/templates/saha-denetimi/files/src/score.test.ts.tpl +48 -0
- package/templates/saha-denetimi/files/src/score.ts.tpl +102 -0
- package/templates/saha-denetimi/files/src/shell.ts.tpl +585 -0
- package/templates/saha-denetimi/manifest.json.tpl +49 -0
- package/templates/shell.test.ts.tpl +30 -0
- package/templates/shell.ts.tpl +201 -0
- package/templates/uygunsuzluk-dof/AGENTS.template.md.tpl +40 -0
- package/templates/uygunsuzluk-dof/README.md.tpl +46 -0
- package/templates/uygunsuzluk-dof/files/AGENT_GUIDE.md.tpl +56 -0
- package/templates/uygunsuzluk-dof/files/one.data.json +99 -0
- package/templates/uygunsuzluk-dof/files/src/agent.ts.tpl +45 -0
- package/templates/uygunsuzluk-dof/files/src/app.ts.tpl +431 -0
- package/templates/uygunsuzluk-dof/files/src/capa.test.ts.tpl +53 -0
- package/templates/uygunsuzluk-dof/files/src/capa.ts.tpl +114 -0
- package/templates/uygunsuzluk-dof/files/src/jobs.ts.tpl +127 -0
- package/templates/uygunsuzluk-dof/files/src/queries.ts.tpl +102 -0
- package/templates/uygunsuzluk-dof/files/src/shell.ts.tpl +441 -0
- package/templates/uygunsuzluk-dof/manifest.json.tpl +56 -0
- package/templates/ziyaretci-kaydi/AGENTS.template.md.tpl +35 -0
- package/templates/ziyaretci-kaydi/README.md.tpl +46 -0
- package/templates/ziyaretci-kaydi/files/AGENT_GUIDE.md.tpl +39 -0
- package/templates/ziyaretci-kaydi/files/one.data.json +42 -0
- package/templates/ziyaretci-kaydi/files/src/agent.ts.tpl +13 -0
- package/templates/ziyaretci-kaydi/files/src/app.ts.tpl +293 -0
- package/templates/ziyaretci-kaydi/files/src/jobs.ts.tpl +49 -0
- package/templates/ziyaretci-kaydi/files/src/notice.ts.tpl +48 -0
- package/templates/ziyaretci-kaydi/files/src/queries.ts.tpl +45 -0
- package/templates/ziyaretci-kaydi/files/src/shell.ts.tpl +252 -0
- package/templates/ziyaretci-kaydi/files/src/visitor.test.ts.tpl +34 -0
- package/templates/ziyaretci-kaydi/files/src/visitor.ts.tpl +57 -0
- package/templates/ziyaretci-kaydi/manifest.json.tpl +38 -0
package/README.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-devoplus-one-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Yeni bir **Devoplus One** uygulamasının iskeletini üretir: uygulama manifest'i, `@devoplus/one-sdk` ile yazılmış sunucu
|
|
4
|
+
giriş noktası, Devoplus Portal ile konuşan sayfa, veritabanı migration'ı, testler, `README.md` ve uygulamayı yazacak
|
|
5
|
+
yapay zeka aracı için kuralları anlatan `AGENTS.md`. Boş iskelet yerine çalışan bir iş uygulaması şablonundan da
|
|
6
|
+
başlayabilirsiniz.
|
|
7
|
+
|
|
8
|
+
## Kullanım
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm create devoplus-one-app@next -- <hedef-dizin> --slug <kısa-ad> --owner <sahibin e-postası> [--template <şablon>]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`npx create-devoplus-one-app@next <hedef-dizin> --slug <kısa-ad> --owner <e-posta>` aynı işi yapar. Kararlı sürüm henüz yayımlanmadı; bu yüzden `@next`
|
|
15
|
+
yazın. Node 22 ya da üstü gerekir. Hedef dizin boş değilse `--force` verilmeden hiçbir dosya yazılmaz.
|
|
16
|
+
|
|
17
|
+
⚠️ Devoplus One'ın iskelet üreticisi yalnız bu pakettir. Adı benzeyen başka paketler Devoplus'a ait değildir; onları
|
|
18
|
+
çalıştırmayın.
|
|
19
|
+
|
|
20
|
+
## Şablonlar
|
|
21
|
+
|
|
22
|
+
| Şablon | Ne için |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `blank` (varsayılan) | Kişisel notlar örneğiyle en küçük başlangıç; hiçbir şablon uymuyorsa |
|
|
25
|
+
| `envanter` | Stok kalemleri, giriş ve çıkış hareketleri, düşük stok uyarısı |
|
|
26
|
+
| `bakim-takvimi` | Cihaz ve ekipmanların periyodik bakımı ve hatırlatmaları |
|
|
27
|
+
| `saha-denetimi` | Mağaza ve şube denetimi için kontrol listesi, puan ve rapor |
|
|
28
|
+
| `egitim-sertifika` | Çalışanların eğitim ve sertifikaları, süre dolumu hatırlatması |
|
|
29
|
+
| `ziyaretci-kaydi` | Resepsiyonda ziyaretçi girişi ve çıkışı, ziyaret edilene bildirim |
|
|
30
|
+
| `uygunsuzluk-dof` | Kalite ve iş güvenliği uygunsuzlukları ve düzeltici faaliyetler |
|
|
31
|
+
| `musteri-ziyaret` | Saha ekibinin müşteri ziyaretleri ve sonraki adımlar |
|
|
32
|
+
| `hedef-takibi` | Ekip hedefleri, ölçülebilir sonuçlar ve aksiyonlar |
|
|
33
|
+
|
|
34
|
+
`--list-templates` şablonları kısa açıklamalarıyla listeler.
|
|
35
|
+
|
|
36
|
+
## Seçenekler
|
|
37
|
+
|
|
38
|
+
| Seçenek | Açıklama |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `--owner <e-posta>` | Zorunlu: uygulamanın sahibinin e-posta adresi |
|
|
41
|
+
| `--slug <kısa-ad>` | Küçük harf, rakam ve tek tire; harfle başlar, en çok 24 karakter (varsayılan: hedef dizinin adı) |
|
|
42
|
+
| `--template <şablon>` | Yukarıdaki şablonlardan biri (varsayılan: `blank`) |
|
|
43
|
+
| `--name "<ad>"` / `--name-en "<ad>"` | Türkçe ve İngilizce ad (varsayılan: kısa addan) |
|
|
44
|
+
| `--services <liste>` | `database` her zaman açıktır; `files` dosya deposu, `cache` önbellek ekler |
|
|
45
|
+
| `--scopes <liste>` | İstenen yetki alanları, ör. `orgschema.read,mail.execute` |
|
|
46
|
+
| `--permission-code <kod>` | Uygulamayı açan izin kodu, `One.<Ad>.<Ad>` (varsayılan: kısa addan türetilir) |
|
|
47
|
+
| `--sdk <aralık>` / `--version <sürüm>` | SDK sürüm aralığı ve uygulamanın başlangıç sürümü |
|
|
48
|
+
| `--force` | Boş olmayan dizine yazar |
|
|
49
|
+
|
|
50
|
+
## Sonraki adımlar
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd <hedef-dizin>
|
|
54
|
+
pnpm install && pnpm type-check && pnpm test
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Ardından `AGENTS.md`'yi okuyun ve üretilen `README.md`'deki kayıt, kurulum ve yayın adımlarını izleyin. Platformun
|
|
58
|
+
geliştirici belgeleri ön sürüm kanalı için https://test-one.devoplus.dev/docs adresindedir.
|
|
59
|
+
|
|
60
|
+
## Sürümler ve lisans
|
|
61
|
+
|
|
62
|
+
Paket `@devoplus/one-sdk` ile aynı iki kanaldan yayımlanır: `next` (ön sürüm, `<sürüm>-next.<n>`) ve `latest`
|
|
63
|
+
(kararlı). Lisans: `UNLICENSED` (açık kaynak lisansı verilmez).
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{mkdir,readdir,stat,writeFile}from"node:fs/promises";import{basename,dirname,join as join2,resolve}from"node:path";import{readFileSync,readdirSync,statSync}from"node:fs";var MONTHS=["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"];var DAYS=["sun","mon","tue","wed","thu","fri","sat"];var FIELDS=[{label:"dakika",min:0,max:59},{label:"saat",min:0,max:23},{label:"ayın günü",min:1,max:31},{label:"ay",min:1,max:12,names:MONTHS,nameBase:1},{label:"haftanın günü",min:0,max:7,names:DAYS,nameBase:0}];function value(raw,spec){if(/^\d{1,2}$/.test(raw)){const n=Number(raw);return n>=spec.min&&n<=spec.max?n:null}if(spec.names){const i=spec.names.indexOf(raw.toLowerCase());if(i>=0)return i+(spec.nameBase??0)}return null}function field(raw,spec){const values=new Set;const star=raw.startsWith("*");for(const part of raw.split(",")){const m=/^(\*|[A-Za-z0-9]+(?:-[A-Za-z0-9]+)?)(?:\/(\d{1,2}))?$/.exec(part);if(!m)return`${spec.label} alanında tanınmayan öğe: "${part}"`;const[,range,stepRaw]=m;const step=stepRaw===void 0?1:Number(stepRaw);if(step<1)return`${spec.label} alanında adım en az 1 olmalı: "${part}"`;let from;let to;if(range==="*"){from=spec.min;to=spec.max}else if(range.includes("-")){const[a,b]=range.split("-");const va=value(a,spec);const vb=value(b,spec);if(va===null||vb===null)return`${spec.label} alanında geçersiz aralık: "${part}" (${spec.min}-${spec.max})`;if(va>vb)return`${spec.label} alanında aralık ters: "${part}"`;from=va;to=vb}else{const v=value(range,spec);if(v===null)return`${spec.label} alanında geçersiz değer: "${part}" (${spec.min}-${spec.max})`;from=v;to=stepRaw===void 0?v:spec.max}for(let n=from;n<=to;n+=step)values.add(n)}return{values,star}}function parseCron(expression){if(typeof expression!=="string")return{ok:false,error:"cron ifadesi metin olmalı"};const parts=expression.trim().split(/\s+/);if(parts.length!==5||parts[0]==="")return{ok:false,error:"cron ifadesi tam 5 alan olmalı (dakika saat ayın-günü ay haftanın-günü)"};const parsed=[];for(let i=0;i<5;i++){const r=field(parts[i],FIELDS[i]);if(typeof r==="string")return{ok:false,error:r};parsed.push(r)}const[minute,hour,dom,month,dow]=parsed;const dayOfWeek=new Set([...dow.values].map(d=>d===7?0:d));return{ok:true,schedule:{minute:minute.values,hour:hour.values,dayOfMonth:dom.values,month:month.values,dayOfWeek,dayOfMonthStar:dom.star,dayOfWeekStar:dow.star}}}var TIME_ZONE_RE=/^[A-Za-z][A-Za-z0-9_+-]*(?:\/[A-Za-z0-9_+-]+){0,2}$/;function canonicalTimeZone(timeZone){if(typeof timeZone!=="string"||!TIME_ZONE_RE.test(timeZone))return null;try{return new Intl.DateTimeFormat("en-US",{timeZone}).resolvedOptions().timeZone}catch{return null}}import{z}from"zod";var SLUG_RE=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;var SLUG_MAX=24;function isValidSlug(slug2){return slug2.length<=SLUG_MAX&&SLUG_RE.test(slug2)}var JOB_NAME_RE=/^[a-z][a-z0-9-]{1,39}$/;var JOBS_MAX_SCHEDULES=10;var JOBS_MAX_QUEUES=10;var JOBS_DEFAULT_TIME_ZONE="Europe/Istanbul";var JOB_MAX_RETRIES_DEFAULT=3;var JOB_MAX_RETRIES_MAX=10;var JOB_BODY_MAX_BYTES=64*1024;var JOB_IDENTITY_MAX_AGE_MS=48*60*60*1e3;var JOB_SERVICE_SCOPES=["mail.execute","sms.execute","push.execute","dexa.read","ai.use"];var jobName=z.string().regex(JOB_NAME_RE,"ad küçük harfle başlar; yalnız küçük harf, rakam ve tire; 2-40 karakter");var description=z.string().trim().min(1).max(200).optional();var scheduleSchema=z.object({name:jobName,cron:z.string().superRefine((value2,ctx)=>{const r=parseCron(value2);if(!r.ok)ctx.addIssue({code:"custom",message:r.error})}),timeZone:z.string().default(JOBS_DEFAULT_TIME_ZONE).transform((value2,ctx)=>{const canonical=canonicalTimeZone(value2);if(!canonical){ctx.addIssue({code:"custom",message:`"${value2}" tanınan bir saat dilimi değil (ör. Europe/Istanbul, UTC)`});return z.NEVER}return canonical}),description}).strict();var queueSchema=z.object({name:jobName,maxRetries:z.number().int().min(0).max(JOB_MAX_RETRIES_MAX).default(JOB_MAX_RETRIES_DEFAULT),description}).strict();function uniqueNames(items,ctx){const seen=new Set;items.forEach((item,i)=>{if(seen.has(item.name))ctx.addIssue({code:"custom",path:[i,"name"],message:`"${item.name}" adı birden çok kez yazılmış`});seen.add(item.name)})}var jobsSchema=z.object({schedules:z.array(scheduleSchema).max(JOBS_MAX_SCHEDULES,`en çok ${JOBS_MAX_SCHEDULES} zamanlama beyan edilebilir`).superRefine(uniqueNames).default([]),queues:z.array(queueSchema).max(JOBS_MAX_QUEUES,`en çok ${JOBS_MAX_QUEUES} kuyruk beyan edilebilir`).superRefine(uniqueNames).default([]),services:z.array(z.string().refine(s=>JOB_SERVICE_SCOPES.includes(s),{message:`arka plan işi servisleri yalnız şunlar olabilir: ${JOB_SERVICE_SCOPES.join(", ")} (DEXA'ya yazma bu yolla açılmaz)`})).max(JOB_SERVICE_SCOPES.length).superRefine((items,ctx)=>{const seen=new Set;items.forEach((s,i)=>{if(seen.has(s))ctx.addIssue({code:"custom",path:[i],message:`"${s}" birden çok kez yazılmış`});seen.add(s)})}).optional()}).strict().refine(jobs=>jobs.schedules.length+jobs.queues.length>0,{message:"jobs en az bir zamanlama (schedules) ya da kuyruk (queues) taşımalıdır; hiç yoksa alanı yazmayın"});import{z as z3}from"zod";import{z as z2}from"zod";var AGENT_TOOL_NAME_RE=/^[a-z][a-z0-9-]{1,39}$/;var AGENT_TOOLS_MAX=20;var AGENT_INPUT_SCHEMA_MAX_BYTES=8*1024;var AGENT_INPUT_SCHEMA_MAX_DEPTH=8;var AGENT_INPUT_SCHEMA_MAX_PROPERTIES=50;var AGENT_INPUT_SCHEMA_MAX_ENUM=100;var AGENT_INPUT_SCHEMA_DESCRIPTION_MAX=500;var AGENT_INPUT_PROPERTY_RE=/^[A-Za-z_][A-Za-z0-9_]{0,63}$/;var AGENT_INPUT_KEYWORDS=["type","properties","required","enum","minimum","maximum","minLength","maxLength","items","description"];var AGENT_INPUT_TYPES=["object","string","number","integer","boolean","array"];var KEYWORDS=new Set(AGENT_INPUT_KEYWORDS);var TYPES=new Set(AGENT_INPUT_TYPES);function isPlainObject(value2){return value2!==null&&typeof value2==="object"&&!Array.isArray(value2)}function utf8Length(text){let n=0;for(const ch of text){const c=ch.codePointAt(0)??0;n+=c<128?1:c<2048?2:c<65536?3:4}return n}function join(path,key){return path?`${path}.${key}`:String(key)}function agentInputSchemaProblems(schema){const problems=[];let serialized;try{serialized=JSON.stringify(schema)??""}catch{return[{path:"",message:"şema JSON'a çevrilemiyor"}]}const bytes=utf8Length(serialized);if(bytes>AGENT_INPUT_SCHEMA_MAX_BYTES)problems.push({path:"",message:`şema en çok ${AGENT_INPUT_SCHEMA_MAX_BYTES} bayt olabilir (${bytes})`});if(!isPlainObject(schema))return[...problems,{path:"",message:"şema bir nesne olmalıdır"}];if(schema.type!=="object")problems.push({path:"type",message:'araç girdisinin kökü "type": "object" olmalıdır'});node(schema,"",1,problems);return problems}function node(schema,path,depth,out){if(!isPlainObject(schema)){out.push({path,message:"şema düğümü bir nesne olmalıdır"});return}if(depth>AGENT_INPUT_SCHEMA_MAX_DEPTH){out.push({path,message:`şema en çok ${AGENT_INPUT_SCHEMA_MAX_DEPTH} düzey iç içe olabilir`});return}for(const key of Object.keys(schema))if(!KEYWORDS.has(key))out.push({path:join(path,key),message:`"${key}" desteklenmiyor; izinli anahtarlar: ${AGENT_INPUT_KEYWORDS.join(", ")}`});const type=schema.type;if(typeof type!=="string"||!TYPES.has(type)){out.push({path:join(path,"type"),message:`"type" zorunludur ve şunlardan biri olmalıdır: ${AGENT_INPUT_TYPES.join(", ")}`});return}const only=(key,allowed)=>{if(key in schema&&!allowed.includes(type))out.push({path:join(path,key),message:`"${key}" yalnız ${allowed.join("/")} tipinde kullanılabilir`})};only("properties",["object"]);only("required",["object"]);only("items",["array"]);only("minimum",["number","integer"]);only("maximum",["number","integer"]);only("minLength",["string"]);only("maxLength",["string"]);if("description"in schema){const d=schema.description;if(typeof d!=="string"||d.trim().length===0||d.length>AGENT_INPUT_SCHEMA_DESCRIPTION_MAX)out.push({path:join(path,"description"),message:`"description" 1-${AGENT_INPUT_SCHEMA_DESCRIPTION_MAX} karakterlik metin olmalıdır`})}for(const key of["minimum","maximum"]){if(key in schema&&(typeof schema[key]!=="number"||!Number.isFinite(schema[key])))out.push({path:join(path,key),message:`"${key}" sonlu bir sayı olmalıdır`})}if(typeof schema.minimum==="number"&&typeof schema.maximum==="number"&&schema.minimum>schema.maximum)out.push({path:join(path,"minimum"),message:'"minimum" "maximum"dan büyük olamaz'});for(const key of["minLength","maxLength"]){const v=schema[key];if(key in schema&&(typeof v!=="number"||!Number.isSafeInteger(v)||v<0))out.push({path:join(path,key),message:`"${key}" negatif olmayan bir tam sayı olmalıdır`})}if(typeof schema.minLength==="number"&&typeof schema.maxLength==="number"&&schema.minLength>schema.maxLength)out.push({path:join(path,"minLength"),message:'"minLength" "maxLength"ten büyük olamaz'});if("enum"in schema){const values=schema.enum;if(!Array.isArray(values)||values.length===0||values.length>AGENT_INPUT_SCHEMA_MAX_ENUM){out.push({path:join(path,"enum"),message:`"enum" 1-${AGENT_INPUT_SCHEMA_MAX_ENUM} öğeli bir dizi olmalıdır`})}else{const seen=new Set;values.forEach((v,i)=>{const ok=type==="string"&&typeof v==="string"||type==="boolean"&&typeof v==="boolean"||type==="number"&&typeof v==="number"&&Number.isFinite(v)||type==="integer"&&typeof v==="number"&&Number.isSafeInteger(v);if(!ok)out.push({path:join(join(path,"enum"),i),message:`"enum" öğesi "${type}" tipinde olmalıdır`});const k=JSON.stringify(v);if(seen.has(k))out.push({path:join(join(path,"enum"),i),message:'"enum" öğesi birden çok kez yazılmış'});seen.add(k)})}}if(type==="object"){const props=schema.properties??{};if(!isPlainObject(props)){out.push({path:join(path,"properties"),message:'"properties" bir nesne olmalıdır'})}else{const names=Object.keys(props);if(names.length>AGENT_INPUT_SCHEMA_MAX_PROPERTIES)out.push({path:join(path,"properties"),message:`bir nesnede en çok ${AGENT_INPUT_SCHEMA_MAX_PROPERTIES} alan olabilir`});for(const name of names){if(!AGENT_INPUT_PROPERTY_RE.test(name))out.push({path:join(join(path,"properties"),name),message:'alan adı harf ya da "_" ile başlar, yalnız harf/rakam/"_" içerir, en çok 64 karakter'});node(props[name],join(join(path,"properties"),name),depth+1,out)}if("required"in schema){const req=schema.required;if(!Array.isArray(req)||req.some(r=>typeof r!=="string")){out.push({path:join(path,"required"),message:'"required" metin dizisi olmalıdır'})}else{const seen=new Set;req.forEach((r,i)=>{if(!(r in props))out.push({path:join(join(path,"required"),i),message:`"${r}" "properties" içinde tanımlı değil`});if(seen.has(r))out.push({path:join(join(path,"required"),i),message:`"${r}" birden çok kez yazılmış`});seen.add(r)})}}}}if(type==="array"){if(!("items"in schema))out.push({path:join(path,"items"),message:'dizi tipinde "items" zorunludur'});else node(schema.items,join(path,"items"),depth+1,out)}}var localized=z2.object({tr:z2.string().trim().min(1).max(200),en:z2.string().trim().min(1).max(200).optional()});var localizedLong=z2.object({tr:z2.string().trim().min(1).max(1e3),en:z2.string().trim().min(1).max(1e3).optional()});var agentToolSchema=z2.object({name:z2.string().regex(AGENT_TOOL_NAME_RE,'araç adı küçük harfle başlar, yalnız küçük harf/rakam/"-" içerir, 2-40 karakter'),title:localized,description:localizedLong,readOnly:z2.boolean(),permission:z2.string().optional(),input:z2.unknown().superRefine((schema,ctx)=>{for(const p of agentInputSchemaProblems(schema))ctx.addIssue({code:"custom",path:p.path?p.path.split("."):[],message:p.message})})}).strict();var agentSectionSchema=z2.object({summary:localizedLong,tools:z2.array(agentToolSchema).max(AGENT_TOOLS_MAX,`en çok ${AGENT_TOOLS_MAX} araç beyan edilebilir`).superRefine((tools,ctx)=>{const seen=new Set;tools.forEach((t,i)=>{if(seen.has(t.name))ctx.addIssue({code:"custom",path:[i,"name"],message:`"${t.name}" aracı birden çok kez yazılmış`});seen.add(t.name)})}).default([])}).strict();var localizedText=z3.object({tr:z3.string().trim().min(1).max(120),en:z3.string().trim().min(1).max(120).optional()});var SCOPE_RE=/^(?:[a-z][a-z0-9]*\.(?:read|write|manage|execute)|ai\.use)$/;var DEXA_QUERY_KEY_RE=/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;var DEXA_QUERY_KEYS_MAX=64;function renamed(next){return z3.never({error:`bu alan kaldırıldı; yerine "services.${next}" yazın`}).optional()}var DEXA_READ_SCOPE="dexa.read";var DEXA_COMMAND_KEYS_MAX=32;var DEXA_WRITE_SCOPE="dexa.write";function dexaKeyList(kind,max){return z3.array(z3.string().regex(DEXA_QUERY_KEY_RE,"DEXA anahtarı harf ya da rakamla başlamalı, yalnız harf/rakam/./_/- içermeli ve en çok 128 karakter olmalıdır")).min(1,`en az bir DEXA ${kind} anahtarı beyan edilmelidir`).max(max,`en çok ${max} DEXA ${kind} anahtarı beyan edilebilir`).superRefine((keys,ctx)=>{const seen=new Set;keys.forEach((key,i)=>{if(seen.has(key))ctx.addIssue({code:"custom",path:[i],message:`"${key}" anahtarı birden çok kez yazılmış`});seen.add(key)})})}var NAVIGATION_MAX_ITEMS=12;var NAVIGATION_PATH_RE=/^\/(?:[A-Za-z0-9_-][A-Za-z0-9._-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9._-]*)*)?$/;var NAVIGATION_PATH_MAX_LENGTH=200;var ACTING_SESSION_MODES=["read","write"];var DEVICE_PERMISSIONS=["camera","microphone","geolocation"];var semver=z3.string().regex(/^\d+\.\d+\.\d+$/,"semver biçimi x.y.z olmalı");var PERMISSION_CODE_RE=/^One\.[A-Z][A-Za-z0-9]*\.[A-Z][A-Za-z0-9]*$/;var permissionCode=z3.string().regex(PERMISSION_CODE_RE);var EXTRA_PERMISSIONS_MAX=16;function permissionNamespace(code){return code.slice(0,code.lastIndexOf(".")+1)}var manifestSchema=z3.object({schemaVersion:z3.literal(1),app:z3.object({slug:z3.string().max(SLUG_MAX).regex(SLUG_RE,"slug küçük harf/rakam ve tek tire içerebilir; harfle başlar"),name:localizedText,version:semver,description:localizedText.optional(),owner:z3.object({email:z3.string().email()})}),entry:z3.string().regex(/^[\w./-]+\.(ts|js|mjs)$/),scopes:z3.array(z3.string().regex(SCOPE_RE)).max(32).default([]),services:z3.object({database:z3.boolean().default(true),files:z3.boolean().default(false),cache:z3.boolean().default(false),d1:renamed("database"),r2:renamed("files"),kv:renamed("cache"),email:z3.boolean().default(false),sms:z3.boolean().default(false),push:z3.boolean().default(false),orgSchema:z3.boolean().default(false),dexa:z3.object({queryKeys:dexaKeyList("sorgu",DEXA_QUERY_KEYS_MAX).optional(),commandKeys:dexaKeyList("işlem",DEXA_COMMAND_KEYS_MAX).optional()}).refine(dexa=>dexa.queryKeys!==void 0||dexa.commandKeys!==void 0,{message:"services.dexa en az bir anahtar listesi taşımalıdır: queryKeys (okuma sorguları) ya da commandKeys (ERP işlemleri)"}).optional()}).strict().transform(({d1:_d1,r2:_r2,kv:_kv,...services})=>services).prefault({}),menu:z3.object({title:localizedText,icon:z3.string().max(64).optional(),group:z3.string().max(64).optional()}),permission:z3.object({code:permissionCode,name:localizedText}),permissions:z3.array(z3.object({code:permissionCode,name:localizedText}).strict()).max(EXTRA_PERMISSIONS_MAX,`en çok ${EXTRA_PERMISSIONS_MAX} ek izin beyan edilebilir`).default([]),navigation:z3.array(z3.object({path:z3.string().max(NAVIGATION_PATH_MAX_LENGTH,`yol en çok ${NAVIGATION_PATH_MAX_LENGTH} karakter olabilir`).regex(NAVIGATION_PATH_RE,"yol '/' ile başlar ve yalnız harf, rakam, '-', '_', '.' içeren parçalardan oluşur (sorgu, '#', '..' yok)"),label:localizedText,permission:z3.string().optional()}).strict()).max(NAVIGATION_MAX_ITEMS,`en çok ${NAVIGATION_MAX_ITEMS} menü öğesi beyan edilebilir`).superRefine((items,ctx)=>{const seen=new Set;items.forEach((item,i)=>{if(seen.has(item.path))ctx.addIssue({code:"custom",path:[i,"path"],message:`"${item.path}" yolu birden çok kez yazılmış`});seen.add(item.path)})}).default([]),actingSessions:z3.enum(ACTING_SESSION_MODES).default("read"),devicePermissions:z3.array(z3.enum(DEVICE_PERMISSIONS)).superRefine((items,ctx)=>{const seen=new Set;items.forEach((item,i)=>{if(seen.has(item))ctx.addIssue({code:"custom",path:[i],message:`"${item}" birden çok kez yazılmış`});seen.add(item)})}).default([]),agent:agentSectionSchema.optional(),limits:z3.object({cpuMs:z3.number().int().min(1).max(1e3).optional(),subRequests:z3.number().int().min(1).max(200).optional()}).optional(),observability:z3.object({headSamplingRate:z3.number().gt(0,"örnekleme oranı 0'dan büyük olmalıdır").max(1,"örnekleme oranı en çok 1 olabilir").optional()}).strict().optional(),jobs:jobsSchema.optional()}).strict().superRefine((manifest,ctx)=>{const ns=permissionNamespace(manifest.permission.code);const seen=new Set([manifest.permission.code]);manifest.permissions.forEach((p,i)=>{if(!p.code.startsWith(ns))ctx.addIssue({code:"custom",path:["permissions",i,"code"],message:`ek izin "${p.code}" ana iznin ad alanında olmalı (${ns}…)`});else if(seen.has(p.code))ctx.addIssue({code:"custom",path:["permissions",i,"code"],message:`"${p.code}" izni birden çok kez yazılmış`});seen.add(p.code)});const declared=new Set([manifest.permission.code,...manifest.permissions.map(p=>p.code)]);manifest.agent?.tools.forEach((tool,i)=>{if(tool.permission===void 0)return;if(!PERMISSION_CODE_RE.test(tool.permission)||!tool.permission.startsWith(ns))ctx.addIssue({code:"custom",path:["agent","tools",i,"permission"],message:`araç izni "${tool.permission}" uygulamanın ad alanında olmalı (${ns}…)`});else if(!declared.has(tool.permission))ctx.addIssue({code:"custom",path:["agent","tools",i,"permission"],message:`araç izni "${tool.permission}" manifest'te beyan edilmemiş — "permission.code" ya da "permissions" içinde olmalı`})});manifest.navigation.forEach((item,i)=>{if(item.permission===void 0)return;if(!PERMISSION_CODE_RE.test(item.permission)||!item.permission.startsWith(ns))ctx.addIssue({code:"custom",path:["navigation",i,"permission"],message:`sekme izni "${item.permission}" uygulamanın ad alanında olmalı (${ns}…)`});else if(!declared.has(item.permission))ctx.addIssue({code:"custom",path:["navigation",i,"permission"],message:`sekme izni "${item.permission}" manifest'te beyan edilmemiş — "permission.code" ya da "permissions" içinde olmalı`})});manifest.jobs?.services?.forEach((service,i)=>{if(!manifest.scopes.includes(service))ctx.addIssue({code:"custom",path:["jobs","services",i],message:`jobs.services içindeki "${service}" scopes içinde de beyan edilmelidir`})});const dexa=manifest.services.dexa;const pairs=[{scope:DEXA_READ_SCOPE,keys:dexa?.queryKeys,field:"queryKeys",what:"sorgu",verb:"DEXA sorguları"},{scope:DEXA_WRITE_SCOPE,keys:dexa?.commandKeys,field:"commandKeys",what:"işlem",verb:"DEXA işlemleri (ERP yazma)"}];for(const pair of pairs){const hasScope=manifest.scopes.includes(pair.scope);const hasKeys=pair.keys!==void 0;if(hasKeys&&!hasScope)ctx.addIssue({code:"custom",path:["scopes"],message:`services.dexa.${pair.field} beyan edildi; scopes içinde "${pair.scope}" de olmalıdır (${pair.verb} bu scope olmadan çalışmaz)`});if(hasScope&&!hasKeys)ctx.addIssue({code:"custom",path:["services","dexa"],message:`scopes içinde "${pair.scope}" var; services.dexa.${pair.field} ile çalıştırılacak ${pair.what} anahtarları da beyan edilmelidir`})}});function validateManifest(input){const r=manifestSchema.safeParse(input);if(r.success)return{ok:true,manifest:r.data};return{ok:false,errors:r.error.issues.map(i=>({path:i.path.join(".")||"(kök)",message:i.message}))}}var BLOCK_RE=/\{\{([#^])([A-Za-z0-9_]+)\}\}\n?([\s\S]*?)\{\{\/\2\}\}\n?/g;var VAR_RE=/\{\{([A-Za-z0-9_]+)\}\}/g;var MAX_NESTING=8;function lookup(vars,name){if(!Object.hasOwn(vars,name))throw new Error(`şablon değişkeni tanımsız: ${name}`);return vars[name]}function isTruthy(value2){return value2===true||typeof value2==="string"&&value2.length>0}function renderTemplate(source,vars){let out=source;for(let round=0;round<MAX_NESTING;round++){let changed=false;out=out.replace(BLOCK_RE,(_match,kind,name,body)=>{changed=true;return kind==="#"===isTruthy(lookup(vars,name))?body:""});if(!changed)break}if(/\{\{[#^/]/.test(out))throw new Error("şablon bloğu kapanmamış ya da çok derin");return out.replace(VAR_RE,(_match,name)=>{const value2=lookup(vars,name);if(typeof value2!=="string")throw new Error(`şablon değişkeni metin değil: ${name}`);return value2})}var TEMPLATE_NAMES=["blank","envanter","bakim-takvimi","saha-denetimi","egitim-sertifika","ziyaretci-kaydi","uygunsuzluk-dof","musteri-ziyaret","hedef-takibi"];var DEFAULT_TEMPLATE="blank";var TEMPLATES=[{name:"blank",title:{tr:"Boş iskelet",en:"Blank scaffold"},summary:{tr:"Kişisel notlar örneğiyle en küçük başlangıç; hiçbir şablon uymuyorsa",en:"Smallest start with a personal-notes sample; when no template fits"}},{name:"envanter",title:{tr:"Envanter",en:"Inventory"},summary:{tr:"Stok kalemleri, adet ve konum, giriş/çıkış hareketleri, düşük stok uyarısı ve günlük kontrol",en:"Stock items, quantity and location, in/out movements, low-stock alert and daily check"}},{name:"bakim-takvimi",title:{tr:"Bakım takvimi",en:"Maintenance schedule"},summary:{tr:"Cihaz ve ekipmanların periyodik bakımı; sıradaki bakım tarihi sunucuda hesaplanır, yaklaşan ve geciken bakım sorumluya hatırlatılır",en:"Periodic maintenance of devices and equipment; the next due date is computed on the server, upcoming and overdue work is reminded"}},{name:"saha-denetimi",title:{tr:"Saha denetimi",en:"Field audit"},summary:{tr:"Mağaza ve şube denetimi için kontrol listesi; madde başına sonuç, fotoğraf, sunucuda hesaplanan puan ve yazdırılabilir rapor",en:"Checklist for store and branch audits; per-item result, photo, server-computed score and printable report"}},{name:"egitim-sertifika",title:{tr:"Eğitim ve sertifika takibi",en:"Training and certificate tracking"},summary:{tr:"Çalışanların eğitim ve sertifikaları; süresi dolmak üzere olanlar kişiye ve yöneticisine hatırlatılır, yönetici ekibini görür",en:"Employee trainings and certificates; expiring ones are reminded to the person and their manager, managers see their team"}},{name:"ziyaretci-kaydi",title:{tr:"Ziyaretçi kaydı",en:"Visitor log"},summary:{tr:"Resepsiyonda ziyaretçi girişi ve çıkışı; ziyaret edilen çalışana bildirim, kişisel veriler belirli bir süre sonra kendiliğinden silinir",en:"Visitor check-in and check-out at reception; the host is notified, personal data is deleted automatically after a set period"}},{name:"uygunsuzluk-dof",title:{tr:"Uygunsuzluk ve düzeltici faaliyet",en:"Nonconformity and corrective action"},summary:{tr:"Kalite ve iş güvenliği uygunsuzlukları; kök neden, sorumlu ve terminli düzeltici faaliyetler, geciken faaliyet hatırlatması",en:"Quality and safety nonconformities; root cause, corrective actions with owner and due date, overdue reminders"}},{name:"musteri-ziyaret",title:{tr:"Müşteri ziyaret raporu",en:"Customer visit report"},summary:{tr:"Saha ekibinin müşteri ziyaretleri; müşteri listesi ERP'ye bağlanabilir, ziyaret notu ve sonraki adım kaydedilir, yönetici ekibini görür",en:"Field team customer visits; the customer list can be connected to the ERP, visit notes and next steps are recorded, managers see their team"}},{name:"hedef-takibi",title:{tr:"Hedef ve aksiyon takibi",en:"Goal and action tracking"},summary:{tr:"Ekip hedefleri, ölçülebilir sonuçlar ve aksiyonlar; ilerleme sunucuda hesaplanır, dönem panosu ve Excel",en:"Team goals, measurable results and actions; progress is computed on the server, period board and Excel"}}];function isTemplateName(value2){return TEMPLATE_NAMES.includes(value2)}var FOLD_COLUMN_SUFFIX="_fold";var FOLD_PAIRS=[["Ç","c"],["ç","c"],["Ğ","g"],["ğ","g"],["İ","i"],["ı","i"],["Ö","o"],["ö","o"],["Ş","s"],["ş","s"],["Ü","u"],["ü","u"],["Â","a"],["â","a"],["Î","i"],["î","i"],["Û","u"],["û","u"]];var FOLD_MAP=new Map(FOLD_PAIRS);var FOLD_RE=new RegExp(`[${FOLD_PAIRS.map(([from])=>from).join("")}]`,"g");function foldColumnName(column){return`${column}${FOLD_COLUMN_SUFFIX}`}var SQL_FOLD_PAIRS=[...FOLD_PAIRS.map(([from,to])=>[from.normalize("NFD"),to]).filter(([from],i)=>from!==FOLD_PAIRS[i][0]),...FOLD_PAIRS];var DATA_FIELD_TYPES=["text","longText","number","money","date","datetime","boolean","choice","person","file"];var DATA_NAME_RE=/^[a-z][a-z0-9_]{0,39}$/;var DATA_CHOICE_VALUE_RE=/^[a-z0-9][a-z0-9_-]{0,39}$/;var DATA_MAX_ENTITIES=30;var DATA_MAX_FIELDS=60;var DATA_MAX_CHOICES=100;var DATA_TITLE_MAX_LENGTH=120;var TEXT_DEFAULT_MAX_LENGTH=200;var TEXT_MAX_LENGTH=1e3;var LONG_TEXT_DEFAULT_MAX_LENGTH=1e4;var LONG_TEXT_MAX_LENGTH=5e4;var DATA_MAX_COLUMNS=100;var SYSTEM_COLUMNS=["id","created_at","created_by","updated_at","updated_by","deleted_at"];var SQL_KEYWORDS=new Set("abort action add after all alter always analyze and as asc attach autoincrement before begin between by cascade case cast check collate column commit conflict constraint create cross current current_date current_time current_timestamp database default deferrable deferred delete desc detach distinct do drop each else end escape except exclude exclusive exists explain fail filter first following for foreign from full generated glob group groups having if ignore immediate in index indexed initially inner insert instead intersect into is isnull join key last left like limit match materialized natural no not nothing notnull null nulls of offset on or order others outer over partition plan pragma preceding primary query raise range recursive references regexp reindex release rename replace restrict returning right rollback row rows savepoint select set table temp temporary then ties to transaction trigger unbounded union unique update using vacuum values view virtual when where window with without rowid oid _rowid_".split(" "));var RESERVED_TABLE_PREFIXES=["sqlite_","d1_","cf_","one_"];var DataModelError=class extends Error{issues;constructor(issues){super(issues.length===1?issues[0]:`Veri modelinde ${issues.length} sorun var:
|
|
3
|
+
- ${issues.join("\n- ")}`);this.name="DataModelError";this.issues=issues}};var isRecord=v=>typeof v==="object"&&v!==null&&!Array.isArray(v);function nameIssue(name,kind){if(!DATA_NAME_RE.test(name))return`${kind} adı "${name}" geçersiz: küçük harfle başlamalı, yalnız küçük harf (a-z), rakam ve alt çizgi içermeli, en çok 40 karakter olmalı.`;if(name.includes("__")||name.endsWith("_"))return`${kind} adı "${name}" çift alt çizgi içeremez ve alt çizgiyle bitemez.`;if(SQL_KEYWORDS.has(name))return`${kind} adı "${name}" veritabanı dilinde ayrılmış bir sözcüktür; başka bir ad seçin (ör. "${name}_value").`;return null}function titleIssue(raw,where){if(!isRecord(raw))return`${where}: "title" { "tr": "…", "en": "…" } biçiminde olmalı.`;for(const lang of["tr","en"]){const v=raw[lang];if(typeof v!=="string"||v.trim()==="")return`${where}: "title.${lang}" boş olamaz (Türkçe ve İngilizce ad zorunlu).`;if(v.length>DATA_TITLE_MAX_LENGTH)return`${where}: "title.${lang}" en çok ${DATA_TITLE_MAX_LENGTH} karakter olabilir.`}for(const k of Object.keys(raw))if(k!=="tr"&&k!=="en")return`${where}: "title" içinde bilinmeyen dil "${k}" (yalnız "tr" ve "en").`;return null}var FIELD_KEYS=new Set(["type","title","required","unique","index","default","choices","maxLength","min","max"]);var ENTITY_KEYS=new Set(["title","fields","softDelete"]);var NOT_UNIQUE=new Set(["longText","boolean"]);var NOT_INDEXED=new Set(["longText"]);var NO_DEFAULT=new Set(["person","file"]);function moneyToMinor(raw){let text;if(typeof raw==="number"){if(!Number.isFinite(raw))return null;text=String(raw);if(/e/i.test(text))return null}else text=raw.trim();const m=/^(-)?(\d{1,16})(?:[.,](\d{1,2}))?$/.exec(text);if(!m)return null;const minor=Number(m[2])*100+Number((m[3]??"").padEnd(2,"0")||"0");if(!Number.isSafeInteger(minor))return null;return m[1]&&minor!==0?-minor:minor}function minorToMoney(minor){const abs=Math.abs(minor);const major=Math.floor(abs/100);const cents=String(abs%100).padStart(2,"0");return`${minor<0?"-":""}${major}.${cents}`}var DATE_RE=/^(\d{4})-(\d{2})-(\d{2})$/;var DATETIME_RE=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/;function isCalendarDate(text){const m=DATE_RE.exec(text);if(!m)return false;const[y,mo,d]=[Number(m[1]),Number(m[2]),Number(m[3])];const date=new Date(Date.UTC(y,mo-1,d));return y>=1&&date.getUTCFullYear()===y&&date.getUTCMonth()===mo-1&&date.getUTCDate()===d}function normalizeDateTime(text){if(!DATETIME_RE.test(text)||!isCalendarDate(text.slice(0,10)))return null;const t=Date.parse(text);if(!Number.isFinite(t))return null;return new Date(t).toISOString()}function defaultIssue(field2,value2,where){const bad=expected=>`${where}: "default" ${expected} olmalı.`;switch(field2.type){case"text":case"longText":return typeof value2==="string"?null:bad("metin");case"number":return typeof value2==="number"&&Number.isFinite(value2)?null:bad("sayı");case"money":return(typeof value2==="number"||typeof value2==="string")&&moneyToMinor(value2)!==null?null:bad("en çok iki ondalık basamaklı tutar (ör. 12.50)");case"boolean":return typeof value2==="boolean"?null:bad("true ya da false");case"date":return typeof value2==="string"&&isCalendarDate(value2)?null:bad("YYYY-AA-GG biçiminde tarih");case"datetime":return typeof value2==="string"&&normalizeDateTime(value2)!==null?null:bad("bölgeli ISO tarih-saat (ör. 2026-01-31T09:00:00Z)");case"choice":return typeof value2==="string"&&field2.choices.some(c=>c.value===value2)?null:bad('seçeneklerden birinin "value" değeri');default:return`${where}: bu alan türünde varsayılan verilemez.`}}function parseDataModel(raw){const issues=[];if(!isRecord(raw))throw new DataModelError(['one.data.json bir JSON nesnesi olmalı: { "version": 1, "entities": { … } }.']);if(raw.version!==1)issues.push('"version" 1 olmalı.');for(const k of Object.keys(raw))if(k!=="version"&&k!=="entities"&&k!=="$schema")issues.push(`Bilinmeyen üst alan "${k}" (yalnız "version" ve "entities").`);if(!isRecord(raw.entities)){issues.push('"entities" bir nesne olmalı: { "<varlık adı>": { "title": …, "fields": { … } } }.');throw new DataModelError(issues)}const entityNames=Object.keys(raw.entities);if(entityNames.length>DATA_MAX_ENTITIES)issues.push(`En çok ${DATA_MAX_ENTITIES} varlık tanımlanabilir (${entityNames.length}).`);const entities=[];for(const entityName of entityNames){const where=`"${entityName}"`;const ni=nameIssue(entityName,"varlık");if(ni)issues.push(ni);else if(RESERVED_TABLE_PREFIXES.some(p=>entityName.startsWith(p)))issues.push(`varlık adı "${entityName}" platform için ayrılmış bir önekle başlıyor (${RESERVED_TABLE_PREFIXES.join(", ")}).`);const e=raw.entities[entityName];if(!isRecord(e)){issues.push(`${where}: varlık bir nesne olmalı.`);continue}for(const k of Object.keys(e))if(!ENTITY_KEYS.has(k))issues.push(`${where}: bilinmeyen alan "${k}" (title · fields · softDelete).`);const ti=titleIssue(e.title,where);if(ti)issues.push(ti);if(e.softDelete!==void 0&&typeof e.softDelete!=="boolean")issues.push(`${where}: "softDelete" true ya da false olmalı.`);if(!isRecord(e.fields)){issues.push(`${where}: "fields" bir nesne olmalı.`);continue}const fieldNames=Object.keys(e.fields);if(fieldNames.length===0)issues.push(`${where}: en az bir alan tanımlanmalı.`);if(fieldNames.length>DATA_MAX_FIELDS)issues.push(`${where}: bir varlıkta en çok ${DATA_MAX_FIELDS} alan olabilir (${fieldNames.length}).`);const fields=[];const columns=new Set(SYSTEM_COLUMNS);for(const fieldName of fieldNames){const fw=`${where}.${fieldName}`;const fni=nameIssue(fieldName,"alan");if(fni)issues.push(fni);else if(SYSTEM_COLUMNS.includes(fieldName))issues.push(`alan adı "${fieldName}" platformun otomatik kolonudur (${SYSTEM_COLUMNS.join(", ")}); başka bir ad seçin.`);else if(fieldName.endsWith("_minor"))issues.push(`alan adı "${fieldName}" "_minor" ile bitemez (para alanlarının kolon eki).`);else if(fieldName.endsWith(FOLD_COLUMN_SUFFIX))issues.push(`alan adı "${fieldName}" "${FOLD_COLUMN_SUFFIX}" ile bitemez (metin alanlarının arama kolonu eki).`);const f=e.fields[fieldName];if(!isRecord(f)){issues.push(`${fw}: alan bir nesne olmalı.`);continue}for(const k of Object.keys(f))if(!FIELD_KEYS.has(k))issues.push(`${fw}: bilinmeyen özellik "${k}".`);const type=f.type;if(typeof type!=="string"||!DATA_FIELD_TYPES.includes(type)){issues.push(`${fw}: "type" şunlardan biri olmalı: ${DATA_FIELD_TYPES.join(", ")}.`);continue}const t=type;const fti=titleIssue(f.title,fw);if(fti)issues.push(fti);for(const flag of["required","unique","index"])if(f[flag]!==void 0&&typeof f[flag]!=="boolean")issues.push(`${fw}: "${flag}" true ya da false olmalı.`);const unique=f.unique===true;if(unique&&NOT_UNIQUE.has(t))issues.push(`${fw}: "${t}" türündeki alan tekil (unique) olamaz.`);if(f.index===true&&NOT_INDEXED.has(t))issues.push(`${fw}: "${t}" türündeki alana dizin (index) eklenemez.`);let choices=[];if(t==="choice"){if(!Array.isArray(f.choices)||f.choices.length===0)issues.push(`${fw}: seçim alanında "choices" en az bir seçenek içermeli.`);else if(f.choices.length>DATA_MAX_CHOICES)issues.push(`${fw}: en çok ${DATA_MAX_CHOICES} seçenek olabilir.`);else{const seen=new Set;f.choices.forEach((c,i)=>{const cw=`${fw}.choices[${i}]`;if(!isRecord(c)||typeof c.value!=="string"||!DATA_CHOICE_VALUE_RE.test(c.value)){issues.push(`${cw}: "value" küçük harf, rakam, "_" ya da "-" içeren en çok 40 karakterlik bir kod olmalı (ör. "beklemede").`);return}if(seen.has(c.value))issues.push(`${cw}: "${c.value}" değeri tekrar ediyor.`);seen.add(c.value);const cti=titleIssue(c.title,cw);if(cti)issues.push(cti);for(const k of Object.keys(c))if(k!=="value"&&k!=="title")issues.push(`${cw}: bilinmeyen özellik "${k}".`);if(!cti)choices.push({value:c.value,title:{tr:c.title.tr,en:c.title.en}})})}}else if(f.choices!==void 0)issues.push(`${fw}: "choices" yalnız seçim (choice) alanında kullanılır.`);let maxLength;if(t==="text"||t==="longText"){const cap=t==="text"?TEXT_MAX_LENGTH:LONG_TEXT_MAX_LENGTH;if(f.maxLength===void 0)maxLength=t==="text"?TEXT_DEFAULT_MAX_LENGTH:LONG_TEXT_DEFAULT_MAX_LENGTH;else if(typeof f.maxLength!=="number"||!Number.isInteger(f.maxLength)||f.maxLength<1||f.maxLength>cap)issues.push(`${fw}: "maxLength" 1 ile ${cap} arasında bir tamsayı olmalı.`);else maxLength=f.maxLength}else if(f.maxLength!==void 0)issues.push(`${fw}: "maxLength" yalnız metin alanlarında kullanılır.`);let min;let max;for(const bound of["min","max"]){const v=f[bound];if(v===void 0)continue;if(t!=="number"&&t!=="money"){issues.push(`${fw}: "${bound}" yalnız sayı ve para alanlarında kullanılır.`);continue}if(typeof v!=="number"||!Number.isFinite(v)||t==="money"&&moneyToMinor(v)===null)issues.push(`${fw}: "${bound}" geçerli bir sayı olmalı${t==="money"?" (en çok iki ondalık basamak)":""}.`);else if(bound==="min")min=v;else max=v}if(min!==void 0&&max!==void 0&&min>max)issues.push(`${fw}: "min" "max"tan büyük olamaz.`);let def;if(f.default!==void 0){if(NO_DEFAULT.has(t))issues.push(`${fw}: "${t}" türündeki alanda varsayılan verilemez.`);else if(unique)issues.push(`${fw}: tekil (unique) alanda varsayılan verilemez (her kayıt aynı değeri alırdı).`);else{const di=defaultIssue({type:t,choices},f.default,fw);if(di)issues.push(di);else if(t==="money")def=minorToMoney(moneyToMinor(f.default));else if(t==="datetime")def=normalizeDateTime(f.default);else def=f.default;if(def!==void 0&&(t==="number"||t==="money")){const n=t==="money"?moneyToMinor(def)/100:def;if(min!==void 0&&n<min||max!==void 0&&n>max)issues.push(`${fw}: "default" min/max aralığının dışında.`)}if(def!==void 0&&maxLength!==void 0&&[...String(def)].length>maxLength)issues.push(`${fw}: "default" "maxLength" sınırını aşıyor.`)}}const column=t==="money"?`${fieldName}_minor`:fieldName;if(columns.has(column))issues.push(`${fw}: kolon adı "${column}" başka bir kolonla çakışıyor.`);columns.add(column);const foldColumn=t==="text"||t==="longText"?foldColumnName(column):void 0;if(foldColumn!==void 0){if(columns.has(foldColumn))issues.push(`${fw}: arama kolonu "${foldColumn}" başka bir kolonla çakışıyor.`);columns.add(foldColumn)}if(!fti)fields.push({name:fieldName,column,type:t,title:{tr:f.title.tr,en:f.title.en},required:f.required===true,unique,index:f.index===true,default:def,choices,maxLength,foldColumn,min,max})}if(columns.size>DATA_MAX_COLUMNS)issues.push(`${where}: tabloda en çok ${DATA_MAX_COLUMNS} kolon olabilir; bu varlık ${columns.size} kolon gerektiriyor (${SYSTEM_COLUMNS.length} otomatik + alanlar + her metin alanı için bir arama kolonu). Alanları iki varlığa bölün.`);if(!ti)entities.push({name:entityName,title:{tr:e.title.tr,en:e.title.en},softDelete:e.softDelete!==false,fields})}if(issues.length>0)throw new DataModelError(issues);return{version:1,entities}}function toModelInput(model){const entities={};for(const e of model.entities){const fields={};for(const f of e.fields){const out={type:f.type,title:{...f.title}};if(f.required)out.required=true;if(f.unique)out.unique=true;if(f.index)out.index=true;if(f.default!==void 0)out.default=f.default;if(f.choices.length>0)out.choices=f.choices.map(c=>({value:c.value,title:{...c.title}}));if(f.maxLength!==void 0)out.maxLength=f.maxLength;if(f.min!==void 0)out.min=f.min;if(f.max!==void 0)out.max=f.max;fields[f.name]=out}entities[e.name]={title:{...e.title},fields,softDelete:e.softDelete}}return{version:1,entities}}var DATA_SNAPSHOT_FILE=".one-data-snapshot.json";var DATA_MODEL_FILE="one.data.json";var DATA_TYPES_FILE="src/data/types.ts";var MIGRATION_NAME_RE=/^(\d{4})_[a-z0-9_]+\.sql$/;var q=name=>`"${name}"`;function sqlType(f){switch(f.type){case"number":return"REAL";case"money":case"boolean":return"INTEGER";default:return"TEXT"}}function literal(f,value2){if(f.type==="boolean")return value2===true?"1":"0";if(f.type==="money")return String(moneyToMinor(value2));if(f.type==="number")return String(value2);return`'${String(value2).replace(/'/g,"''")}'`}function checkClause(f){if(f.type==="boolean")return` CHECK (${q(f.column)} IN (0, 1))`;return""}function foldColumnDef(f){return f.foldColumn===void 0?null:`${q(f.foldColumn)} TEXT`}function columnDef(f,adding){let sql=`${q(f.column)} ${sqlType(f)}`;if(f.required){sql+=" NOT NULL";if(adding&&f.default!==void 0)sql+=` DEFAULT ${literal(f,f.default)}`}return sql+checkClause(f)}var indexName=(kind,entity,field2)=>`${kind}_${entity}__${field2}`;function indexSql(e,f){if(f.unique)return`CREATE UNIQUE INDEX ${q(indexName("ux",e.name,f.name))} ON ${q(e.name)} (${q(f.column)}) WHERE "deleted_at" IS NULL;`;if(f.index)return`CREATE INDEX ${q(indexName("ix",e.name,f.name))} ON ${q(e.name)} (${q(f.column)});`;return null}function createEntitySql(e){const cols=['"id" TEXT NOT NULL PRIMARY KEY','"created_at" TEXT NOT NULL','"created_by" TEXT NOT NULL','"updated_at" TEXT NOT NULL','"updated_by" TEXT NOT NULL','"deleted_at" TEXT',...e.fields.flatMap(f=>{const fold=foldColumnDef(f);return fold===null?[columnDef(f,false)]:[columnDef(f,false),fold]})];const out=[`CREATE TABLE ${q(e.name)} (
|
|
4
|
+
${cols.join(",\n ")}
|
|
5
|
+
);`];out.push(`CREATE INDEX ${q(`ix_${e.name}__created_at`)} ON ${q(e.name)} ("created_at") WHERE "deleted_at" IS NULL;`);for(const f of e.fields){const ix=indexSql(e,f);if(ix)out.push(ix)}return out}var CURRENT={foldColumns:true,choiceChecks:new Set};function diffDataModels(prev,next,legacy=CURRENT){const statements=[];const changes=[];const errors=[];const prevEntities=new Map((prev?.entities??[]).map(e=>[e.name,e]));const nextNames=new Set(next.entities.map(e=>e.name));for(const old of prevEntities.values())if(!nextNames.has(old.name))errors.push(`"${old.name}" varlığı modelden kaldırılmış. Tablo ve verisi otomatik silinmez; varlığı one.data.json'a geri ekleyin (kullanmıyorsanız ekranlardan kaldırmanız yeterli).`);for(const e of next.entities){const old=prevEntities.get(e.name);if(!old){statements.push(`-- ${e.name}: ${e.title.tr}`,...createEntitySql(e));changes.push({kind:"create-table",entity:e.name});continue}const oldFields=new Map(old.fields.map(f=>[f.name,f]));const nextFieldNames=new Set(e.fields.map(f=>f.name));for(const of of old.fields)if(!nextFieldNames.has(of.name))errors.push(`"${e.name}.${of.name}" alanı kaldırılmış ya da adı değiştirilmiş. Kolon ve verisi otomatik silinmez/yeniden adlandırılmaz; alanı geri ekleyin (yeni bir ad gerekiyorsa yeni bir alan açın, eskisini kullanmayı bırakın).`);const added=[];for(const f of e.fields){const of=oldFields.get(f.name);const where=`"${e.name}.${f.name}"`;if(!of){if(f.required&&f.default===void 0)errors.push(`${where} zorunlu bir alan olarak eklenmiş ama varsayılanı yok: var olan kayıtlar bu alanı boş bırakamaz. Ya "default" verin ya da alanı isteğe bağlı ekleyin.`);if(f.unique&&f.default!==void 0)errors.push(`${where} tekil ve varsayılanlı eklenemez (var olan kayıtlar aynı değeri alırdı).`);if(f.unique&&f.required)errors.push(`${where} var olan bir tabloya tekil VE zorunlu eklenemez; isteğe bağlı tekil ekleyin.`);added.push(`ALTER TABLE ${q(e.name)} ADD COLUMN ${columnDef(f,true)};`);const fold=foldColumnDef(f);if(fold)added.push(`ALTER TABLE ${q(e.name)} ADD COLUMN ${fold};`);const ix=indexSql(e,f);if(ix)added.push(ix);changes.push({kind:"add-column",entity:e.name,field:f.name});continue}if(of.type!==f.type)errors.push(`${where} alanının türü "${of.type}" → "${f.type}" değiştirilmiş; tür değişikliği desteklenmez (yeni bir alan açın).`);if(of.required!==f.required)errors.push(`${where} alanı ${f.required?"zorunlu yapılmış":"isteğe bağlı yapılmış"}; var olan kolonun bu kuralı değiştirilemez (yeni bir alan açın ya da eski hâle döndürün).`);if(of.unique!==f.unique)errors.push(`${where} alanının tekillik (unique) kuralı değiştirilmiş; var olan kolonda desteklenmez.`);if(of.index&&!f.index)errors.push(`${where} alanının dizini (index) kaldırılmış; dizin silme desteklenmez, "index": true kalmalı.`);if(!of.index&&f.index&&!f.unique){added.push(indexSql(e,f));changes.push({kind:"add-index",entity:e.name,field:f.name})}if(of.type===f.type&&f.type==="choice"){const nextValues=new Set(f.choices.map(c=>c.value));const removed=of.choices.map(c=>c.value).filter(v=>!nextValues.has(v));const oldValues=new Set(of.choices.map(c=>c.value));const addedValues=f.choices.map(c=>c.value).filter(v=>!oldValues.has(v));if(removed.length>0)errors.push(`${where} seçim alanından değer kaldırılmış (${removed.join(", ")}). Var olan kayıtlar bu değeri taşıyabilir; değeri listede bırakın (artık kullanılmayacaksa ekranlardan gizleyin). Yalnız görünen adlar (title) ve sıra değişebilir.`);else if(addedValues.length>0&&legacy.choiceChecks.has(`${e.name}.${f.name}`))errors.push(`${where} seçim alanına değer eklenmiş (${addedValues.join(", ")}), ama bu alanın tablosu eski bir sürümle kurulmuş ve veritabanında değerleri bir kısıtla sabitlenmiş; kısıt ekleme yapan bir migration ile kaldırılamaz. Yeni değerler için yeni bir seçim alanı açın ya da platform ekibine danışın.`)}if(of.type===f.type&&f.foldColumn!==void 0&&!legacy.foldColumns){added.push(`ALTER TABLE ${q(e.name)} ADD COLUMN ${foldColumnDef(f)};`);changes.push({kind:"add-search-column",entity:e.name,field:f.name})}}if(added.length>0)statements.push(`-- ${e.name}: ${e.title.tr}`,...added)}return{statements,changes,errors}}var DATA_SNAPSHOT_VERSION=2;var isLegacyKey=v=>typeof v==="string"&&/^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/.test(v);function readSnapshot(raw){if(raw===null)return null;const broken=why=>new DataModelError([`migrations/${DATA_SNAPSHOT_FILE} ${why} — dosya elle düzenlenmez; git'teki son hâline döndürün.`]);let json;try{json=JSON.parse(raw)}catch{throw new DataModelError([`migrations/${DATA_SNAPSHOT_FILE} okunamadı (geçerli JSON değil). Bu dosya elle düzenlenmez; git'teki son hâline döndürün.`])}const doc2=json??{};let model;try{model=parseDataModel(doc2.model)}catch(e){if(e instanceof DataModelError)throw broken(`geçersiz: ${e.issues.join(" · ")}`);throw e}if(doc2.v===void 0||doc2.v===1){const checks=new Set;for(const e of model.entities)for(const f of e.fields)if(f.type==="choice")checks.add(`${e.name}.${f.name}`);return{model,legacy:{foldColumns:false,choiceChecks:checks}}}if(doc2.v!==DATA_SNAPSHOT_VERSION)throw broken(`bu SDK'nın tanımadığı bir sürümde (v: ${JSON.stringify(doc2.v)}); SDK'yı güncelleyin`);const raw2=doc2.choiceChecks??[];if(!Array.isArray(raw2)||!raw2.every(isLegacyKey))throw broken('geçersiz: "choiceChecks" "<varlık>.<alan>" listesi olmalı');return{model,legacy:{foldColumns:true,choiceChecks:new Set(raw2)}}}function renderSnapshot(model,legacy=CURRENT){const present=new Set(model.entities.flatMap(e=>e.fields.filter(f=>f.type==="choice").map(f=>`${e.name}.${f.name}`)));const choiceChecks=[...legacy.choiceChecks].filter(k=>present.has(k)).sort();return JSON.stringify({note:"one data generate tarafından yazılır; elle düzenlemeyin. Bir sonraki üretimin farkı bu modele göre alınır.",v:DATA_SNAPSHOT_VERSION,...choiceChecks.length>0?{choiceChecks}:{},model:toModelInput(model)},null,2)+"\n"}function nextMigrationName(existing2){let max=0;for(const name of existing2){const m=MIGRATION_NAME_RE.exec(name);if(m)max=Math.max(max,Number(m[1]))}if(max>=9999)throw new DataModelError(["migrations/ altında 9999 numarasına ulaşılmış; yeni numara verilemiyor."]);return`${String(max+1).padStart(4,"0")}_one_data.sql`}function tablesCreatedIn(sql){const text=sql.replace(/--[^\n]*/g," ").replace(/\/\*[\s\S]*?\*\//g," ");const out=new Set;const re=/\bcreate\s+(?:temp\s+|temporary\s+|virtual\s+)?table\s+(?:if\s+not\s+exists\s+)?(?:main\s*\.\s*)?["`[]?([A-Za-z0-9_]+)["`\]]?/gi;for(const m of text.matchAll(re))out.add(m[1].toLowerCase());return out}function planDataGeneration(input){const next=parseDataModel(input.model);const prev=readSnapshot(input.snapshot);const legacy=prev?.legacy??CURRENT;const diff=diffDataModels(prev?.model??null,next,legacy);const issues=[...diff.errors];const handwritten=new Set;for(const m of input.existing)for(const t of tablesCreatedIn(m.sql))handwritten.add(t);for(const c of diff.changes)if(c.kind==="create-table"&&handwritten.has(c.entity))issues.push(`"${c.entity}" adında bir tablo elle yazılmış bir migration'da zaten kuruluyor; varlığa başka bir ad verin.`);if(issues.length>0)throw new DataModelError(issues);const migration=diff.statements.length===0?null:{name:nextMigrationName(input.existing.map(m=>m.name)),sql:"-- one data generate tarafından one.data.json'dan üretildi.\n-- ⛔ Uygulandıktan sonra bu dosya DEĞİŞTİRİLMEZ: model değişince bir sonraki numarayla yeni dosya üretilir.\n-- Her kurulumun kendi veritabanı vardır; tablolarda portal kolonu yoktur. Kişi = kullanıcı kimliği (sub).\n"+diff.statements.join("\n")+"\n"};return{migration,types:renderTypes(next),snapshot:renderSnapshot(next,legacy),changes:diff.changes}}var pascal=name=>name.split("_").filter(Boolean).map(p=>p[0].toUpperCase()+p.slice(1)).join("");function tsValueType(f){switch(f.type){case"number":return"number";case"boolean":return"boolean";case"choice":return f.choices.map(c=>JSON.stringify(c.value)).join(" | ");default:return"string"}}function tsInputType(f){if(f.type==="money")return"string | number";if(f.type==="number")return"number | string";return tsValueType(f)}var doc=f=>{const unit=f.type==="money"?' — tutar, ondalıklı metin ("1234.50")':f.type==="date"?" — YYYY-AA-GG":f.type==="datetime"?" — ISO tarih-saat (UTC)":f.type==="person"?" — kişi (kullanıcı kimliği, sub)":f.type==="file"?" — dosya anahtarı":"";return` /** ${f.title.tr}${unit} */
|
|
6
|
+
`};function renderTypes(model){const lines=["// one data generate tarafından one.data.json'dan üretilir; elle düzenlemeyin (değişiklik one.data.json'a yazılır).","import type { OneDataModel } from '@devoplus/one-sdk'","",`export type EntityName = ${model.entities.length>0?model.entities.map(e=>JSON.stringify(e.name)).join(" | "):"never"}`,""];for(const e of model.entities){const name=pascal(e.name);lines.push(`/** ${e.title.tr} — API'nin döndürdüğü kayıt. */`,`export interface ${name}Record {`);lines.push(" id: string"," created_at: string"," created_by: string"," updated_at: string"," updated_by: string");for(const f of e.fields)lines.push(doc(f).trimEnd(),` ${f.name}: ${tsValueType(f)}${f.required?"":" | null"}`);lines.push("}","",`/** ${e.title.tr} — kayıt oluştururken gönderilen alanlar (güncellemede hepsi isteğe bağlıdır). */`,`export interface ${name}Input {`);for(const f of e.fields){const optional=!f.required||f.default!==void 0;lines.push(` ${f.name}${optional?"?":""}: ${tsInputType(f)}${f.required?"":" | null"}`)}lines.push("}","")}lines.push("/** Doğrulanmış veri modeli — createCrudHandler({ model: dataModel, … }) ile kullanılır. */",`export const dataModel: OneDataModel = ${JSON.stringify(toModelInput(model),null,2)}`,"");return lines.join("\n")}var SERVICES=["database","files","cache"];var DEFAULT_SDK_SPEC="^0.2.0-next.0";var DEFAULT_VERSION="0.1.0";var DEV_DEPENDENCIES={typescript:"^7.0.2",vitest:"^5.0.4"};var SEMVER_RE=/^\d+\.\d+\.\d+$/;var LOCALIZED_TEXT_MAX=120;var DESCRIPTION_SUFFIX_TR=" — Devoplus One uygulaması";var DESCRIPTION_SUFFIX_EN=" — Devoplus One app";var PERMISSION_NAME_SUFFIX_TR=" uygulamasını kullan";var PERMISSION_NAME_PREFIX_EN="Use the ";var PERMISSION_NAME_SUFFIX_EN=" app";var NAME_MAX=LOCALIZED_TEXT_MAX-Math.max(DESCRIPTION_SUFFIX_TR.length,DESCRIPTION_SUFFIX_EN.length,PERMISSION_NAME_SUFFIX_TR.length,PERMISSION_NAME_PREFIX_EN.length+PERMISSION_NAME_SUFFIX_EN.length);var TSCONFIG={compilerOptions:{target:"ES2022",module:"ESNext",moduleResolution:"Bundler",lib:["ES2022","WebWorker"],types:[],strict:true,noUncheckedIndexedAccess:true,exactOptionalPropertyTypes:true,noImplicitOverride:true,noFallthroughCasesInSwitch:true,isolatedModules:true,verbatimModuleSyntax:true,skipLibCheck:true,noEmit:true,allowImportingTsExtensions:true,resolveJsonModule:true},include:["src"]};var GITIGNORE="node_modules/\ndist/\n.one/\n.dev.vars*\n.env*\n";function template(name){return readFileSync(new URL(`../templates/${name}`,import.meta.url),"utf8")}function pascalCase(slug2){return slug2.split("-").filter(Boolean).map((part,index)=>{if(index>0&&(/^[0-9]/.test(part)||part.startsWith("x")))return`X${part}`;return part.charAt(0).toUpperCase()+part.slice(1)}).join("")}function titleCase(slug2){return slug2.split("-").filter(Boolean).map(part=>part.charAt(0).toUpperCase()+part.slice(1)).join(" ")}function permissionCodeFor(slug2){return`One.${pascalCase(slug2)}.Use`}function requireText(value2,label,max){const trimmed=value2.trim();if(trimmed.length===0)throw new Error(`${label} boş olamaz`);if(trimmed.length>max)throw new Error(`${label} en çok ${max} karakter olabilir (${trimmed.length})`);return trimmed}function buildScaffold(options){const slug2=options.slug.trim();if(!isValidSlug(slug2))throw new Error(`slug geçersiz: "${slug2}" — küçük harf/rakam ve tek tire, harfle başlar, en çok ${SLUG_MAX} karakter`);const nameTr2=requireText(options.nameTr,"ad (TR)",NAME_MAX);const nameEn2=options.nameEn===void 0?void 0:requireText(options.nameEn,"ad (EN)",NAME_MAX);const version2=options.version??DEFAULT_VERSION;if(!SEMVER_RE.test(version2))throw new Error(`sürüm semver (x.y.z) olmalı: "${version2}"`);const sdkSpec2=options.sdkSpec?.trim()||DEFAULT_SDK_SPEC;const services=new Set(["database",...options.services??[]]);for(const s of services)if(!SERVICES.includes(s))throw new Error(`bilinmeyen servis: ${s} (database · files · cache)`);const withFiles=services.has("files");const withCache=services.has("cache");const scopes2=[...new Set((options.scopes??[]).map(s=>s.trim()).filter(Boolean))];for(const scope of scopes2)if(!SCOPE_RE.test(scope))throw new Error(`scope biçimi geçersiz: "${scope}" (<modül>.<read|write|manage|execute>)`);if(scopes2.includes(DEXA_READ_SCOPE))throw new Error(`"${DEXA_READ_SCOPE}" scope'u iskeletle verilemez: one.manifest.json'da services.dexa.queryKeys ile birlikte beyan edilir ve anahtarlar uydurulmaz. İskeleti bu scope olmadan üretin; sonra manifest'e scope'u ve Devoplus Console kayıt defterindeki anahtarları ekleyin.`);if(scopes2.includes(DEXA_WRITE_SCOPE))throw new Error(`"${DEXA_WRITE_SCOPE}" scope'u iskeletle verilemez: one.manifest.json'da services.dexa.commandKeys ile birlikte beyan edilir ve ERP işlem anahtarları uydurulmaz. İskeleti bu scope olmadan üretin; sonra manifest'e scope'u ve Devoplus Console kayıt defterindeki işlem anahtarlarını ekleyin.`);const permissionCode3=options.permissionCode?.trim()||permissionCodeFor(slug2);if(!PERMISSION_CODE_RE.test(permissionCode3))throw new Error(`izin kodu geçersiz: "${permissionCode3}" — biçim One.<Ad>.<Ad> (büyük harfle başlayan harf/rakam parçaları), örnek ${permissionCodeFor(slug2)}`);const packageName=`one-app-${slug2}`;const templateName=options.template??DEFAULT_TEMPLATE;if(!isTemplateName(templateName))throw new Error(`bilinmeyen şablon: ${String(templateName)} (${TEMPLATE_NAMES.join(" · ")})`);if(templateName!=="blank")return buildTemplateApp({templateName,slug:slug2,nameTr:nameTr2,nameEn:nameEn2,version:version2,sdkSpec:sdkSpec2,services,scopes:scopes2,permissionCode:permissionCode3,packageName,ownerEmail:options.ownerEmail.trim()});const manifest={schemaVersion:1,app:{slug:slug2,name:nameEn2?{tr:nameTr2,en:nameEn2}:{tr:nameTr2},version:version2,description:nameEn2?{tr:`${nameTr2}${DESCRIPTION_SUFFIX_TR}`,en:`${nameEn2}${DESCRIPTION_SUFFIX_EN}`}:{tr:`${nameTr2}${DESCRIPTION_SUFFIX_TR}`},owner:{email:options.ownerEmail.trim()}},entry:"src/index.ts",scopes:scopes2,services:{database:true,files:withFiles,cache:withCache},menu:{title:nameEn2?{tr:nameTr2,en:nameEn2}:{tr:nameTr2},icon:"pi pi-box"},navigation:[{path:"/",label:{tr:"Ana sayfa",en:"Home"}}],actingSessions:"read",permission:{code:permissionCode3,name:nameEn2?{tr:`${nameTr2}${PERMISSION_NAME_SUFFIX_TR}`,en:`${PERMISSION_NAME_PREFIX_EN}${nameEn2}${PERMISSION_NAME_SUFFIX_EN}`}:{tr:`${nameTr2}${PERMISSION_NAME_SUFFIX_TR}`}},limits:{cpuMs:50,subRequests:10},agent:{summary:nameEn2?{tr:`${nameTr2}: kullanıcının kişisel notları.`,en:`${nameEn2}: the user's personal notes.`}:{tr:`${nameTr2}: kullanıcının kişisel notları.`},tools:[{name:"list-notes",title:{tr:"Notlarımı listele",en:"List my notes"},description:{tr:"Kullanıcının kendi notlarını en yenisi önce döndürür. Salt okunur; hiçbir şeyi değiştirmez.",en:"Returns the user's own notes, newest first. Read-only; changes nothing."},readOnly:true,permission:permissionCode3,input:{type:"object",properties:{limit:{type:"integer",minimum:1,maximum:50,description:"En çok kaç not (varsayılan 10)"}}}}]}};const validation=validateManifest(manifest);if(!validation.ok){const detail=validation.errors.map(e=>`${e.path}: ${e.message}`).join("; ");throw new Error(`üretilen manifest geçersiz — ${detail}`)}const vars={slug:slug2,nameTr:nameTr2,nameEn:nameEn2??"",nameTrJs:JSON.stringify(nameTr2),nameEnJs:JSON.stringify(nameEn2??nameTr2),packageName,permissionCode:permissionCode3,ownerEmail:options.ownerEmail.trim(),scopesList:scopes2.length>0?scopes2.map(s=>`\`${s}\``).join(", "):"",files:withFiles,cache:withCache,hasScopes:scopes2.length>0};const packageJson={name:packageName,version:version2,private:true,description:`${nameTr2}${DESCRIPTION_SUFFIX_TR}`,type:"module",scripts:{"type-check":"tsc -p tsconfig.json",test:"vitest run",build:"one build",migrate:"one migrate",dev:"one dev"},dependencies:{"@devoplus/one-sdk":sdkSpec2},devDependencies:{...DEV_DEPENDENCIES}};const json=value2=>JSON.stringify(value2,null,2)+"\n";const files=[{path:"one.manifest.json",content:json(manifest)},{path:"package.json",content:json(packageJson)},{path:"tsconfig.json",content:json(TSCONFIG)},{path:".gitignore",content:GITIGNORE},{path:"src/index.ts",content:renderTemplate(template("index.ts.tpl"),vars)},{path:"src/shell.ts",content:renderTemplate(template("shell.ts.tpl"),vars)},{path:"src/agent.ts",content:renderTemplate(template("agent.ts.tpl"),vars)},{path:"src/markdown.d.ts",content:renderTemplate(template("markdown.d.ts.tpl"),vars)},{path:"AGENT_GUIDE.md",content:renderTemplate(template("AGENT_GUIDE.md.tpl"),vars)},{path:"src/shell.test.ts",content:renderTemplate(template("shell.test.ts.tpl"),vars)},{path:"migrations/0001_init.sql",content:renderTemplate(template("0001_init.sql.tpl"),vars)},{path:"AGENTS.md",content:renderTemplate(template("AGENTS.md.tpl"),vars)},{path:"README.md",content:renderTemplate(template("README.md.tpl"),vars)}];return{files,manifest,packageName,permissionCode:permissionCode3,template:"blank"}}var TEMPLATES_ROOT=new URL("../templates/",import.meta.url);function walk(dir,prefix=""){const out=[];for(const name of readdirSync(dir).sort()){const rel=prefix?`${prefix}/${name}`:name;if(statSync(new URL(name,dir)).isDirectory())out.push(...walk(new URL(`${name}/`,dir),rel));else out.push(rel)}return out}function templateFiles(dir,vars,skip){return walk(dir).filter(rel=>!skip(rel)).map(rel=>{const raw=readFileSync(new URL(rel,dir),"utf8");return rel.endsWith(".tpl")?{path:rel.slice(0,-".tpl".length),content:renderTemplate(raw,vars)}:{path:rel,content:raw}})}function composeAgents(common,section){const at=common.indexOf("\n## ");if(at<0)throw new Error("AGENTS.md şablonunda ikinci düzey başlık yok");return`${common.slice(0,at+1)}${section.trimEnd()}
|
|
7
|
+
|
|
8
|
+
${common.slice(at+1)}`}function buildTemplateApp(input){const{templateName,slug:slug2,nameTr:nameTr2,nameEn:nameEn2,version:version2,sdkSpec:sdkSpec2,permissionCode:permissionCode3,packageName,ownerEmail:ownerEmail2}=input;const permissionPrefix=permissionCode3.slice(0,permissionCode3.lastIndexOf(".")+1);const baseVars={slug:slug2,nameTr:nameTr2,nameEn:nameEn2??"",nameTrJs:JSON.stringify(nameTr2),nameEnJs:JSON.stringify(nameEn2??nameTr2),packageName,permissionCode:permissionCode3,permissionPrefix,ownerEmail:ownerEmail2,templateName};const dir=new URL(`${templateName}/`,TEMPLATES_ROOT);const fragment=JSON.parse(renderTemplate(readFileSync(new URL("manifest.json.tpl",dir),"utf8"),baseVars));const services=new Set(input.services);for(const s of SERVICES)if(fragment.services[s])services.add(s);const scopes2=[...new Set([...fragment.scopes,...input.scopes])];const withFiles=services.has("files");const withCache=services.has("cache");const withMail=scopes2.includes("mail.execute");const manifest={schemaVersion:1,app:{slug:slug2,name:nameEn2?{tr:nameTr2,en:nameEn2}:{tr:nameTr2},version:version2,description:nameEn2?{tr:`${nameTr2}${DESCRIPTION_SUFFIX_TR}`,en:`${nameEn2}${DESCRIPTION_SUFFIX_EN}`}:{tr:`${nameTr2}${DESCRIPTION_SUFFIX_TR}`},owner:{email:ownerEmail2}},entry:"src/index.ts",scopes:scopes2,services:{database:true,files:withFiles,cache:withCache},menu:{title:nameEn2?{tr:nameTr2,en:nameEn2}:{tr:nameTr2},icon:fragment.menuIcon},navigation:fragment.navigation,actingSessions:"read",permission:{code:permissionCode3,name:nameEn2?{tr:`${nameTr2}${PERMISSION_NAME_SUFFIX_TR}`,en:`${PERMISSION_NAME_PREFIX_EN}${nameEn2}${PERMISSION_NAME_SUFFIX_EN}`}:{tr:`${nameTr2}${PERMISSION_NAME_SUFFIX_TR}`}},...fragment.permissions?{permissions:fragment.permissions}:{},limits:{cpuMs:50,subRequests:10},agent:fragment.agent,...fragment.jobs?{jobs:fragment.jobs}:{}};const validation=validateManifest(manifest);if(!validation.ok){const detail=validation.errors.map(e=>`${e.path}: ${e.message}`).join("; ");throw new Error(`üretilen manifest geçersiz (${templateName}) — ${detail}`)}const vars={...baseVars,scopesList:scopes2.length>0?scopes2.map(s=>`\`${s}\``).join(", "):"",files:withFiles,cache:withCache,hasScopes:scopes2.length>0};const shared=templateFiles(new URL("_shared/files/",TEMPLATES_ROOT),vars,rel=>rel==="src/lib/mail.ts.tpl"&&!withMail||rel==="src/lib/files.ts.tpl"&&!withFiles);const own=templateFiles(new URL("files/",dir),vars,()=>false);const dataModel=own.find(f=>f.path===DATA_MODEL_FILE);if(!dataModel)throw new Error(`${templateName} şablonunda ${DATA_MODEL_FILE} yok`);const plan=planDataGeneration({model:JSON.parse(dataModel.content),snapshot:null,existing:[]});if(!plan.migration)throw new Error(`${templateName} veri modeli migration üretmedi`);const packageJson={name:packageName,version:version2,private:true,description:`${nameTr2}${DESCRIPTION_SUFFIX_TR}`,type:"module",scripts:{"type-check":"tsc -p tsconfig.json",test:"vitest run",build:"one build",migrate:"one migrate",dev:"one dev","data:generate":"one data generate"},dependencies:{"@devoplus/one-sdk":sdkSpec2},devDependencies:{...DEV_DEPENDENCIES}};const json=value2=>JSON.stringify(value2,null,2)+"\n";const files=[{path:"one.manifest.json",content:json(manifest)},{path:"package.json",content:json(packageJson)},{path:"tsconfig.json",content:json(TSCONFIG)},{path:".gitignore",content:GITIGNORE},{path:"src/markdown.d.ts",content:renderTemplate(template("markdown.d.ts.tpl"),vars)},...shared,...own,{path:`migrations/${plan.migration.name}`,content:plan.migration.sql},{path:`migrations/${DATA_SNAPSHOT_FILE}`,content:plan.snapshot},{path:DATA_TYPES_FILE,content:plan.types},{path:"AGENTS.md",content:composeAgents(renderTemplate(template("AGENTS.md.tpl"),vars),`${renderTemplate(readFileSync(new URL("AGENTS.template.md.tpl",dir),"utf8"),vars).trimEnd()}
|
|
9
|
+
|
|
10
|
+
${renderTemplate(template("_shared/AGENTS.writes.md.tpl"),vars)}`)},{path:"README.md",content:renderTemplate(readFileSync(new URL("README.md.tpl",dir),"utf8"),vars).trimEnd()+"\n"+renderTemplate(template("_shared/README.tail.md.tpl"),vars)}];const seen=new Set;for(const f of files){if(seen.has(f.path))throw new Error(`${templateName} şablonunda aynı dosya iki kez üretildi: ${f.path}`);seen.add(f.path)}return{files,manifest,packageName,permissionCode:permissionCode3,template:templateName}}function fail(message){console.error(`create-devoplus-one-app: ${message}`);process.exit(1)}function arg(name){const i=process.argv.indexOf(`--${name}`);return i>=0?process.argv[i+1]:void 0}if(process.argv.includes("--list-templates")){for(const t of TEMPLATES)console.log(`${t.name.padEnd(12)} ${t.title.tr} — ${t.summary.tr}`);process.exit(0)}var[targetArg]=process.argv.slice(2).filter((a,i,all)=>!a.startsWith("--")&&(i===0||!all[i-1]?.startsWith("--")));if(!targetArg)fail("kullanım: create-devoplus-one-app <hedefDizin> --slug <slug> --owner <e-posta> [--template <ad> --name … --name-en … --services database,files,cache --scopes … --permission-code … --sdk … --version … --force]");var targetDir=resolve(targetArg);var slug=arg("slug")??basename(targetDir);var ownerEmail=arg("owner");if(!ownerEmail)fail("--owner <e-posta> gerekli (manifest app.owner.email; uydurulmaz)");var nameTr=arg("name")??titleCase(slug);var nameEn=arg("name-en");var servicesRaw=(arg("services")??"database").split(",").map(s=>s.trim()).filter(Boolean);for(const s of servicesRaw)if(!SERVICES.includes(s))fail(`--services bilinmeyen değer: ${s} (${SERVICES.join(" · ")})`);var scopes=(arg("scopes")??"").split(",").map(s=>s.trim()).filter(Boolean);var permissionCode2=arg("permission-code");var sdkArg=arg("sdk");var sdkSpec=sdkArg==="workspace"?"workspace:*":sdkArg;var version=arg("version");var force=process.argv.includes("--force");var templateArg=arg("template")??DEFAULT_TEMPLATE;if(!isTemplateName(templateArg))fail(`--template bilinmeyen şablon: ${templateArg} (${TEMPLATE_NAMES.join(" · ")}; liste: --list-templates)`);var existing=await stat(targetDir).catch(()=>null);if(existing&&!existing.isDirectory())fail(`hedef bir dizin değil: ${targetDir}`);if(existing&&(await readdir(targetDir)).length>0&&!force)fail(`hedef dizin boş değil: ${targetDir} (ezmek için --force)`);var scaffold;try{scaffold=buildScaffold({slug,nameTr,...nameEn!==void 0?{nameEn}:{},ownerEmail,services:servicesRaw,scopes,...permissionCode2!==void 0?{permissionCode:permissionCode2}:{},...sdkSpec!==void 0?{sdkSpec}:{},...version!==void 0?{version}:{},template:templateArg})}catch(error){fail(error instanceof Error?error.message:String(error))}for(const file of scaffold.files){const path=join2(targetDir,file.path);await mkdir(dirname(path),{recursive:true});await writeFile(path,file.content,"utf8");console.log(` + ${file.path}`)}console.log(`
|
|
11
|
+
✓ ${scaffold.packageName} üretildi (şablon: ${scaffold.template}) → ${targetDir}
|
|
12
|
+
|
|
13
|
+
Sonraki adımlar:
|
|
14
|
+
1. cd ${targetDir} && pnpm install && pnpm type-check && pnpm test
|
|
15
|
+
2. AGENTS.md'yi okuyun (uygulamayı yazacak AI aracı da okur) ve README.md'deki kayıt/kurulum/yayın adımlarını izleyin.
|
|
16
|
+
3. İzin kodu ${scaffold.permissionCode} — Devoplus Console › Devoplus One Uygulamaları ekranındaki kayıt bununla açılır.
|
|
17
|
+
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-devoplus-one-app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0-next.158",
|
|
4
|
+
"description": "Yeni bir Devoplus One uygulaması iskeleti üretir: manifest, SDK'lı giriş noktası, Portal köprülü kabuk, veritabanı migration'ı ve AGENTS.md",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-devoplus-one-app": "./dist/cli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"templates",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"devoplus",
|
|
23
|
+
"devoplus-one",
|
|
24
|
+
"create"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"zod": "^4.6.7"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^26.6.5",
|
|
31
|
+
"esbuild": "0.28.2",
|
|
32
|
+
"@devoplus/one-manifest": "0.1.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"type-check": "tsc -p tsconfig.json",
|
|
36
|
+
"build": "node scripts/build.mjs"
|
|
37
|
+
}
|
|
6
38
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- {{nameTr}} — veritabanı şeması. Her kurulumun KENDİ veritabanı vardır (D-17): tabloda PortalId kolonu YOKTUR, izolasyon
|
|
2
|
+
-- veritabanı sınırındadır. Şema değişikliği: bu dosya DEĞİŞTİRİLMEZ; yeni numaralı dosya eklenir (0002_….sql).
|
|
3
|
+
-- one-ci migration'ları sürümle birlikte, sırayla uygular.
|
|
4
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
5
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
6
|
+
sub TEXT NOT NULL,
|
|
7
|
+
text TEXT NOT NULL,
|
|
8
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX IF NOT EXISTS ix_notes_sub ON notes (sub);
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# AGENTS.md: {{nameTr}} (Devoplus One uygulaması)
|
|
2
|
+
|
|
3
|
+
Bu dosya, bu uygulamayı geliştiren AI aracına (Claude Code, Codex vb.) yöneliktir. Kurallar bağlayıcıdır; bir
|
|
4
|
+
kuralın gerekçesi SDK belgesinde ya da platform karar defterindedir (aşağıda "Kaynaklar").
|
|
5
|
+
|
|
6
|
+
## Platform sözleşmesi
|
|
7
|
+
|
|
8
|
+
- Uygulama **Devoplus One** üzerinde, **Devoplus Portal içinde iframe ile** çalışır.
|
|
9
|
+
Kimlik, kiracı ve backend erişimi `@devoplus/one-sdk`'dan gelir; **kendi oturum/çerez/token mekanizması YAZILMAZ.**
|
|
10
|
+
- `X-One-*` başlıklarını platform (dispatch) yazar; uygulama bunları `readContext(request.headers)` ile **okur**, asla
|
|
11
|
+
üretmez. İstemciden gelen `X-One-*` başlıkları platformda silinir.
|
|
12
|
+
- `ctx.user.sub` devoplus-auth kimlik uzayıdır (`UserAccount._id`); Organization Schema `User._id` DEĞİLDİR.
|
|
13
|
+
İkisi de 24-hex olduğu için karıştırılırsa sessizce hiç eşleşmez.
|
|
14
|
+
- Dış dünyaya yalnız `fetch` ile ve yalnız platformun izin verdiği adreslere çıkılır (CA backend `env.API_BASE`);
|
|
15
|
+
başka adrese istek **403** döner. Veritabanı sürücüsü, TCP soketi, üçüncü taraf API anahtarı kullanılmaz.
|
|
16
|
+
- **Sır koda/git'e yazılmaz.** Backend kimlik bilgisi platformun çıkış katmanındadır; uygulama hiçbir sır taşımaz.
|
|
17
|
+
`one-ci validate|plan|deploy` git'e girmiş/girecek dosyaları ve son dağıtımdan bu yana eklenen satırları tarar (sır
|
|
18
|
+
taraması kapısı, D-53): API anahtarı, belirteç, özel anahtar, parolalı bağlantı adresi ya da depoya girmiş
|
|
19
|
+
`.dev.vars`/`.env` dağıtımı durdurur. Yerel değerler `.dev.vars`'tadır ve `.gitignore`'dadır. Test sabiti gibi
|
|
20
|
+
gerçek olmayan bir bulgu `.one-secrets-ignore`'a parmak izi + **gerekçe** ile eklenir; sır bir kez commit'lendiyse
|
|
21
|
+
dosyadan silmek YETMEZ: geçmişte kalır, sağlayıcısında iptal edilip yenilenir.
|
|
22
|
+
- **Kod güvenlik taraması (D-65):** birleştirme isteğinde ve dağıtımdan önce Semgrep koşar; yüksek önemli bulgu
|
|
23
|
+
(`ERROR`, `CRITICAL`, `HIGH`) hattı durdurur. Kullanıcı girdisini `eval`/`new Function`'a ya da `innerHTML`'e veren kod
|
|
24
|
+
yazılmaz. Gerçek olmayan bir bulgu `.one-sast-ignore`'a parmak izi + **gerekçe** ile eklenir; kaynaktaki `nosemgrep`
|
|
25
|
+
yorumu sayılmaz.
|
|
26
|
+
- **Hat (CI) platformundur:** bu depoya `.gitlab-ci.yml` ya da başka bir CI dosyası YAZILMAZ (platform projesindeki hat
|
|
27
|
+
koşar, deponunki okunmaz). Testler `pnpm test` ile MR'da koşar; `pnpm-lock.yaml` commit edilir. Kurulum betiği
|
|
28
|
+
(`postinstall`, `.pnpmfile.cjs`) EKLENMEZ; hattın dağıtım işi bağımlılık kurmaz, kurulum yalnız sırsız işte yapılır.
|
|
29
|
+
|
|
30
|
+
## Veri ve servisler
|
|
31
|
+
|
|
32
|
+
- **Veritabanı** `env.DB` (SDK tipi `OneDatabase`): bu kurulumun (uygulama × portal) KENDİ veritabanıdır; tabloya `PortalId` eklenmez, portal
|
|
33
|
+
süzgeci yazılmaz; kişiye özel satır `sub` ile bağlanır. Şema `migrations/NNNN_*.sql` ile ilerler: var olan dosya
|
|
34
|
+
DEĞİŞTİRİLMEZ, yeni dosya en büyük numaradan SONRA eklenir. `DROP TABLE`, `ALTER TABLE ... DROP/RENAME` ve WHERE'siz
|
|
35
|
+
`DELETE` yazılmaz: önceki sürüm geri alındığında çalışmaz; yeni tablo/sütun eklenir, eskisi sonraki sürümde
|
|
36
|
+
kaldırılır. `one-ci deploy` bu üç kuralı denetler (süreklilik kapısı) ve ihlalde dağıtımı durdurur.
|
|
37
|
+
- **Kayıt tutan ekranlar için önce veri modeli:** tabloyu, tipleri ve liste/kayıt/güncelleme/silme uçlarını elle yazmak
|
|
38
|
+
yerine varlıkları `one.data.json`'da tanımlayın, `one data generate` çalıştırın (yeni `migrations/NNNN_one_data.sql` +
|
|
39
|
+
`src/data/types.ts` + `migrations/.one-data-snapshot.json`; üçü de depoya girer, anlık görüntü elle düzenlenmez) ve
|
|
40
|
+
uçları SDK `createCrudHandler({ model: dataModel, entity, records: 'shared' | 'own', canWrite? })` ile bağlayın.
|
|
41
|
+
Model yalnız büyür: alan silme, ad/tür değişikliği, zorunlu yapma ve seçim değeri çıkarma üretilmez, komut durur;
|
|
42
|
+
seçim alanına yeni değer eklemek serbesttir (migration gerekmez). Arama Türkçe katlanmıştır: her metin alanının
|
|
43
|
+
`<alan>_fold` arama kolonu vardır; bir metin alanını elle yazdığınız SQL ile güncellerseniz bu kolonu da
|
|
44
|
+
`foldSearchText(değer)` ile yazın ya da `NULL` yapın. Excel/CSV/yazdırma uçları hazırdır
|
|
45
|
+
(`/api/<varlık>/export.xlsx`, `export.csv`, `report.html`); dosya arayüzde `one.download` ile Portal'a verilir, PDF
|
|
46
|
+
tarayıcının "PDF olarak kaydet"iyle alınır. Belgeler: SDK başvurusu "Veri modeli ve kayıt işlemleri" + "Rapor ve dışa aktarma".
|
|
47
|
+
{{#files}}
|
|
48
|
+
- **Dosya deposu** `env.FILES` (SDK `OneFiles`): kurulum başına depo. Anahtar/boyut kuralları SDK'dadır; kullanıcıdan gelen ad
|
|
49
|
+
`sanitizeFileName` ile tek parçaya indirilir; yükleme gövdesi `readBoundedBody` ile alınır (`request.arrayBuffer()`
|
|
50
|
+
YAZILMAZ, çünkü sınır gövde belleğe alındıktan sonra uygulanırdı). Liste imleci istemciye olduğu gibi döner.
|
|
51
|
+
Anahtar `uploads/<sub>/<uuid>-<ad>` biçimindedir: her kullanıcı yalnız KENDİ ön ekini listeler/indirir/siler
|
|
52
|
+
(`uploadPrefixFor`), uuid aynı adla ikinci yüklemenin öncekini ezmesini önler. Ortak ön ek ya da yalnız dosya adı
|
|
53
|
+
YAZILMAZ: başkasının dosyasını ezer ve anahtarı bilen herkese sildirir.
|
|
54
|
+
{{/files}}
|
|
55
|
+
{{^files}}
|
|
56
|
+
- Dosya saklamak gerekirse: `one.manifest.json` dosyasında `services.files: true`, `Env`'e `FILES: OneFilesBinding`, SDK `OneFiles`
|
|
57
|
+
(bkz. SDK başvurusu "Dosya servisi"). Başka bir depolama kullanılmaz.
|
|
58
|
+
{{/files}}
|
|
59
|
+
{{#cache}}
|
|
60
|
+
- **Önbellek** `env.CACHE` (SDK `OneCache`): kurulum başına, yalnız **önbellek**. TTL zorunludur (60 sn ile 30 gün arası),
|
|
61
|
+
değer JSON ≤ 1 MiB. Önbellek nihai tutarlıdır; sayaç, kilit ya da kalıcı kayıt için KULLANILMAZ; kalıcı veri veritabanındadır.
|
|
62
|
+
{{/cache}}
|
|
63
|
+
{{^cache}}
|
|
64
|
+
- Önbellek gerekirse: `one.manifest.json` dosyasında `services.cache: true`, `Env`'e `CACHE: OneCacheBinding`, SDK `OneCache`
|
|
65
|
+
(TTL zorunlu; kalıcı veri için değil). Başka bir önbellek kullanılmaz.
|
|
66
|
+
{{/cache}}
|
|
67
|
+
- **Backend** (`BackendClient`): CA `api/v1/*` uçları kullanıcının uygulama belirteciyle çağrılır; backend
|
|
68
|
+
"kullanıcı yetkisi ∩ uygulama scope'u ∩ portal" kuralını kendisi uygular. Uç adı/alanı **uydurulmaz**: belgeden
|
|
69
|
+
ya da kardeş depodan doğrulanır, doğrulanamıyorsa geliştiriciye sorulur.
|
|
70
|
+
- **E-posta/SMS/mobil bildirim** yalnız SDK `sendMail`/`sendSms`/`sendPush` ile; alıcı `sub` listesidir (ya da org
|
|
71
|
+
kimliği `toOrgUserIds`), serbest adres/telefon/cihaz kimliği YAZILMAZ ve başka bir bildirim servisi kullanılmaz.
|
|
72
|
+
Manifest `scopes` içinde `mail.execute`/`sms.execute`/`push.execute` olmalı. Bu üç scope ve `dexa.write`, kullanıcı
|
|
73
|
+
uygulamaya o portalda AYRICA ve süreli (30 gün) onay vermeden belirtece GİRMEZ (D-59): uygulama yine açılır, portal
|
|
74
|
+
üstte "İzinleri incele" düğmesini gösterir. Bu işleri yapan rotayı `requireScope` ile koruyun; arayüzde
|
|
75
|
+
`awaitsUserConsent(ctx, scope)` `true` ise düğmeyi kapatıp "portaldaki İzinleri incele düğmesiyle izin verin" yazın.
|
|
76
|
+
Mobil bildirim yalnız başlık (≤ 100) +
|
|
77
|
+
metin (≤ 300) taşır, bağlantı/veri yükü yoktur; bugün hiçbir ortamda açık değildir (`ONE-APP-E-029`); kodunuz bu
|
|
78
|
+
hatada da çalışmayı sürdürmeli, e-postayı ya da asıl işlemi düşürmemelidir (`notify` kanalları bağımsız gönderir).
|
|
79
|
+
- **DEXA (ERP okuma)** yalnız SDK `queryDexa(api, key, parameters?)` ile ve yalnız ÖNCEDEN tanımlı QueryStore
|
|
80
|
+
anahtarlarıyla; serbest SQL ya da ERP'ye yazma YOKTUR. Anahtarları Devoplus Console'da uygulamanın kayıt defterine
|
|
81
|
+
yönetici ekler (kayıtta olmayan anahtar 403). Manifest'te `services.dexa.queryKeys` ve `scopes` içinde `dexa.read`
|
|
82
|
+
birlikte beyan edilir; anahtar adı uydurulmaz, geliştiriciden alınır. `result.unavailable === true` ise sorgu
|
|
83
|
+
ÇALIŞAMADI; boş `rows` "kayıt yok" SAYILMAZ, kullanıcıya "şu anda getirilemiyor" gösterilir; `truncated` sonucun
|
|
84
|
+
5.000 satırda kesildiğini söyler. `LangCode` platformundur, uygulama GÖNDERMEZ. `UserCode`, `CompanyCode`,
|
|
85
|
+
`CostCenterCode`: kurulumda yönetici "kodları uygulama belirler" onayını verdiyse uygulama bunları her kipte (kişi,
|
|
86
|
+
ajan, arka plan işi) gönderebilir ve sunucu kişi ile şirket kodunu kurulumun portalına karşı doğrular; onay yoksa
|
|
87
|
+
gönderilen kod reddedilir (`ONE-APP-E-026`). Gönderilmezse kişi/ajan kipinde değer oturumdaki kişiden çözülür, arka
|
|
88
|
+
plan işinde sorgu reddedilir. Boş değer ERP'ye gitmez: boş ya da yalnız boşluk değer `ONE-SDK-E-140` ile hiç
|
|
89
|
+
gönderilmez; kodu bilmiyorsanız parametreyi göndermeyin, uydurmayın.
|
|
90
|
+
- **DEXA işlemi (ERP'ye YAZMA)** yalnız SDK `runDexaCommand(api, key, parameters, { idempotencyKey })` ile ve yalnız
|
|
91
|
+
kayıt defterinde izinli (`dexaCommandKeys`) işlem anahtarlarıyla; manifest'te `services.dexa.commandKeys` + `scopes`
|
|
92
|
+
içinde `dexa.write`. Anahtar uydurulmaz. ⛔ Her kullanıcı eylemi TEK bir `Idempotency-Key` alır: `newIdempotencyKey()`
|
|
93
|
+
ile üretin ve isteği göndermeden ÖNCE veritabanına yazın; ağ hatası / zaman aşımı / 409 `ERPCMD-APP-E-016` / 424 sonrası
|
|
94
|
+
AYNI anahtarla tekrar deneyin ya da `getDexaCommandStatus` ile durumu okuyun. `outcome === 'inDoubt'` ise ERP'ye
|
|
95
|
+
gidip gitmediği bilinmiyor: YENİ anahtarla ASLA yeniden göndermeyin (çift kayıt). Kullanıcıya "sonuç doğrulanamadı"
|
|
96
|
+
deyin. 4xx yanıt ERP'ye hiçbir şey gitmediğini söyler.
|
|
97
|
+
- **Arka plan işi (zamanlama / kuyruk) ve Devoplus servisleri:** işte kullanıcı belirteci yoktur; servisler
|
|
98
|
+
UYGULAMANIN kimliğiyle çağrılır (`ctx.mode === 'job'`, `new BackendClient({ baseUrl, authorization: null })`).
|
|
99
|
+
Kullanılabilenler yalnız manifest `jobs.services` (ve `scopes`) ile beyan edilen `mail.execute`, `sms.execute`,
|
|
100
|
+
`push.execute`, `dexa.read`, `ai.use`'dur. ⛔ **Arka plan işi servisleri kurulumda yönetici onayıyla açılır**:
|
|
101
|
+
onaysız çağrı 403; DEXA'ya yazma bu yolla AÇILMAZ. "Kendime gönder" yoktur, alıcıyı açıkça verin.
|
|
102
|
+
- **Denetim kaydı:** kritik kullanıcı işlemi `logAudit` (console izi) ile işaretlenir; kalıcı kayıt gerekiyorsa
|
|
103
|
+
`recordAudit(api, ctx, action, target?, data?)` (scope `audit.write`). `target`/`data`ya kişisel veri ya da sır
|
|
104
|
+
YAZILMAZ; kayıt Devoplus Console'da görünür ve 365 gün saklanır.
|
|
105
|
+
- **Olay kaydı (Event Logger):** çalışma zamanı olayı `logEvent(ctx, level, message, data?)`, beklenmeyen hata
|
|
106
|
+
`logError(ctx, message, error, data?)`. Satırlar platform günlüğünde saklanır (platform her kurulumda açar; örnekleme
|
|
107
|
+
manifest `observability.headSamplingRate`). `data`daki parola/token/secret/authorization/cookie/API anahtarı adlı
|
|
108
|
+
alanlar otomatik "[gizli]" olur ve 8 KiB üstü `data` yerine boyutu yazılır; yine de kişisel veri ya da sır
|
|
109
|
+
yazmayın. `problemResponse(error, ctx)` bilinmeyen hatayı bu kayda bağlamıyla düşer.
|
|
110
|
+
|
|
111
|
+
## Scope ve yetki
|
|
112
|
+
|
|
113
|
+
- Bir yetenek `one.manifest.json` dosyasındaki `scopes` içinde beyan edilmeden kullanılamaz; kod tarafında `requireScope(ctx,
|
|
114
|
+
'<scope>')` kapısı konur (yoksa 403). Scope adları devoplus-auth kataloğundandır: `<modül>.<read|write|manage|execute>`
|
|
115
|
+
ve ayrıca `ai.use` (Devoplus yapay zekası).
|
|
116
|
+
{{#hasScopes}}
|
|
117
|
+
Bu uygulamanın beyan ettiği scope'lar: {{scopesList}}.
|
|
118
|
+
{{/hasScopes}}
|
|
119
|
+
{{^hasScopes}}
|
|
120
|
+
Bu uygulama henüz scope beyan etmiyor (yalnız kimlik + kendi veritabanı).
|
|
121
|
+
{{/hasScopes}}
|
|
122
|
+
- Portal menüsünde görünme izni `{{permissionCode}}` kodudur; kimin kullanacağını portal yöneticisi Yetki Yönetimi'nden
|
|
123
|
+
atar; uygulama kendi içinde ikinci bir yetki modeli KURMAZ (kendi rol tablosu, e-posta listesiyle yetki vb. YAZILMAZ).
|
|
124
|
+
- Uygulama içinde rol gerekiyorsa (ör. yalnız onaylayanlar onaylasın) **ek izin kodu** beyan edilir:
|
|
125
|
+
`one.manifest.json` dosyasına `permissions: [{ "code": "<{{permissionCode}} ile aynı önek>.<Eylem>", "name": { ... } }]`.
|
|
126
|
+
Portal yöneticisi bu kodu da Yetki Yönetimi'nde kullanıcı gruplarına (şirket, departman, ünvan, pozisyon gibi düğümler)
|
|
127
|
+
atar; kod tarafında `requirePermission(ctx, '<kod>')` (403) ya da `hasPermission(ctx, '<kod>')` kullanılır. Portal
|
|
128
|
+
yöneticisi her izne sahip sayılır (platformla aynı kural). Platform uygulamaya yalnız KENDİ ad alanındaki izinleri
|
|
129
|
+
iletir; başka bir uygulamanın ya da portalın izni okunamaz.
|
|
130
|
+
|
|
131
|
+
## Devoplus yapay zekası: iki yön (ZORUNLU adım)
|
|
132
|
+
|
|
133
|
+
Devoplus yapay zekası bu uygulamanın verisine ve işlemlerine erişebilmelidir: kullanıcı Devoplus'ta "son notlarımı
|
|
134
|
+
göster" dediğinde yanıt bu uygulamadan gelir. Bu bir ek değil, uygulamanın parçasıdır: **her yeni özellikte** aşağıdaki
|
|
135
|
+
üç dosya birlikte güncellenir:
|
|
136
|
+
|
|
137
|
+
1. **`one.manifest.json` dosyasındaki `agent.tools`**: yapay zekanın çağırabileceği her araç: `name` (küçük harf/rakam/`-`),
|
|
138
|
+
TR/EN `title` ve `description` (ne yapar, ne döndürür, ne zaman KULLANILMAZ), `readOnly`, gerekiyorsa `permission`
|
|
139
|
+
(uygulamanın kendi izin kodlarından biri) ve `input` (JSON şeması; yalnız `type`, `properties`, `required`, `enum`,
|
|
140
|
+
`minimum`, `maximum`, `minLength`, `maxLength`, `items`, `description`). Şemada olmayan alan çalışma anında
|
|
141
|
+
reddedilir.
|
|
142
|
+
2. **`src/agent.ts`**: aracın işleyicisi: `defineAgentTools({ '<ad>': { handler: async ({ input, ctx, env }) => ... } })`.
|
|
143
|
+
Girdi SDK'da şemaya göre doğrulanmış gelir; işleyici kullanıcının KENDİ yetkisiyle çalışır (`ctx.user`), ekrandaki
|
|
144
|
+
kurallar aynen uygulanır (ör. yalnız kendi kayıtları). `ctx.admin` = kişi bu portalın şirket yöneticisi, `ctx.actor`
|
|
145
|
+
= Vekalet / Yerine Görüntüle. Araç Devoplus servislerini kişi adına çağırabilir; süreli onay gerektiren scope'lar
|
|
146
|
+
(e-posta, SMS, mobil bildirim, DEXA'ya yazma) ajan aracında KULLANILAMAZ. JSON'a çevrilebilir bir değer döndürün; gereksiz kişisel veri
|
|
147
|
+
döndürmeyin.
|
|
148
|
+
3. **`AGENT_GUIDE.md`**: yapay zekanın okuduğu kullanım rehberi. Dört başlık zorunludur: `## Uygulama ne yapar`,
|
|
149
|
+
`## Veri`, `## Araçlar`, `## Örnek istekler`. Her aracın adı rehberde geçer. `one-ci validate` araç beyanı olan
|
|
150
|
+
uygulamada bu dosyayı arar ve eksikse yayını DURDURUR; araç beyanı yoksa "Devoplus yapay zekası bu uygulamaya
|
|
151
|
+
erişemez" uyarısı verir.
|
|
152
|
+
|
|
153
|
+
Araç tasarım kuralları:
|
|
154
|
+
|
|
155
|
+
- Önce **salt okunur** araçlar (listeleme, arama, özet). Bir şeyi değiştiren araç `readOnly: false` beyan edilir:
|
|
156
|
+
Devoplus yapay zekası her çağrıdan önce kullanıcıya onay sorar ve bu araçlar Vekalet / Yerine Görüntüle oturumunda
|
|
157
|
+
çalışmaz. Silme gibi geri alınamaz işler araç yapılmaz; kullanıcı ekrana yönlendirilir.
|
|
158
|
+
- Bir araç tek bir işi yapar ve adı bunu söyler (`list-requests`, `get-request`, `approve-request`). Girdiler küçük ve
|
|
159
|
+
sınırlıdır (`enum`, `minimum`/`maximum`, `maxLength`); sayfa boyu gibi değerlerin bir tavanı olur.
|
|
160
|
+
- Araç, kullanıcının uygulama ekranında yapabileceğinden fazlasını yapmaz: aynı izin ve aynı süzgeç (`sub`) uygulanır.
|
|
161
|
+
- Teknik olmayan kullanıcıya araç adı, şema ya da JSON sorulmaz. Hangi soruların bu uygulamaya ait olduğunu keşif
|
|
162
|
+
görüşmesindeki iş listesinden siz çıkarırsınız; rehberi siz yazarsınız.
|
|
163
|
+
- Araç çağrısının yolu `/__one/agent/<ad>`'dır ve yalnız platformun imzalı çağrısı ulaşır (tarayıcıdan gelen 404 alır);
|
|
164
|
+
bu yolu ekranda kullanmayın. Katalog ve rehber `/.well-known/one-agent.json`, `/.well-known/one-agent.md`
|
|
165
|
+
adreslerinden kendiliğinden sunulur (`src/index.ts` başındaki iki satır); elle yazılmaz.
|
|
166
|
+
|
|
167
|
+
**Uygulamanın Devoplus yapay zekasını kullanması:** metin özetleme, sınıflandırma, alan çıkarma, metin üretme ve sohbet
|
|
168
|
+
için SDK `completeAi(api, ctx, { task, input, ... })` (manifest `scopes` içinde `ai.use`). Model, kota ve portalda açık
|
|
169
|
+
olup olmadığı Devoplus'tadır: kapalı portalda 503, kota dolunca 429 döner; uygulama bu durumda da çalışmayı sürdürür.
|
|
170
|
+
⛔ Model sayı HESAPLAMAZ ve kayıt YAZMAZ: tutar/toplam/tarih farkı kodda hesaplanıp `input`'a hazır verilir; modelin
|
|
171
|
+
çıktısı bir öneridir, veritabanına yazmadan önce kod doğrular ve gerekirse kullanıcıya onaylatır. `input`'a sır ya da
|
|
172
|
+
işin gerektirmediği kişisel veri konmaz.
|
|
173
|
+
|
|
174
|
+
## Kabuk ve Portal köprüsü (`src/shell.ts`)
|
|
175
|
+
|
|
176
|
+
- Sayfa üç dosyayı KENDİ adresinden yükler: `/one-ui.css` (görsel dil), `/one-bridge.js` (Devoplus One köprüsü,
|
|
177
|
+
`window.one`) ve `/shell.js` (sayfanın kendi betiği). Sunucu tarafı ilk ikisini `clientAssetResponse(pathname)`, üçüncüsünü
|
|
178
|
+
`scriptResponse(renderShellScript(...))` ile döndürür. ⛔ Satır içi `<script>` ve başka bir adresten (`https://...`) betik
|
|
179
|
+
YAZILMAZ: platform ikisini de çalıştırmaz ve `one-ci validate` yayını durdurur. Kütüphane gerekiyorsa bağımlılık olarak
|
|
180
|
+
eklenir ve uygulamayla birlikte sunulur. Satır içi `<style>` serbesttir.
|
|
181
|
+
- Portal ile HER konuşma köprüden geçer; `postMessage` doğrudan YAZILMAZ. Köprü iletinin kaynağını (`parent` sorgusundaki
|
|
182
|
+
origin + üst pencere) doğrular ve yalnız o origin'lere gönderir.
|
|
183
|
+
- Belirteç `one.onToken(fn)` / `one.token`; istekler `one.fetch(yol, init)` ile gider (belirteç eklenir). Tema, dil ve
|
|
184
|
+
Vekalet / Yerine Görüntüle bilgisi `one.onContext(fn)` ile gelir; sayfa yeniden yüklenmeden uygulanır.
|
|
185
|
+
- `one.start({ navigation })` sayfa hazır olunca BİR kez çağrılır: Portal'a hazır olduğunu ve menüyü bildirir, yükseklik
|
|
186
|
+
bildirimini başlatır. Menü sunucuda `navigationFromManifest(manifest)` ile `one.manifest.json`'dan üretilir.
|
|
187
|
+
- **Sekme izni:** bir sekme yalnız belli bir izni olanlara görünecekse manifest'te öğeye `"permission": "One.<Uygulama>.<Eylem>"`
|
|
188
|
+
yazın (kod `permission.code` ya da `permissions` içinde beyan edilmiş olmalı). Köprü bu sekmeyi oturumdaki kişinin
|
|
189
|
+
izinleri bildirilene kadar menüye KOYMAZ: sayfa oturum bilgisini okuyunca `one.setNavigationAccess({ permissions, admin })`
|
|
190
|
+
çağırır (yönetici her sekmeyi görür). Bu yalnız görünürlüktür; sayfanın verisi sunucu ucunda `requirePermission` ile
|
|
191
|
+
ayrıca korunur; adresi doğrudan açan yetkisiz kişiye "yetkiniz yok" notu gösterin.
|
|
192
|
+
- **Yükseklik:** köprü içerik yüksekliğini Portal'a bildirir; Portal iframe'i o boyda çizer ve sayfanın kendi taşmasını
|
|
193
|
+
gizler; sayfada tek kaydırma çubuğu Portal'ınkidir. ⛔ Kök öğelerde (`html`, `body`, sayfa sarmalayıcısı) `100vh` ya da
|
|
194
|
+
`height: 100%` KULLANMAYIN: içerik iframe boyuna bağlanırsa yükseklik hiç küçülmez. Kendi içinde kayan bir alan
|
|
195
|
+
gerekiyorsa (uzun çıktı, tablo) ona sabit bir `max-height` verin.
|
|
196
|
+
- Kabuk iki dili de gömer (`STRINGS`); yeni metin **iki dile birden** eklenir (test anahtar kümelerini karşılaştırır).
|
|
197
|
+
Kullanıcıya görünen metin Türkçe ve resmi "siz" dilindedir; İngilizce kavram adı bırakılmaz.
|
|
198
|
+
- Kullanıcı girdisi DOM'a `textContent` ile yazılır, `innerHTML` ile değil. Portal'a giden bütün metinler (bildirim,
|
|
199
|
+
onay sorusu, başlık) DÜZ METİNDİR; HTML yazılmaz.
|
|
200
|
+
- Dile bağlı her metin `one:context` gelince yeniden çevrilmelidir: statik öğe `data-i18n` taşır, sonradan
|
|
201
|
+
eklenen öğeye de `setAttribute('data-i18n', ...)` yazılır; durum satırı yalnız `showStatus(anahtar)` ile değişir
|
|
202
|
+
(`textContent = t(...)` yazılmaz, çünkü dil değişiminde eski dilde kalır).
|
|
203
|
+
|
|
204
|
+
## Arayüz standartları (U1 ile U12 arası, bağlayıcı)
|
|
205
|
+
|
|
206
|
+
Her Devoplus One uygulaması Portal'ın bir parçası gibi görünür. Kurallar karar defterinde D-76'dır.
|
|
207
|
+
|
|
208
|
+
- **U1 Menü:** uygulama sol menü / yan çekmece ÇİZMEZ. Görünümler `one.manifest.json` dosyasındaki `navigation` alanına yazılır
|
|
209
|
+
(`[{ "path": "/", "label": { "tr": "...", "en": "..." }, "permission"?: "One.<Uygulama>.<Eylem>" }]`, en çok 12); sekmeleri Portal iframe'in üstünde çizer ve
|
|
210
|
+
Portal adresi `/One/<kısa ad>/<yol>` olur (yenileme, geri ve paylaşılan bağlantı çalışır). Portal sekmesine tıklanınca
|
|
211
|
+
`one.onNavigate(fn)` çağrılır; uygulama kendi içinde görünüm değiştirdiğinde `one.setRoute(yol)` çağırır. Sunucu
|
|
212
|
+
`navigation` yollarının hepsinde sayfayı döndürür. Yol `/` ile başlar; sorgu ve `#` taşımaz.
|
|
213
|
+
- **U2 Üst bilgi:** kullanıcı adı/fotoğrafı, çıkış, portal/dil/tema seçicisi, logo ve uygulama adını tekrarlayan başlık
|
|
214
|
+
ÇİZİLMEZ; bunları Portal gösterir, bilgi `ctx`'tedir. Alt sayfanın başlığı (ör. açılan kaydın adı)
|
|
215
|
+
`one.setTitle(metin | null)` ile Portal'daki sayfa yoluna yazılır (en çok 120 karakter).
|
|
216
|
+
- **U3 Vekalet / Yerine Görüntüle:** başkası adına açılan oturumda sunucuda `ctx.actor` doludur (`{ sub, kind }`;
|
|
217
|
+
`kind` `delegation` = Vekalet, `impersonation` = Yerine Görüntüle), tarayıcıda `one.actor` (`{ kind }`). Varsayılan
|
|
218
|
+
olarak bu oturumlar SALT-OKUNURDUR: platform GET/HEAD dışındaki isteği uygulamaya ulaşmadan 403 ile reddeder. Uygulama
|
|
219
|
+
bu oturumlarda da yazmak zorundaysa `one.manifest.json` dosyasında `"actingSessions": "write"` beyan eder ve kim adına kimin
|
|
220
|
+
yazdığını (`ctx.user.sub` + `ctx.actor.sub`) kaydeder. Bu beyan bile ERP komutu, e-posta, SMS ve mobil bildirimi bu
|
|
221
|
+
oturumlarda AÇMAZ; `recordAudit` her zaman yazılır ve vekili ayrıca tutar. Uyarı şeridini Portal çizer; uygulama yazma düğmelerini
|
|
222
|
+
`one.actor` doluyken kapatabilir.
|
|
223
|
+
- **U4 Pencere ve bildirim:** Portal iframe'i içerik boyunda çizdiği için `position: fixed` öğe ekranın dışında kalabilir.
|
|
224
|
+
Bildirim `one.toast({ severity, summary, detail? })` (`success`, `info`, `warn`, `error`), onay
|
|
225
|
+
`await one.confirm({ message, title?, severity?, acceptLabel?, rejectLabel? })` (sonuç `true`/`false`) ile Portal'da
|
|
226
|
+
çizilir. Uygulamanın kendi büyük penceresi gerekiyorsa `one.viewport.current` (`{ top, height }`, iframe pikseli) ve
|
|
227
|
+
`one.viewport.onChange(fn)` ile görünen alana yerleştirilir.
|
|
228
|
+
- **U5 Görsel dil:** `/one-ui.css` sınıfları kullanılır: `one-btn` (`one-btn-primary`, `one-btn-danger`, `one-btn-text`),
|
|
229
|
+
`one-field` + `one-label` + `one-input` / `one-select` / `one-textarea`, `one-table` (`one-table-wrap` içinde),
|
|
230
|
+
`one-card`, `one-message` (`-success`, `-info`, `-warn`, `-error`), içerik sekmeleri `one-tabs` + `one-tab`
|
|
231
|
+
(`aria-selected`). Renk/yazı/köşe `--one-primary`, `--one-primary-contrast`, `--one-font-family`, `--one-radius` ve
|
|
232
|
+
`--one-bg`, `--one-fg`, `--one-muted`, `--one-surface`, `--one-border` değişkenlerinden okunur; açık/koyu tema ve
|
|
233
|
+
Portal'ın marka değerleri bunlara yazılır. Sabit renk YAZILMAZ.
|
|
234
|
+
- **Hazır bileşenler (D-83):** liste, kayıt formu, süzgeç, kişi seçimi, dosya yükleme ve grafik için SDK'nın
|
|
235
|
+
`/one-components.js` bileşenleri kullanılır: `<one-table>` (sıralama, arama, sayfalama, seçim, Excel/CSV),
|
|
236
|
+
`<one-form>` (doğrulama, sunucu hatası alanın altında, kaydedilmemiş değişiklik), `<one-filter>`,
|
|
237
|
+
`<one-person-picker>`, `<one-file-upload>`, `<one-chart>`. Sayfa betiği köprüden SONRA yükler
|
|
238
|
+
(`<script src="/one-components.js"></script>`; `clientAssetResponse` sunar, yüklenmezse `one-ci validate` uyarır);
|
|
239
|
+
kolon/alan tanımları `/shell.js`'te JavaScript özelliğidir, etiketler `{ tr, en }`. Uzak liste ucu
|
|
240
|
+
`GET /api/<varlık>?limit=&cursor=&sort=&order=&q=&<alan>=` isteğine `{ items, nextCursor }` döndürür, dışa aktarma aynı ucu
|
|
241
|
+
`?format=xlsx|csv` ile çağırır. Kişi seçici için sunucuda `peopleSearchHandler(request, env, ctx)` (`/api/one/people`,
|
|
242
|
+
scope `orgschema.read`; dönen `id` org kimliğidir, `sub` değil). Elle tablo/form yazılmaz. Ayrıntı: SDK başvurusu
|
|
243
|
+
"Hazır arayüz bileşenleri".
|
|
244
|
+
- **U6 İndirme ve dış bağlantı:** iframe'de indirme ve yeni pencere kapalıdır. Dosya `await one.download({ name, mime,
|
|
245
|
+
data })` ile Portal'a verilir (en çok 25 MB); dış bağlantı `one.openExternal('https://...')` ile açılır (yalnız https,
|
|
246
|
+
Portal kullanıcıya sorar). `<a download>` ve `window.open` kullanılmaz.
|
|
247
|
+
- **U7 Kaydedilmemiş değişiklik:** formda kaydedilmemiş değişiklik varken `one.setDirty(true)`, kaydedilince
|
|
248
|
+
`one.setDirty(false)`; Portal kullanıcı ayrılmadan önce sorar.
|
|
249
|
+
- **U8 Dil ve biçim:** her metin TR + EN; tarih `gg.aa.yyyy`, sayı `tr-TR` biçiminde; telefon E.164 saklanır; hata
|
|
250
|
+
kullanıcının dilinde gösterilir; yükleniyor / boş / hata durumları ayrı gösterilir ("boş" ile "getirilemedi" karışmaz).
|
|
251
|
+
- **U9 Erişilebilirlik ve boyut:** en küçük genişlik 360 px, yatay kaydırma yok, kenar boşluğu 16 px; her işlem
|
|
252
|
+
klavyeyle yapılabilir, odak görünür; metin/zemin kontrastı yeterlidir.
|
|
253
|
+
- **U10 Kimlik:** Devoplus logosu ya da Portal menüsü taklit edilmez.
|
|
254
|
+
- **U11 Denetim:** `one-ci validate` sol menü, kendi üst bilgisi, kapsayıcısız `position: fixed` ve eksik TR/EN için
|
|
255
|
+
UYARI verir (ilk sürümde yayını durdurmaz; sonra durduracak). Uyarıyı kalıcı bırakmayın.
|
|
256
|
+
- **U12 Güvenlik tabanı:** platform her yanıta içerik güvenliği politikası ekler: betik, bağlantı, yazı tipi ve görsel
|
|
257
|
+
yalnız uygulamanın kendi adresinden (görselde ayrıca `data:`, `blob:`, Devoplus API ve Devoplus CDN, yani kullanıcı
|
|
258
|
+
fotoğrafı ve kurumsal görseller; veri isteği yine yalnız kendi adresinize); satır içi stil açık, satır içi betik kapalı;
|
|
259
|
+
başka sayfayı çerçeveleme, nesne gömme ve başka adrese form gönderme kapalı. Kamera, mikrofon ve konum varsayılan
|
|
260
|
+
kapalıdır; gerekiyorsa `one.manifest.json` dosyasında `"devicePermissions": ["camera" | "microphone" | "geolocation"]` beyan
|
|
261
|
+
edilir. Uygulama bu politikayı kendi başlığıyla yalnız DARALTABİLİR.
|
|
262
|
+
|
|
263
|
+
## Çalışma kuralları
|
|
264
|
+
|
|
265
|
+
- `pnpm type-check && pnpm test && pnpm build` yeşil olmadan commit yapılmaz.
|
|
266
|
+
- Hata yolu tek yerdedir: `catch (error) { return problemResponse(error) }`. `OneError` RFC 9457 gövdeye çevrilir,
|
|
267
|
+
bilinmeyen hata 500 döner ve ayrıntı sızdırılmaz. `OneError.retryAfterSeconds` pozitif tam sayıysa (ör. 409
|
|
268
|
+
`ERPCMD-APP-E-016`) yanıt onu `Retry-After` başlığında ve gövdedeki `retryAfterSeconds` alanında da taşır.
|
|
269
|
+
- Kod Devoplus git deposuna push edilir; `one-ci validate` (manifest + giriş dosyası + README.md/AGENTS.md + araç
|
|
270
|
+
beyanı varsa AGENT_GUIDE.md + `one.data.json` varsa üretilmiş dosyaların güncelliği (`one data generate --check`) +
|
|
271
|
+
arayüz denetimi) ve taramalardan geçmeyen sürüm yüklenmez. Geliştirici makinesinden doğrudan yükleme YOKTUR.
|
|
272
|
+
- **AI değişikliği ayrı dalda yapılır ve izlenebilir kalır:** çalışmaya `ai/<konu>` adlı yeni bir dalda başlayın;
|
|
273
|
+
yayın dallarına (`test`, `uat`, `main`) doğrudan commit atmayın, değişiklik birleştirme isteğiyle (merge request /
|
|
274
|
+
pull request) girer. AI ile yazdığınız her commit'in iletisine ayrı satırda `AI-Assisted: <araç adı>` trailer'ı
|
|
275
|
+
ekleyin (ör. `AI-Assisted: Claude Code`). `one-ci deploy` bu commit'leri ve geldikleri dalı sürüm defterine yazar;
|
|
276
|
+
platform `ONE_AI_BRANCH_POLICY=enforce` açarsa yayın dalına doğrudan yazılmış AI commit'i yayını durdurur.
|
|
277
|
+
- `README.md` uygulamanın amacını ve davranışını, bu dosya AI kurallarını, `AGENT_GUIDE.md` Devoplus yapay zekasının
|
|
278
|
+
araçları nasıl kullanacağını anlatır; davranışı değiştiren değişiklik üçünü aynı commit'te günceller (one-ci README ya da
|
|
279
|
+
AGENTS yoksa veya boşsa, araç beyanı olan uygulamada AGENT_GUIDE.md eksikse yayını durdurur).
|
|
280
|
+
|
|
281
|
+
## Kaynaklar
|
|
282
|
+
|
|
283
|
+
- SDK API ve hata kodları (SDK başvurusu): `node_modules/@devoplus/one-sdk/REFERENCE.md` (bu dosyayı taşımayan eski ön
|
|
284
|
+
sürümlerde `README.md`)
|
|
285
|
+
- Platform mimarisi: `devoplus/devoplus-one` deposundaki `docs/architecture.md`; kararlar `devoplus-root/docs/devoplus-one-karar-defteri.md`
|
|
286
|
+
- Örnek/test konsolu: `devoplus/devoplus-one` deposundaki `apps/hello`
|