@appaflytech/zeyrek-ai-mcp 0.0.5 → 0.0.7

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.
Files changed (145) hide show
  1. package/.env.example +2 -2
  2. package/.env.production.example +9 -1
  3. package/README.md +14 -38
  4. package/dist/config.js +36 -3
  5. package/dist/knowledge/backendConstraints.js +982 -0
  6. package/dist/knowledge/componentCatalog.js +549 -92
  7. package/dist/knowledge/componentEvents.js +120 -25
  8. package/dist/knowledge/coverage.js +43 -0
  9. package/dist/knowledge/flowGuards.js +30 -0
  10. package/dist/knowledge/functionGuards.js +134 -0
  11. package/dist/knowledge/guideSections.js +49 -0
  12. package/dist/knowledge/hydrate.js +97 -0
  13. package/dist/knowledge/languageCodes.js +36 -0
  14. package/dist/knowledge/languages.js +105 -0
  15. package/dist/knowledge/layoutSummary.js +10 -0
  16. package/dist/knowledge/localization.js +51 -0
  17. package/dist/knowledge/normalize.js +500 -16
  18. package/dist/knowledge/outlines.js +167 -12
  19. package/dist/knowledge/policy.js +14 -5
  20. package/dist/knowledge/propTypes.js +80 -0
  21. package/dist/knowledge/rules.js +961 -44
  22. package/dist/knowledge/schemaGuard.js +199 -0
  23. package/dist/knowledge/styleResolve.js +63 -4
  24. package/dist/observability/trace.js +27 -3
  25. package/dist/platform/csv.js +123 -0
  26. package/dist/platform/errors.js +107 -6
  27. package/dist/platform/http.js +34 -4
  28. package/dist/platform/patch.js +27 -0
  29. package/dist/platform/refs.js +104 -0
  30. package/dist/platform/responses.js +25 -6
  31. package/dist/platform/screenshot.js +544 -16
  32. package/dist/platform/secrets.js +40 -0
  33. package/dist/platform/tokenVault.js +89 -0
  34. package/dist/platform/writeDiff.js +281 -0
  35. package/dist/resources/examples/flow-call-function.json +66 -0
  36. package/dist/resources/examples/function-total.json +70 -0
  37. package/dist/resources/examples/layout-app-shell.json +1 -1
  38. package/dist/resources/examples/microfunction-node-sum.json +56 -0
  39. package/dist/resources/examples/microservice-table-list.json +2 -4
  40. package/dist/resources/examples/page-dynamic-form.json +3 -3
  41. package/dist/resources/examples/page-flex-sections.json +99 -0
  42. package/dist/resources/examples/page-sign-in.json +4 -3
  43. package/dist/resources/guides/component.md +81 -8
  44. package/dist/resources/guides/entity.md +712 -20
  45. package/dist/resources/guides/expressions.md +47 -2
  46. package/dist/resources/guides/external-component.md +9 -0
  47. package/dist/resources/guides/functions.md +262 -0
  48. package/dist/resources/guides/getting-started.md +82 -7
  49. package/dist/resources/guides/layout.md +53 -10
  50. package/dist/resources/guides/merge.md +231 -0
  51. package/dist/resources/guides/microfunction.md +166 -0
  52. package/dist/resources/guides/microservice.md +287 -15
  53. package/dist/resources/guides/middleware.md +89 -1
  54. package/dist/resources/guides/modify-existing.md +23 -0
  55. package/dist/resources/guides/page.md +402 -49
  56. package/dist/resources/guides/translation.md +150 -8
  57. package/dist/resources/guides/view.md +16 -1
  58. package/dist/resources/guides/workflow.md +199 -28
  59. package/dist/resources/knowledge.js +5 -0
  60. package/dist/resources/rules.js +27 -0
  61. package/dist/resources/schema-fingerprint.json +8 -0
  62. package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
  63. package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
  64. package/dist/resources/schemas/CreateBulkRowCommand.json +36 -0
  65. package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
  66. package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
  67. package/dist/resources/schemas/CreateEntityCommand.json +52 -12
  68. package/dist/resources/schemas/CreateEnvironmentCommand.json +31 -0
  69. package/dist/resources/schemas/CreateGlobalFunctionCommand.json +95 -0
  70. package/dist/resources/schemas/CreateImportPlanCommand.json +278 -0
  71. package/dist/resources/schemas/CreateMergePlanCommand.json +34 -0
  72. package/dist/resources/schemas/CreateMicrofunctionCommand.json +117 -0
  73. package/dist/resources/schemas/CreateMicrofunctionRuntimeCommand.json +51 -0
  74. package/dist/resources/schemas/CreateMicroserviceCommand.json +145 -3
  75. package/dist/resources/schemas/CreateQueryCommand.json +56 -3
  76. package/dist/resources/schemas/CreateRowCommand.json +38 -0
  77. package/dist/resources/schemas/CreateViewCommand.json +13 -10
  78. package/dist/resources/schemas/CreateWorkflowCommand.json +106 -14
  79. package/dist/resources/schemas/DataJobDto.json +267 -0
  80. package/dist/resources/schemas/ExportRowsCommand.json +172 -0
  81. package/dist/resources/schemas/Filter.json +3 -1
  82. package/dist/resources/schemas/GetRowsQuery.json +6 -1
  83. package/dist/resources/schemas/GlobalFunctionDto.json +89 -0
  84. package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +249 -0
  85. package/dist/resources/schemas/ImportAnalysisDto.json +401 -0
  86. package/dist/resources/schemas/ImportPlanDto.json +260 -0
  87. package/dist/resources/schemas/ImportSampleDto.json +90 -0
  88. package/dist/resources/schemas/InvokeMicrofunctionCommand.json +25 -0
  89. package/dist/resources/schemas/PreviewMicroserviceCommand.json +135 -1
  90. package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
  91. package/dist/resources/schemas/PreviewWorkflowCommand.json +45 -9
  92. package/dist/resources/schemas/ResolveMergeConflictsCommand.json +58 -0
  93. package/dist/resources/schemas/RevertMergePlanCommand.json +19 -0
  94. package/dist/resources/schemas/RunImportCommand.json +62 -0
  95. package/dist/resources/schemas/StepDto.json +34 -3
  96. package/dist/resources/schemas/UpdateBulkRowCommand.json +36 -0
  97. package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
  98. package/dist/resources/schemas/UpdateEntityCommand.json +52 -11
  99. package/dist/resources/schemas/UpdateGlobalFunctionCommand.json +66 -0
  100. package/dist/resources/schemas/UpdateMicrofunctionCommand.json +121 -0
  101. package/dist/resources/schemas/UpdateMicrofunctionRuntimeCommand.json +54 -0
  102. package/dist/resources/schemas/UpdateMicroserviceCommand.json +147 -4
  103. package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
  104. package/dist/resources/schemas/UpdateRowCommand.json +40 -0
  105. package/dist/resources/schemas/UpdateViewCommand.json +15 -10
  106. package/dist/resources/schemas/UpdateWorkflowCommand.json +106 -14
  107. package/dist/server.js +17 -1
  108. package/dist/tools/access.js +3 -3
  109. package/dist/tools/activities.js +40 -0
  110. package/dist/tools/applications.js +1 -1
  111. package/dist/tools/automations.js +76 -34
  112. package/dist/tools/common.js +55 -0
  113. package/dist/tools/components.js +16 -7
  114. package/dist/tools/datasources.js +83 -21
  115. package/dist/tools/dependencies.js +1 -1
  116. package/dist/tools/deployments.js +34 -2
  117. package/dist/tools/dtos.js +25 -12
  118. package/dist/tools/entities.js +113 -28
  119. package/dist/tools/environments.js +52 -0
  120. package/dist/tools/externalComponents.js +9 -3
  121. package/dist/tools/files.js +57 -42
  122. package/dist/tools/flows.js +205 -13
  123. package/dist/tools/folders.js +7 -2
  124. package/dist/tools/functions.js +331 -0
  125. package/dist/tools/globalFunctions.js +63 -0
  126. package/dist/tools/importSchemas.js +79 -0
  127. package/dist/tools/imports.js +241 -0
  128. package/dist/tools/layouts.js +103 -29
  129. package/dist/tools/merges.js +113 -0
  130. package/dist/tools/microfunctions.js +155 -0
  131. package/dist/tools/microservices.js +270 -40
  132. package/dist/tools/organizations.js +5 -6
  133. package/dist/tools/pages.js +421 -104
  134. package/dist/tools/platformInfo.js +155 -11
  135. package/dist/tools/projects.js +183 -33
  136. package/dist/tools/queries.js +32 -10
  137. package/dist/tools/revisions.js +94 -0
  138. package/dist/tools/rows.js +484 -50
  139. package/dist/tools/screenWrite.js +68 -0
  140. package/dist/tools/screens.js +28 -0
  141. package/dist/tools/templates.js +32 -5
  142. package/dist/tools/translations.js +24 -8
  143. package/dist/tools/views.js +20 -10
  144. package/dist/tools/workflows.js +56 -21
  145. package/package.json +7 -2
@@ -1,7 +1,212 @@
1
- import { guid, projectParam, environmentParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
1
+ import { findLocalizationCompanion, localizationRows, platformIgnoredLocalizations } from '../knowledge/localization.js';
2
+ import { guid, entityRef, resolveEntityId, withResolved, projectParam, environmentParam, languageParam, pagingParams, readOnly, writes, updates, destructive } from './common.js';
3
+ import { unmappedMembers, withUnmappedMembers } from '../knowledge/schemaGuard.js';
4
+ import { withNormalization } from '../knowledge/normalize.js';
5
+ import { RefResolver } from '../platform/refs.js';
6
+ import { attachFindings, evaluatePolicy, enforcePolicy } from '../knowledge/policy.js';
7
+ import { policyRules } from '../knowledge/rules.js';
8
+ import { languageHeader } from '../knowledge/languageCodes.js';
2
9
  import { handle } from '../platform/responses.js';
10
+ import { uploadFile } from './files.js';
11
+ import { PlatformApiError } from '../platform/errors.js';
12
+ import { secretKeyPattern } from '../platform/secrets.js';
13
+ import { csvRows } from '../platform/csv.js';
14
+ import { readFile, stat, writeFile } from 'node:fs/promises';
15
+ import { extname, resolve } from 'node:path';
3
16
  import { z } from 'zod';
4
17
  const rowValues = z.record(z.unknown()).describe("Alan SLUG'ı → değer sözlüğü; anahtarlar entity_get çıktısındaki field SLUG'larıdır (name değil — name kullanmak 'Please enter a valid value' hatası verir)");
18
+ /**
19
+ * `row_*` DBMS düzeyi bir satır yüzeyidir; var olan satır kümesinden TÜRETİLEN satırları
20
+ * (geçmiş, hareket, log, junction, backfill) gövdede taşımak onun işi değildir — 2026-09-05'te
21
+ * 802, 2026-09-06'da 552 satır bu yüzden elle yazıldı (`B-34`, `B-50`) ve ikinci koşunun 65
22
+ * dakikasının ~21'i buraya gitti. Eşik büyük tutuldu: bulgu her toplu yazmada bağırırsa koşunun
23
+ * sinyal oranını kendisi düşürür, bu yüzden yalnız "elle taşınamayacak kadar büyük" dizilerde
24
+ * konuşur ve cümlesi KOŞULLUDUR — dışarıdan gelen veriyi (içe aktarma, kullanıcı girdisi)
25
+ * yazan çağıranı suçlamaz.
26
+ */
27
+ const DERIVED_ROW_THRESHOLD = 50;
28
+ function derivedRowFindings(rows) {
29
+ if (!rows || rows.length < DERIVED_ROW_THRESHOLD)
30
+ return [];
31
+ return [{
32
+ ruleId: 'ROW-001',
33
+ severity: 'warn',
34
+ message: `Gövdede ${rows.length} satır var. Satırlar dışarıdan bir dosyadan geliyorsa gövdeden değil DOSYADAN yazın: aynı çağrıyı \`path: "seed.csv" | "seed.json"\` ile verin, gateway dosyayı okur ve 1000'lik partilere böler — satırlar ajan bağlamından hiç geçmez. Bu satırlar VAR OLAN satırlardan türetiliyorsa (geçmiş/hareket/log/junction tablosu, backfill) onları gövdede taşımayın: tek bir kayıtlı 'INSERT … SELECT' sorgusu aynı işi TEK çağrıda yapar ve hiçbir id elle taşınmaz (ölçüldü 2026-09-07). Satırlar dışarıdan geliyorsa (içe aktarma, kullanıcı verisi) bu çağrı doğru yoldur. Ham SQL yolu UYGULAMA katmanını atlar (automation tetiklenmez, incremental sayaç ilerlemez, localization companion yazılmaz) ama veritabanı kısıtları — NOT NULL, FK, UNIQUE, enum CHECK — aynen uygulanır. Reçete, merdiven kalıbı ve anahtar sözleşmesi: platform_guide('entity') §'Toplu ve türetilmiş satır'.`,
35
+ guide: 'ngapps://guides/entity'
36
+ }];
37
+ }
38
+ const bulkBatchSize = 1000;
39
+ const maxPathBytes = 50 * 1024 * 1024;
40
+ const maxPathRows = 100_000;
41
+ const pathParam = z.string().min(1).optional()
42
+ .describe("Gateway sürecinin okuyabildiği yerel `.json` (satır dizisi) ya da `.csv` dosyası; 'row'/'rows' ile birlikte verilmez. Satırlar ajan bağlamından HİÇ geçmez: gateway dosyayı okur, 1000'lik partilere böler ve ardışık toplu çağrı yapar. CSV ayracı ilk satırdan bulunur (`;` sayısı `,` sayısını geçerse `;`), değerler string gider ve tipe backend çevirir. 50 MB / 100.000 satır üstü reddedilir — o boyut import_analyze zinciridir.");
43
+ const relationsByNameParam = z.boolean().optional()
44
+ .describe("İlişki alanlarına id yerine GÖRÜNEN AD yazmayı açar: 'departman: \"Satış\"' hedef tablonun display ya da benzersiz alanında aranır ve id'ye çevrilir. Yalnız toplu yolda (rows / path) çalışır. Eşleşmeyen değer 422 döndürür ve hangi satırın hangi alanı olduğunu söyler — sessizce boş bırakmaz.");
45
+ function collectIds(payload, into) {
46
+ const data = payload?.data;
47
+ if (Array.isArray(data))
48
+ into.push(...data);
49
+ }
50
+ async function readRowsFromPath(tool, path) {
51
+ const absolute = resolve(path);
52
+ const extension = extname(absolute).toLowerCase();
53
+ if (extension !== '.json' && extension !== '.csv') {
54
+ throw new PlatformApiError(`${tool}: \`path\` yalnız .json ya da .csv okur — verilen: ${absolute}`);
55
+ }
56
+ let sizeBytes;
57
+ try {
58
+ sizeBytes = (await stat(absolute)).size;
59
+ }
60
+ catch (error) {
61
+ throw new PlatformApiError(`${tool}: dosya okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}). Yol gateway sürecinin çalıştığı makineye göre çözülür.`);
62
+ }
63
+ if (sizeBytes > maxPathBytes) {
64
+ throw new PlatformApiError(`${tool}: dosya ${maxPathBytes} bayt sınırını aşıyor (${sizeBytes} B). Bu boyut satır yazma yolu değil içe aktarma zinciridir — import_analyze → import_plan → import_run.`);
65
+ }
66
+ const text = await readFile(absolute, 'utf8');
67
+ let rows;
68
+ if (extension === '.csv') {
69
+ rows = csvRows(text);
70
+ }
71
+ else {
72
+ try {
73
+ rows = JSON.parse(text);
74
+ }
75
+ catch (error) {
76
+ throw new PlatformApiError(`${tool}: JSON okunamadı — ${absolute} (${error instanceof Error ? error.message : String(error)}).`);
77
+ }
78
+ }
79
+ if (!Array.isArray(rows)) {
80
+ throw new PlatformApiError(`${tool}: \`path\` dosyası bir satır DİZİSİ taşımalı — ${absolute} içinde ${typeof rows} bulundu.`);
81
+ }
82
+ if (rows.length === 0) {
83
+ throw new PlatformApiError(`${tool}: dosyada satır yok — ${absolute}. CSV ise başlık satırı var mı ve ayraç doğru mu kontrol edin.`);
84
+ }
85
+ if (rows.length > maxPathRows) {
86
+ throw new PlatformApiError(`${tool}: ${rows.length} satır, ${maxPathRows} sınırının üstünde. Bu boyutta içe aktarma zincirini kullanın: import_analyze → import_plan → import_run (iş kuyruğa alınır, ilerleme import_status'tan izlenir).`);
87
+ }
88
+ return rows;
89
+ }
90
+ /**
91
+ * Dosya satır numarası 1 tabanlıdır ve başlık satırını sayar: CSV'de düşen satırı kullanıcı
92
+ * editörde bu numarayla bulur. JSON dizisinde de aynı formül kullanılır — ilk eleman 2. satırda
93
+ * durur çünkü açılış köşeli parantezi ilk satırı yer.
94
+ */
95
+ function fileLineOf(batchIndex, rowIndexInBatch) {
96
+ return batchIndex * bulkBatchSize + rowIndexInBatch + 2;
97
+ }
98
+ async function writeInBatches(platform, method, project, environment, headers, entityId, rows, rowContext, relationsByName) {
99
+ const query = relationsByName ? { relationsByName: true } : undefined;
100
+ const ids = [];
101
+ let written = 0;
102
+ let batches = 0;
103
+ for (let start = 0; start < rows.length; start += bulkBatchSize) {
104
+ const batch = rows.slice(start, start + bulkBatchSize);
105
+ const batchIndex = batches;
106
+ try {
107
+ const payload = await platform.tenant(method, project, `/entities/${entityId}/row/bulk`, { environment, headers, body: { rows: batch, ...(rowContext ? { rowContext } : {}) }, ...(query ? { query } : {}) });
108
+ collectIds(payload, ids);
109
+ written += batch.length;
110
+ batches += 1;
111
+ }
112
+ catch (error) {
113
+ // Hiç satır yazılmadıysa ortada kısmi durum yoktur: bu düz bir REDDİR ve platformun kendi
114
+ // mesajı (satır indeksi + all-or-nothing notu) her özetten iyidir. Yutup 'written: 0'
115
+ // döndürmek çağırana başarı gibi okunurdu.
116
+ if (written === 0)
117
+ throw error;
118
+ const rowIndexInBatch = error instanceof PlatformApiError && typeof error.rowIndex === 'number' ? error.rowIndex : 0;
119
+ return {
120
+ written,
121
+ batches,
122
+ ...(ids.length > 0 ? { ids } : {}),
123
+ stopped: {
124
+ fileLine: fileLineOf(batchIndex, rowIndexInBatch),
125
+ batchIndex,
126
+ rowIndexInBatch,
127
+ error: error instanceof Error ? error.message : String(error),
128
+ note: `${written} satır YAZILDI ve öyle kaldı: partiler arası transaction yoktur, yalnız düşen parti geri alındı. Dosyayı ${fileLineOf(batchIndex, rowIndexInBatch)}. satırdan düzeltip kalanını yeniden gönderin; hepsi-ya-hiç gerekiyorsa import_run kullanın.`
129
+ }
130
+ };
131
+ }
132
+ }
133
+ return { written, batches, ...(ids.length > 0 ? { ids } : {}) };
134
+ }
135
+ /**
136
+ * Üç kaynaktan tam olarak biri: gövdedeki tek satır, gövdedeki dizi ya da dosya. İkisi birden
137
+ * verilirse hangisinin yazılacağı belirsizdir ve sessizce birini seçmek yanlış veri yazar.
138
+ */
139
+ function assertOneSource(tool, row, rows, path) {
140
+ const given = [row, rows, path].filter(value => value !== undefined).length;
141
+ if (given === 1)
142
+ return;
143
+ throw new Error(given === 0
144
+ ? `${tool}: 'row', 'rows' ya da 'path' parametrelerinden biri zorunlu.`
145
+ : `${tool}: 'row', 'rows' ve 'path' birlikte verilemez — tam olarak biri.`);
146
+ }
147
+ const exportFolderName = 'Dışa aktarmalar';
148
+ const maxSelectedRowIds = 1000;
149
+ /**
150
+ * `inline` örneği ajan bağlamına girer. Sütun tanımı + iki satır dar bir tabloda birkaç KB'dir;
151
+ * yüzlerce sütunlu tabloda aynı iki satır bağlamı dosyanın kendisi kadar doldururdu. Sınırı aşan
152
+ * örnekte `data` düşer, `columns` kalır — dosyanın şekli yine görülür, satırlar Sürücü'dedir.
153
+ */
154
+ const templateInlineMaxChars = 16_000;
155
+ const templateFormatNotes = {
156
+ xlsx: "xlsx şablonda ipuçları başlık hücresi yorumunda ve gizli `_zeyrek` sayfasındadır; veri alanında ipucu satırı yoktur.",
157
+ csv: 'csv şablonda ipucu satırı ve `#zeyrek` satırı yoktur; başlıklar alan adıdır.',
158
+ json: 'JSON şablon `{ meta, data }` gövdesidir; `meta` analizde atlanır, satırlar `data`dadır ve anahtarları alan slug\'ıdır.'
159
+ };
160
+ const rowFiltersParam = z.array(z.record(z.unknown())).optional()
161
+ .describe('Platform Filter dizisi; biçim için row_list\'i filtresiz çağırıp Studio\'daki bir filtreyi referans alın');
162
+ const exportScopeParam = z.object({
163
+ kind: z.enum(['view', 'all', 'selected']).describe("'view' filtre + sıralama ile, 'all' tüm satırlar, 'selected' rowIds listesi"),
164
+ filters: rowFiltersParam,
165
+ sort: z.object({ field: z.string(), ascending: z.boolean().optional() }).optional()
166
+ .describe("Sıralama; 'ascending' verilmezse artan"),
167
+ rowIds: z.array(z.string()).optional().describe(`'selected' kapsamında dışa aktarılacak satır id'leri; en çok ${maxSelectedRowIds} — üstü için filtre kullanın`)
168
+ }).optional().describe("Verilmezse tüm satırlar ('all')");
169
+ /**
170
+ * Dosya adı sunucudan gelir; başlık RFC 5987 yüzde kodlu da olabilir. Ada zaman damgası
171
+ * eklenir çünkü aynı tablo günde birkaç kez dışa aktarılır ve Sürücü'de aynı ad çakışır.
172
+ */
173
+ function exportFileName(headers, fallbackSlug, extension) {
174
+ const disposition = headers['content-disposition'] ?? '';
175
+ const matched = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition);
176
+ let base = fallbackSlug;
177
+ const captured = matched?.[1];
178
+ if (captured) {
179
+ try {
180
+ base = decodeURIComponent(captured);
181
+ }
182
+ catch {
183
+ base = captured;
184
+ }
185
+ }
186
+ const dot = base.lastIndexOf('.');
187
+ const stem = dot > 0 ? base.slice(0, dot) : base;
188
+ const stamp = new Date().toISOString().slice(0, 16).replace(/[-:]/g, '').replace('T', '-');
189
+ return `${stem}-${stamp}.${extension}`;
190
+ }
191
+ /**
192
+ * Klasörün `name`'i ASCII'ye katlanmıştır ('Dışa aktarmalar' → 'Disa aktarmalar'); ham ad yalnız
193
+ * `displayName`'de durur. Katlanmış ada bakan arama hiçbir zaman eşleşmez ve her dışa aktarma
194
+ * yeni bir '… (1)' klasörü açardı.
195
+ */
196
+ async function exportFolderIdAsync(platform, project, environment) {
197
+ const listed = await platform.tenant('GET', project, '/folders', { environment, query: { searchTerm: exportFolderName, pageLength: 50 } });
198
+ const rows = listed?.data ?? [];
199
+ const existing = rows.find(folder => folder.displayName === exportFolderName);
200
+ if (typeof existing?.id === 'string')
201
+ return existing.id;
202
+ const created = await platform.tenant('POST', project, '/folders', { environment, body: { name: exportFolderName } });
203
+ const id = created?.data?.id;
204
+ return typeof id === 'string' ? id : undefined;
205
+ }
206
+ const includeTranslationsParam = z.boolean().optional()
207
+ .describe("Çok dilli alanların TÜM dillerini de getirir: satıra 'localizations: { <dil>: { <alan>: <değer> } }' eklenir. Verilmezse satır yalnız istek dilinde düzleştirilmiş gelir (çevirisi olmayan alan varsayılan dile düşer).");
208
+ const localizationsParam = z.record(z.record(z.unknown())).optional()
209
+ .describe("Dil kodu → çok dilli alan değerleri; ör. { tr: { name: 'Yıllık izin' }, en: { name: 'Annual leave' } }. Verilen her dil upsert edilir, verilmeyen dile dokunulmaz. Dil kodu projenin dil listesinde değilse platform 400 döner ve ana satır da yazılmaz.");
5
210
  export function registerRowTools(server, platform) {
6
211
  const config = platform.config;
7
212
  server.registerTool('row_list', {
@@ -10,106 +215,335 @@ export function registerRowTools(server, platform) {
10
215
  inputSchema: {
11
216
  project: projectParam,
12
217
  environment: environmentParam,
13
- entityId: guid,
218
+ entityId: entityRef,
14
219
  selectedFields: z.array(z.string()).optional().describe('Dönen satırlar bu alan adlarına indirgenir, id her zaman dahildir; platform tam satırı hesapladığı için kırpma gateway tarafında yapılır'),
15
- filters: z.array(z.record(z.unknown())).optional().describe('Platform Filter dizisi; biçim için row_list\'i filtresiz çağırıp Studio\'daki bir filtreyi referans alın'),
220
+ filters: rowFiltersParam,
221
+ language: languageParam,
222
+ includeTranslations: includeTranslationsParam,
16
223
  ...pagingParams
17
224
  },
18
225
  annotations: readOnly
19
- }, handle(config, async ({ project, environment, entityId, ...body }) => {
20
- const payload = await platform.tenant('POST', project, `/entities/${entityId}/row/list`, { environment, body });
226
+ }, handle(config, async ({ project, environment, entityId, language, ...body }) => {
227
+ const notes = [];
228
+ const headers = languageHeader(language, notes);
229
+ const resolver = new RefResolver(platform, project, environment);
230
+ const resolvedEntityId = await resolveEntityId(resolver, entityId);
231
+ const payload = await platform.tenant('POST', project, `/entities/${resolvedEntityId}/row/list`, { environment, body, headers });
21
232
  const wanted = body.selectedFields ?? [];
22
- if (!wanted.length || !payload || typeof payload !== 'object' || Array.isArray(payload))
23
- return payload;
24
- const rows = payload.data;
25
- if (!Array.isArray(rows))
26
- return payload;
27
- const keep = new Set([...wanted.map(field => field.toLowerCase()), 'id']);
28
- return {
233
+ const rows = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload.data : undefined;
234
+ if (!wanted.length || !Array.isArray(rows))
235
+ return withNormalization(withResolved(payload, resolver.resolved), notes);
236
+ /**
237
+ * 'localizations' keep listesine girmezse includeTranslations'ın getirdiği çeviriler tam da
238
+ * onları isteyen çağrıda kırpılırdı: iki parametre birlikte verildiğinde sessizce boşa çıkar.
239
+ */
240
+ const keep = new Set([...wanted.map(field => field.toLowerCase()), 'id', ...(body.includeTranslations ? ['localizations'] : [])]);
241
+ return withNormalization(withResolved({
29
242
  ...payload,
30
243
  data: rows.map(row => row && typeof row === 'object' && !Array.isArray(row)
31
244
  ? Object.fromEntries(Object.entries(row).filter(([key]) => keep.has(key.toLowerCase())))
32
245
  : row)
33
- };
246
+ }, resolver.resolved), notes);
34
247
  }));
35
248
  server.registerTool('row_get', {
36
249
  title: 'Satır getir',
37
250
  description: 'Tek satırı id ile getirir.',
38
- inputSchema: { project: projectParam, environment: environmentParam, entityId: guid, id: z.string().min(1) },
251
+ inputSchema: { project: projectParam, environment: environmentParam, entityId: entityRef, id: z.string().min(1), language: languageParam, includeTranslations: includeTranslationsParam },
39
252
  annotations: readOnly
40
- }, handle(config, ({ project, environment, entityId, id }) => platform.tenant('GET', project, `/entities/${entityId}/row/${encodeURIComponent(id)}`, { environment })));
253
+ }, handle(config, async ({ project, environment, entityId, id, language, includeTranslations }) => {
254
+ const notes = [];
255
+ const headers = languageHeader(language, notes);
256
+ const resolver = new RefResolver(platform, project, environment);
257
+ const resolvedEntityId = await resolveEntityId(resolver, entityId);
258
+ const payload = await platform.tenant('GET', project, `/entities/${resolvedEntityId}/row/${encodeURIComponent(id)}`, { environment, headers, ...(includeTranslations ? { query: { includeTranslations: true } } : {}) });
259
+ return withNormalization(withResolved(payload, resolver.resolved), notes);
260
+ }));
41
261
  server.registerTool('row_create', {
42
262
  title: 'Satır ekle',
43
- description: "Tek satır için 'row', çoklu ekleme için 'rows' verin (ikisi birden verilmez).",
263
+ description: "Tek satır için 'row', çoklu ekleme için 'rows' verin (ikisi birden verilmez). Çoklu dilli entity'de 'localizations: { tr: {…}, en: {…} }' ile dil satırlarını da aynı çağrıda yazar: companion entity'yi ve FK'yı gateway bulur (yalnız 'row' ile). 'rows' TEK TRANSACTION'da yazılır: bir satır doğrulamadan düşerse HİÇBİRİ yazılmaz ve hata hangi satır indeksinin düştüğünü söyler. Automation'lar satır BAŞINA tetiklenir — sync (isQueue:true) automation'lar aynı transaction'da satır satır koşar, biri düşerse toplu yazma geri alınır. Tek çağrıda en çok 1000 satır. SINIR: row_* bir veritabanı yönetim yüzeyidir — satırları GÖVDEDE taşır. VAR OLAN satırlardan türetilen bir küme (geçmiş/hareket/log/junction, backfill) bu yola AİT DEĞİLDİR; onu tek bir kayıtlı 'INSERT … SELECT' sorgusu + query kaynaklı uç yazar, reçete platform_guide('entity') §'Toplu ve türetilmiş satır'. Toplu ucun id döndürmesine GÜVENMEYİN: 2026-09-07 ölçümünde boş gövde döndü (2026-09-05'te anahtar listesi dönüyordu, fark B-63) — id gerekiyorsa ardından row_list çekin. TOPLU eklemede dil değerlerini satırın KENDİ içine koyun ('rows: [{ kod: 'A1', localizations: { tr: {…} } }]') — üst düzey 'localizations' toplu uçta hangi satıra ait olduğunu söyleyemez. 'row' içine düz çok dilli slug yazarsanız değer İSTEK DİLİ için kaydedilir — dili 'language' ile seçin.",
44
264
  inputSchema: {
45
265
  project: projectParam,
46
266
  environment: environmentParam,
47
- entityId: guid,
267
+ entityId: entityRef,
48
268
  row: rowValues.optional(),
49
- rows: z.array(z.record(z.unknown())).optional()
269
+ rows: z.array(z.record(z.unknown())).optional(),
270
+ path: pathParam,
271
+ relationsByName: relationsByNameParam,
272
+ localizations: localizationsParam,
273
+ language: languageParam,
274
+ rowContext: z.record(z.unknown()).optional()
275
+ .describe("Satır olayının DIŞINDAN automation'a geçirilecek değerler. Tetiklenen automation bunları ifadelerinde `rowContext.<anahtar>` ile okur — zaman damgası, göç kaynağı, toplu iş kimliği gibi. Tarihsel (geçmiş tarihli) veri üretmenin yolu budur: automation `UtcNow()` yerine `rowContext.occurredAt`'i yazabilir. Verilmezse automation bağlamı değişmez.")
50
276
  },
51
277
  annotations: writes
52
- }, handle(config, ({ project, environment, entityId, row, rows }) => {
53
- if (row && rows)
54
- throw new Error("'row' ve 'rows' birlikte verilemez.");
55
- if (rows)
56
- return platform.tenant('POST', project, `/entities/${entityId}/row/bulk`, { environment, body: { rows } });
57
- if (row)
58
- return platform.tenant('POST', project, `/entities/${entityId}/row`, { environment, body: { row } });
59
- throw new Error("'row' ya da 'rows' parametrelerinden biri zorunlu.");
278
+ }, handle(config, async ({ project, environment, entityId, row, rows, path, relationsByName, localizations, rowContext, language }) => {
279
+ assertOneSource('row_create', row, rows, path);
280
+ if (localizations && !row)
281
+ throw new Error("Üst düzey 'localizations' yalnız 'row' ile verilir: toplu uçta hangi satıra ait olduğu belirsizdir. Toplu eklemede dil değerlerini satırın kendi içine koyun — rows: [{ …, localizations: { tr: {…} } }].");
282
+ if (relationsByName && row)
283
+ throw new Error("'relationsByName' yalnız toplu yolda ('rows' ya da 'path') çalışır: tek satır ucu ad → id çözümlemesi yapmaz, ilişki alanına id yazın.");
284
+ const notes = [];
285
+ const headers = languageHeader(language, notes);
286
+ const wantedLocalizations = localizations && Object.keys(localizations).length > 0 ? localizations : undefined;
287
+ const fileRows = path ? await readRowsFromPath('row_create', path) : undefined;
288
+ const bulkRows = rows ?? fileRows;
289
+ const shapeFindings = enforcePolicy(evaluatePolicy(config, policyRules, 'row', bulkRows ? { rows: bulkRows } : { row }, { operation: 'create' }));
290
+ const resolver = new RefResolver(platform, project, environment);
291
+ if (bulkRows) {
292
+ const bulkEntityId = await resolveEntityId(resolver, entityId);
293
+ const bulkDrift = unmappedMembers('CreateBulkRowCommand', { rows: bulkRows, ...(rowContext ? { rowContext } : {}), ...(relationsByName ? { relationsByName } : {}) });
294
+ const outcome = await writeInBatches(platform, 'POST', project, environment, headers, bulkEntityId, bulkRows, rowContext, relationsByName);
295
+ if (fileRows)
296
+ notes.push(`${path} okundu: ${fileRows.length} satır, ${Math.ceil(fileRows.length / bulkBatchSize)} parti.`);
297
+ return attachFindings(withUnmappedMembers(withNormalization(withResolved(outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
298
+ }
299
+ const resolvedEntityId = await resolveEntityId(resolver, entityId);
300
+ const body = { row, ...(wantedLocalizations ? { localizations: wantedLocalizations } : {}), ...(rowContext ? { rowContext } : {}) };
301
+ const drift = unmappedMembers('CreateRowCommand', body);
302
+ const response = await platform.tenantWithHeaders('POST', project, `/entities/${resolvedEntityId}/row`, { environment, headers, body });
303
+ const created = response.body;
304
+ if (!wantedLocalizations || !platformIgnoredLocalizations(response.headers)) {
305
+ return attachFindings(withUnmappedMembers(withNormalization(withResolved(created, resolver.resolved), notes), drift), shapeFindings);
306
+ }
307
+ notes.push("platform 'localizations' alanını tanımıyor (X-Unmapped-Members); dil satırlarını gateway companion entity'ye yazdı.");
308
+ const entity = await resolver.entity(entityId);
309
+ let mainRowId = created.data?.id;
310
+ try {
311
+ // Satır sözlüğü alan SLUG'larıyla anahtarlanır ve PK slug'ı 'id' olmak ZORUNDA değildir
312
+ // (CreateRowCommandHandler `entityDto.PrimaryKey()` kullanır). 'id' yoksa gerçek PK'yı
313
+ // bulmadan yazmak FK'sı NULL dil satırları üretir ve bunu başarı diye raporlardı.
314
+ if (mainRowId === undefined || mainRowId === null) {
315
+ const primaryKey = (await resolver.entityFields(entity)).find(field => field.primaryKey);
316
+ mainRowId = primaryKey ? created.data?.[primaryKey.slug] : undefined;
317
+ }
318
+ if (mainRowId === undefined || mainRowId === null) {
319
+ throw new Error("Ana satır yazıldı ama yanıtta birincil anahtar okunamadı; dil satırları FK'sız yazılmadı (FK'sı boş satır sessiz yanlış olurdu).");
320
+ }
321
+ const companion = await findLocalizationCompanion(resolver, entity);
322
+ const localizationBatch = localizationRows(companion, mainRowId, wantedLocalizations);
323
+ await platform.tenant('POST', project, `/entities/${companion.entityId}/row/bulk`, { environment, body: { rows: localizationBatch } });
324
+ return attachFindings(withUnmappedMembers(withNormalization(withResolved({ ...created, localizations: { entityId: companion.entityId, languages: Object.keys(wantedLocalizations), count: localizationBatch.length } }, resolver.resolved), notes), drift), shapeFindings);
325
+ }
326
+ catch (error) {
327
+ const message = error instanceof Error ? error.message : String(error);
328
+ return attachFindings(withUnmappedMembers(withNormalization(withResolved({ ...created, localizationsFailed: { mainRowId, error: message, note: 'Ana satır YAZILDI, dil satırları yazılamadı; gateway geri alamaz. Ya companion entity\'ye row_create ile elle yazın ya da ana satırı row_delete ile silip tekrar deneyin.' } }, resolver.resolved), notes), drift), shapeFindings);
329
+ }
60
330
  }));
61
331
  server.registerTool('row_update', {
62
332
  title: 'Satır güncelle',
63
- description: "Tek satır için 'row', çoklu güncelleme için 'rows' verin; satır id alanı gövdede olmalıdır.",
333
+ description: "Tek satır için 'row', çoklu güncelleme için 'rows' verin; satır id alanı gövdede olmalıdır. 'rows' TEK TRANSACTION'da yazılır: bir satır düşerse HİÇBİRİ yazılmaz ve hata hangi satır indeksinin düştüğünü söyler. Automation'lar satır BAŞINA tetiklenir — sync (isQueue:true) automation'lar aynı transaction'da satır satır koşar. Tek çağrıda en çok 1000 satır. Aynı sınır row_create'te olduğu gibi geçerlidir: var olan satırlardan HESAPLANAN toplu güncelleme (durum/toplam/damga backfill'i) kayıtlı bir 'UPDATE … FROM' sorgusuna aittir — platform_guide('entity') §'Toplu ve türetilmiş satır'. Çeviri güncellemek için 'localizations: { tr: {…}, en: {…} }' verin — ana satır hiç değişmese de yalnız çeviriler yazılabilir. TOPLU güncellemede aynı nesne satırın KENDİ içine konur ('rows: [{ id: 1, localizations: { tr: {…} } }]'); 'row' içindeki düz çok dilli slug İSTEK DİLİNE yazılır.",
64
334
  inputSchema: {
65
335
  project: projectParam,
66
336
  environment: environmentParam,
67
- entityId: guid,
337
+ entityId: entityRef,
68
338
  row: rowValues.optional(),
69
- rows: z.array(z.record(z.unknown())).optional()
339
+ rows: z.array(z.record(z.unknown())).optional(),
340
+ path: pathParam,
341
+ relationsByName: relationsByNameParam,
342
+ localizations: localizationsParam,
343
+ language: languageParam,
344
+ rowContext: z.record(z.unknown()).optional()
345
+ .describe("Satır olayının DIŞINDAN automation'a geçirilecek değerler. Tetiklenen automation bunları ifadelerinde `rowContext.<anahtar>` ile okur — zaman damgası, göç kaynağı, toplu iş kimliği gibi. Tarihsel (geçmiş tarihli) veri üretmenin yolu budur: automation `UtcNow()` yerine `rowContext.occurredAt`'i yazabilir. Verilmezse automation bağlamı değişmez.")
70
346
  },
71
347
  annotations: updates
72
- }, handle(config, ({ project, environment, entityId, row, rows }) => {
73
- if (row && rows)
74
- throw new Error("'row' ve 'rows' birlikte verilemez.");
75
- if (rows)
76
- return platform.tenant('PUT', project, `/entities/${entityId}/row/bulk`, { environment, body: { rows } });
77
- if (row)
78
- return platform.tenant('PUT', project, `/entities/${entityId}/row`, { environment, body: { row } });
79
- throw new Error("'row' ya da 'rows' parametrelerinden biri zorunlu.");
348
+ }, handle(config, async ({ project, environment, entityId, row, rows, path, relationsByName, localizations, rowContext, language }) => {
349
+ assertOneSource('row_update', row, rows, path);
350
+ if (localizations && !row)
351
+ throw new Error("Üst düzey 'localizations' yalnız 'row' ile verilir: toplu uçta hangi satıra ait olduğu belirsizdir. Toplu güncellemede dil değerlerini satırın kendi içine koyun — rows: [{ id, localizations: { tr: {…} } }].");
352
+ if (relationsByName && row)
353
+ throw new Error("'relationsByName' yalnız toplu yolda ('rows' ya da 'path') çalışır: tek satır ucu ad → id çözümlemesi yapmaz, ilişki alanına id yazın.");
354
+ const notes = [];
355
+ const fileRows = path ? await readRowsFromPath('row_update', path) : undefined;
356
+ const bulkRows = rows ?? fileRows;
357
+ const shapeFindings = enforcePolicy(evaluatePolicy(config, policyRules, 'row', bulkRows ? { rows: bulkRows } : { row }, { operation: 'update' }));
358
+ const headers = languageHeader(language, notes);
359
+ const resolver = new RefResolver(platform, project, environment);
360
+ const resolvedEntityId = await resolveEntityId(resolver, entityId);
361
+ if (bulkRows) {
362
+ const bulkDrift = unmappedMembers('UpdateBulkRowCommand', { rows: bulkRows, ...(rowContext ? { rowContext } : {}), ...(relationsByName ? { relationsByName } : {}) });
363
+ const outcome = await writeInBatches(platform, 'PUT', project, environment, headers, resolvedEntityId, bulkRows, rowContext, relationsByName);
364
+ if (fileRows)
365
+ notes.push(`${path} okundu: ${fileRows.length} satır, ${Math.ceil(fileRows.length / bulkBatchSize)} parti.`);
366
+ return attachFindings(withUnmappedMembers(withNormalization(withResolved(outcome, resolver.resolved), notes), bulkDrift), [...shapeFindings, ...(fileRows ? [] : derivedRowFindings(bulkRows))]);
367
+ }
368
+ const body = { row, ...(localizations ? { localizations } : {}), ...(rowContext ? { rowContext } : {}) };
369
+ const drift = unmappedMembers('UpdateRowCommand', body);
370
+ return attachFindings(withUnmappedMembers(withNormalization(withResolved(await platform.tenant('PUT', project, `/entities/${resolvedEntityId}/row`, { environment, headers, body }), resolver.resolved), notes), drift), shapeFindings);
371
+ }));
372
+ server.registerTool('row_export', {
373
+ title: 'Satırları dışa aktar',
374
+ description: "Bir tablonun satırlarını xlsx / csv / json dosyasına yazar ve dosyayı projenin SÜRÜCÜSÜNE ('Dışa aktarmalar' klasörü) koyar; yanıt yalnız fileId ve sayılardır — dosya içeriği ajanın bağlamına HİÇ girmez, içeriği görmek için file_download kullanın. Kapsam üç şekildedir: 'view' (filtre + sıralama), 'all', 'selected' (rowIds). 5.000 satırın üstünde platform işi arka plana alır ve 202 ile jobId döner; import_status { jobId } ile izlenir, biten işin dosyası result.reportFileId'dedir. CSV UTF-8 BOM'lu yazılır; ayraç ve ondalık bir yerel ayar profilinden gelir (tr-TR `;` · `1,5`, en-US `,` · `1.5`): dışa aktarmada projenin dili seçer, şablonda 'locale' ile seçilir. 'reimportable: true' dosyaya id sütununu ve _zeyrek meta sayfasını ekler: dosya düzenlenip import_analyze → import_plan { mode: 'upsert', keyField: 'id' } ile geri yüklenebilir (Akış C). 'template: true' tablo satırı yazmaz: tablonun alanlarından İKİ SENTETİK örnek satırlı bir şablon üretir ve değiştirilmeden geri yüklenebilir. 'template: true, inline: true' şablonun şeklini (sütunlar, anahtar önerisi, iki satır) yanıta da koyar — toplu yazımdan önce dosyanın nasıl görünmesi gerektiğini indirmeden görmenin yolu budur; satır satır row_create yerine şablon başlıklarıyla CSV → file_upload { text } → import_analyze → import_plan → import_run, reçete platform_guide('entity') §'Dışa aktarma ve geri yükleme'.",
375
+ inputSchema: {
376
+ project: projectParam,
377
+ environment: environmentParam,
378
+ entityId: entityRef,
379
+ format: z.enum(['xlsx', 'csv', 'json']).optional().describe("Varsayılan 'xlsx'. 'json' iş kuyruğuna alınmaz, satır sayısı ne olursa olsun doğrudan döner."),
380
+ scope: exportScopeParam,
381
+ columns: z.enum(['visible', 'all']).optional().describe("'visible' yalnız selectedFields'ta sayılanları, 'all' sistem alanları dahil hepsini yazar; varsayılan 'all'"),
382
+ selectedFields: z.array(z.string()).optional().describe("columns: 'visible' ile birlikte yazılacak alan SLUG'ları"),
383
+ relations: z.enum(['display', 'id', 'both']).optional().describe("İlişki sütunu görünen adla mı, id ile mi, ikisiyle mi yazılsın; varsayılan 'display'. reimportable ile 'both' zorunludur ve gateway onu kendisi seçer."),
384
+ languages: z.enum(['request', 'all']).optional().describe("'all' çok dilli sütunların TÜM dillerini ayrı sütunlara yazar; varsayılan yalnız istek dili"),
385
+ files: z.enum(['name', 'link']).optional().describe("Dosya sütunu ad olarak mı indirme bağlantısı olarak mı yazılsın; varsayılan 'name'"),
386
+ reimportable: z.boolean().optional().describe("Dosyaya id sütunu ve _zeyrek meta sayfası ekler; ilişki sütunları ad+id olarak yazılır. Geri yükleme yolu: import_analyze → import_plan { mode: 'upsert', keyField: 'id' }."),
387
+ template: z.boolean().optional().describe("Tablonun alanlarından iki örnek satırlı şablon; ipuçları veri alanının dışında (xlsx başlık yorumu + `_zeyrek`, json `meta`, csv'de yok); değiştirilmeden geri yüklenebilir. PK, identity, sistem ve artan kod sütunu yoktur; değerler sentetiktir, ilişki sütunu boş ve `manual`dır. scope/columns/selectedFields/relations/languages/files/reimportable yok sayılır."),
388
+ locale: z.enum(['tr-TR', 'en-US']).optional().describe("Yalnız template ile: csv ayracı, ondalık ve tarih profilini ve inline örneğin yerel ayarını seçer (tr-TR `;` · `1,5` · `dd.MM.yyyy`; en-US `,` · `1.5` · `M/d/yyyy`). Verilmezse projenin varsayılan dili seçer. Dışa aktarmada (template yok) yok sayılır."),
389
+ inline: z.boolean().optional().describe(`Yalnız template: true ile: şablonun şekli yanıta 'sample { entity, locale, keyField, columns: [{ slug, header, type, required, options?, relationEntity?, matchField?, manual? }], data }' olarak da konur — data iki sentetik satırdır (JSON biçimi: ondalık nokta, tarih yyyy-MM-dd). Örnek ${templateInlineMaxChars} karakteri aşarsa data atlanır, columns kalır. template olmadan istek gitmeden reddedilir.`),
390
+ download: z.string().min(1).optional().describe("Ayrıca gateway sürecinin yazabildiği bu yerel yola kaydeder; Sürücü'deki kopya yine oluşur.")
391
+ },
392
+ annotations: writes
393
+ }, handle(config, async ({ project, environment, entityId, format, scope, columns, selectedFields, relations, languages, files, reimportable, template, locale, inline, download }) => {
394
+ if (inline && !template) {
395
+ throw new PlatformApiError('row_export: inline yalnız template: true ile kullanılır — satır içi örnek şablonun şeklidir; tablonun gerçek satırlarını görmek için row_list kullanın.');
396
+ }
397
+ if (scope?.kind === 'selected' && !(scope.rowIds?.length)) {
398
+ throw new PlatformApiError("row_export: scope.kind 'selected' verildiğinde rowIds boş olamaz — hangi satırların yazılacağı belirsizdir.");
399
+ }
400
+ if ((scope?.rowIds?.length ?? 0) > maxSelectedRowIds) {
401
+ throw new PlatformApiError(`row_export: ${scope?.rowIds?.length} satır id'si, ${maxSelectedRowIds} sınırının üstünde. Bu kadar satırı seçerek değil filtreyle verin: scope { kind: 'view', filters: [...] }.`);
402
+ }
403
+ const resolvedFormat = format ?? 'xlsx';
404
+ const notes = [];
405
+ const resolver = new RefResolver(platform, project, environment);
406
+ const resolvedEntityId = await resolveEntityId(resolver, entityId);
407
+ const response = template
408
+ ? await platform.tenantWithHeaders('GET', project, `/entities/${resolvedEntityId}/import/template`, { environment, query: { format: resolvedFormat, locale }, expectBinary: true })
409
+ : await platform.tenantWithHeaders('POST', project, `/entities/${resolvedEntityId}/export`, {
410
+ environment,
411
+ expectBinary: true,
412
+ body: {
413
+ format: resolvedFormat,
414
+ scope: {
415
+ kind: scope?.kind ?? 'all',
416
+ ...(scope?.filters ? { filters: scope.filters } : {}),
417
+ ...(scope?.sort ? { sort: { field: scope.sort.field, ascending: scope.sort.ascending ?? true } } : {}),
418
+ ...(scope?.rowIds ? { rowIds: scope.rowIds } : {})
419
+ },
420
+ columns: columns ?? 'all',
421
+ ...(selectedFields ? { selectedFields } : {}),
422
+ relations: reimportable ? 'both' : relations ?? 'display',
423
+ languages: languages ?? 'request',
424
+ files: files ?? 'name',
425
+ reimportable: reimportable ?? false
426
+ }
427
+ });
428
+ const payload = response.body;
429
+ if (response.httpStatus === 202) {
430
+ const started = JSON.parse(payload.bytes.toString('utf8'));
431
+ return withResolved({
432
+ jobId: started.data?.jobId,
433
+ note: "Satır sayısı platformun eşiğini aştı, iş arka plana alındı. import_status { jobId } ile izleyin; tamamlanınca dosya result.reportFileId'dedir ve file_download ile okunur. Gateway beklemez."
434
+ }, resolver.resolved);
435
+ }
436
+ const noteParts = template ? [templateFormatNotes[resolvedFormat], 'İki satır sentetik örnektir — üzerine yazın ya da silin.'] : [];
437
+ let sample;
438
+ if (template && inline) {
439
+ const sampled = await platform.tenant('GET', project, `/entities/${resolvedEntityId}/import/sample`, { environment, query: { locale } });
440
+ const dto = sampled?.data ?? {};
441
+ const sampleJson = dto.json ?? '';
442
+ const sampleColumns = (dto.columns ?? []).map(column => ({
443
+ slug: column.slug,
444
+ header: column.header,
445
+ type: column.type,
446
+ required: column.required,
447
+ ...(column.options?.length ? { options: column.options } : {}),
448
+ ...(column.relationEntity ? { relationEntity: column.relationEntity } : {}),
449
+ ...(column.matchField && column.relationEntity ? { matchField: column.matchField } : {}),
450
+ ...(column.manual ? { manual: true } : {})
451
+ }));
452
+ const fits = sampleJson.length <= templateInlineMaxChars;
453
+ sample = {
454
+ entity: dto.entitySlug,
455
+ locale: dto.locale,
456
+ keyField: dto.keyField ?? null,
457
+ columns: sampleColumns,
458
+ ...(fits ? { data: JSON.parse(sampleJson || '{}').data ?? [] } : {})
459
+ };
460
+ if (!fits) {
461
+ noteParts.push(`Satır içi örnek ${sampleJson.length} karakter, ${templateInlineMaxChars} sınırının üstünde: sample.data atlandı, sample.columns duruyor. Örnek satırları görmek için şablon dosyasını file_download ile okuyun.`);
462
+ }
463
+ const maskedSlugs = sampleColumns.map(column => column.slug ?? '').filter(slug => secretKeyPattern.test(slug));
464
+ if (fits && maskedSlugs.length > 0) {
465
+ noteParts.push(`${maskedSlugs.join(', ')} örnek değeri gateway'in gizli alan maskesiyle *** döner — örnek değildir, bu sütuna kendi değerinizi yazın.`);
466
+ }
467
+ if (sampleColumns.some(column => column.manual)) {
468
+ noteParts.push("manual sütunlar boştur: ilişki hücresine hedef tablonun matchField değerini yazın (şablon hedeften satır okumaz).");
469
+ }
470
+ }
471
+ const fileName = exportFileName(response.headers, 'export', resolvedFormat);
472
+ const folderId = await exportFolderIdAsync(platform, project, environment);
473
+ const uploaded = await uploadFile(platform, {
474
+ project,
475
+ environment,
476
+ content: payload.bytes.toString('base64'),
477
+ filename: fileName,
478
+ contentType: payload.contentType,
479
+ folderId
480
+ });
481
+ const fileId = uploaded?.data?.id;
482
+ if (download) {
483
+ await writeFile(resolve(download), payload.bytes);
484
+ notes.push(`Dosya ayrıca ${resolve(download)} yoluna yazıldı.`);
485
+ }
486
+ if (reimportable) {
487
+ notes.push("Dosya id sütunu ve _zeyrek meta sayfası taşıyor: düzenledikten sonra file_upload → import_analyze → import_plan { mode: 'upsert', keyField: 'id' } ile geri yükleyin.");
488
+ }
489
+ if (resolvedFormat === 'csv' && template) {
490
+ notes.push(`CSV şablon UTF-8 BOM'lu; ayraç, ondalık ve tarih \`locale\` profilinden gelir (tr-TR \`;\` · \`1,5\` · \`dd.MM.yyyy\`, en-US \`,\` · \`1.5\` · \`M/d/yyyy\`)${locale ? `, seçilen ${locale}` : ", locale verilmediği için projenin varsayılan dili seçti"}. Geri yüklerken import_analyze ve import_plan'a aynı locale'i verin; plan analizin locale'ini devralmaz.`);
491
+ }
492
+ else if (resolvedFormat === 'csv') {
493
+ notes.push("CSV UTF-8 BOM'lu yazıldı; ayraç ve ondalık projenin dilinin profilinden gelir (tr → noktalı virgül `;` · `1,5`, diğer diller → virgül `,` · `1.5`). Dışa aktarmada locale seçilemez.");
494
+ }
495
+ if (locale && !template) {
496
+ notes.push('locale yalnız template ile kullanılır; dışa aktarmada yok sayıldı.');
497
+ }
498
+ return withNormalization(withResolved({
499
+ fileId,
500
+ fileName,
501
+ bytes: payload.bytes.byteLength,
502
+ contentType: payload.contentType,
503
+ folder: exportFolderName,
504
+ ...(sample ? { sample } : {}),
505
+ note: [`Dosya Sürücü'de. İçeriği okumak için file_download { id: '${String(fileId)}' }.`, ...noteParts].join(' ')
506
+ }, resolver.resolved), notes);
80
507
  }));
81
508
  server.registerTool('row_delete', {
82
509
  title: 'Satır sil',
83
- description: "Tek satır için 'id', çoklu silme için 'ids' verin.",
510
+ description: "Tek satır için 'id', çoklu silme için 'ids' verin. Çok dilli entity'de dil satırlarını platform ana satırdan ÖNCE kendisi siler; eski platform sürümünde yabancı anahtar hatası dönerse önce companion entity'nin satırlarını silin.",
84
511
  inputSchema: {
85
512
  project: projectParam,
86
513
  environment: environmentParam,
87
- entityId: guid,
514
+ entityId: entityRef,
88
515
  id: z.string().optional(),
89
516
  ids: z.array(z.union([z.string(), z.number()])).optional()
90
517
  },
91
518
  annotations: destructive
92
- }, handle(config, ({ project, environment, entityId, id, ids }) => {
519
+ }, handle(config, async ({ project, environment, entityId, id, ids }) => {
93
520
  if (id && ids)
94
521
  throw new Error("'id' ve 'ids' birlikte verilemez.");
522
+ if (!id && !ids)
523
+ throw new Error("'id' ya da 'ids' parametrelerinden biri zorunlu.");
524
+ const resolver = new RefResolver(platform, project, environment);
525
+ const resolvedEntityId = await resolveEntityId(resolver, entityId);
95
526
  if (ids)
96
- return platform.tenant('POST', project, `/entities/${entityId}/row/bulk/delete`, { environment, body: { rowIds: ids } });
97
- if (id)
98
- return platform.tenant('DELETE', project, `/entities/${entityId}/row/${encodeURIComponent(id)}`, { environment });
99
- throw new Error("'id' ya da 'ids' parametrelerinden biri zorunlu.");
527
+ return withResolved(await platform.tenant('POST', project, `/entities/${resolvedEntityId}/row/bulk/delete`, { environment, body: { rowIds: ids } }), resolver.resolved);
528
+ return withResolved(await platform.tenant('DELETE', project, `/entities/${resolvedEntityId}/row/${encodeURIComponent(String(id))}`, { environment }), resolver.resolved);
100
529
  }));
101
530
  server.registerTool('row_next_code', {
102
531
  title: 'Sıradaki kodu üret',
103
- description: 'Incremental tipli alan için sıradaki kodu döner.',
532
+ description: "Incremental tipli alan için sıradaki kodu döner ve sayacı İLERLETMEZ (persist:false) — kodun nasıl görüneceğini kaydetmeden görmek için. Gövde row_create'in Incremental alanına yazdığınız nesneyle aynıdır: { prefix, separator, counter, digitCount }. 'counter' ve 'digitCount' platform tarafında ZORUNLUDUR (GetNextIncrementalCodeQueryValidator: NotEmpty) — eksik verilirse istek 400 döner, bu yüzden gateway de ikisini zorunlu tutar.",
104
533
  inputSchema: {
105
534
  project: projectParam,
106
535
  environment: environmentParam,
107
- entityId: guid,
536
+ entityId: entityRef,
108
537
  fieldId: guid,
109
- prefix: z.string().optional(),
110
- separator: z.string().optional(),
111
- digitCount: z.number().int().optional()
538
+ prefix: z.string().optional().describe("Kodun önüne gelen sabit ('EMP')"),
539
+ separator: z.string().optional().describe("Önek ile sayaç arasındaki ayraç ('-'); verilmezse boş"),
540
+ counter: z.number().int().min(1).describe('Sayacın BAŞLANGIÇ değeri — platform zorunlu tutar (NotEmpty, yani 0 kabul edilmez). Alanın kayıtlı sayacını bilmiyorsanız entity_get ile alanın tanımına bakın.'),
541
+ digitCount: z.number().int().min(1).describe("Sayacın basamak sayısı (4 → '0006'); platform zorunlu tutar (NotEmpty)")
112
542
  },
113
543
  annotations: readOnly
114
- }, handle(config, ({ project, environment, entityId, ...body }) => platform.tenant('POST', project, `/entities/${entityId}/row/next-code`, { environment, body })));
544
+ }, handle(config, async ({ project, environment, entityId, ...body }) => {
545
+ const resolver = new RefResolver(platform, project, environment);
546
+ const resolvedEntityId = await resolveEntityId(resolver, entityId);
547
+ return withResolved(await platform.tenant('POST', project, `/entities/${resolvedEntityId}/row/next-code`, { environment, body }), resolver.resolved);
548
+ }));
115
549
  }