@appaflytech/zeyrek-ai-mcp 0.0.6 → 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.
- package/.env.example +2 -2
- package/.env.production.example +8 -0
- package/README.md +9 -8
- package/dist/config.js +18 -3
- package/dist/knowledge/backendConstraints.js +85 -0
- package/dist/knowledge/componentCatalog.js +357 -54
- package/dist/knowledge/componentEvents.js +120 -25
- package/dist/knowledge/functionGuards.js +134 -0
- package/dist/knowledge/normalize.js +53 -2
- package/dist/knowledge/outlines.js +31 -0
- package/dist/knowledge/policy.js +14 -5
- package/dist/knowledge/rules.js +343 -1
- package/dist/platform/csv.js +123 -0
- package/dist/platform/errors.js +62 -3
- package/dist/platform/http.js +1 -0
- package/dist/platform/responses.js +16 -4
- package/dist/platform/screenshot.js +18 -1
- package/dist/resources/examples/flow-call-function.json +66 -0
- package/dist/resources/examples/function-total.json +70 -0
- package/dist/resources/examples/microfunction-node-sum.json +56 -0
- package/dist/resources/examples/page-dynamic-form.json +1 -1
- package/dist/resources/examples/page-sign-in.json +1 -1
- package/dist/resources/guides/component.md +13 -1
- package/dist/resources/guides/entity.md +321 -17
- package/dist/resources/guides/expressions.md +3 -0
- package/dist/resources/guides/functions.md +262 -0
- package/dist/resources/guides/getting-started.md +5 -0
- package/dist/resources/guides/merge.md +231 -0
- package/dist/resources/guides/microfunction.md +166 -0
- package/dist/resources/guides/microservice.md +61 -2
- package/dist/resources/guides/modify-existing.md +23 -0
- package/dist/resources/guides/page.md +20 -4
- package/dist/resources/guides/translation.md +24 -0
- package/dist/resources/guides/workflow.md +1 -1
- package/dist/resources/schema-fingerprint.json +4 -4
- package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
- package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
- package/dist/resources/schemas/CreateBulkRowCommand.json +3 -0
- package/dist/resources/schemas/CreateEnvironmentCommand.json +31 -0
- package/dist/resources/schemas/CreateGlobalFunctionCommand.json +95 -0
- package/dist/resources/schemas/CreateImportPlanCommand.json +278 -0
- package/dist/resources/schemas/CreateMergePlanCommand.json +34 -0
- package/dist/resources/schemas/CreateMicrofunctionCommand.json +117 -0
- package/dist/resources/schemas/CreateMicrofunctionRuntimeCommand.json +51 -0
- package/dist/resources/schemas/CreateMicroserviceCommand.json +119 -1
- package/dist/resources/schemas/CreateWorkflowCommand.json +13 -2
- package/dist/resources/schemas/DataJobDto.json +267 -0
- package/dist/resources/schemas/ExportRowsCommand.json +172 -0
- package/dist/resources/schemas/Filter.json +3 -1
- package/dist/resources/schemas/GetRowsQuery.json +3 -1
- package/dist/resources/schemas/GlobalFunctionDto.json +89 -0
- package/dist/resources/schemas/GlobalFunctionUpdateResultDto.json +249 -0
- package/dist/resources/schemas/ImportAnalysisDto.json +401 -0
- package/dist/resources/schemas/ImportPlanDto.json +260 -0
- package/dist/resources/schemas/ImportSampleDto.json +90 -0
- package/dist/resources/schemas/InvokeMicrofunctionCommand.json +25 -0
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +119 -1
- package/dist/resources/schemas/PreviewWorkflowCommand.json +13 -2
- package/dist/resources/schemas/ResolveMergeConflictsCommand.json +58 -0
- package/dist/resources/schemas/RevertMergePlanCommand.json +19 -0
- package/dist/resources/schemas/RunImportCommand.json +62 -0
- package/dist/resources/schemas/StepDto.json +13 -2
- package/dist/resources/schemas/UpdateBulkRowCommand.json +3 -0
- package/dist/resources/schemas/UpdateEntityCommand.json +3 -1
- package/dist/resources/schemas/UpdateGlobalFunctionCommand.json +66 -0
- package/dist/resources/schemas/UpdateMicrofunctionCommand.json +121 -0
- package/dist/resources/schemas/UpdateMicrofunctionRuntimeCommand.json +54 -0
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +119 -1
- package/dist/resources/schemas/UpdateWorkflowCommand.json +13 -2
- package/dist/server.js +15 -3
- package/dist/tools/activities.js +40 -0
- package/dist/tools/automations.js +14 -5
- package/dist/tools/common.js +16 -0
- package/dist/tools/dependencies.js +1 -1
- package/dist/tools/environments.js +52 -0
- package/dist/tools/files.js +51 -50
- package/dist/tools/flows.js +95 -2
- package/dist/tools/functions.js +331 -0
- package/dist/tools/importSchemas.js +79 -0
- package/dist/tools/imports.js +241 -0
- package/dist/tools/merges.js +113 -0
- package/dist/tools/microfunctions.js +155 -0
- package/dist/tools/microservices.js +42 -3
- package/dist/tools/pages.js +62 -10
- package/dist/tools/platformInfo.js +4 -1
- package/dist/tools/revisions.js +94 -0
- package/dist/tools/rows.js +342 -22
- package/dist/tools/screenWrite.js +11 -2
- package/dist/tools/templates.js +32 -5
- package/dist/tools/workflows.js +24 -6
- package/package.json +3 -1
package/dist/tools/rows.js
CHANGED
|
@@ -7,6 +7,12 @@ import { attachFindings, evaluatePolicy, enforcePolicy } from '../knowledge/poli
|
|
|
7
7
|
import { policyRules } from '../knowledge/rules.js';
|
|
8
8
|
import { languageHeader } from '../knowledge/languageCodes.js';
|
|
9
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';
|
|
10
16
|
import { z } from 'zod';
|
|
11
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)");
|
|
12
18
|
/**
|
|
@@ -25,10 +31,178 @@ function derivedRowFindings(rows) {
|
|
|
25
31
|
return [{
|
|
26
32
|
ruleId: 'ROW-001',
|
|
27
33
|
severity: 'warn',
|
|
28
|
-
message: `Gövdede ${rows.length} satır var. 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'.`,
|
|
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'.`,
|
|
29
35
|
guide: 'ngapps://guides/entity'
|
|
30
36
|
}];
|
|
31
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
|
+
}
|
|
32
206
|
const includeTranslationsParam = z.boolean().optional()
|
|
33
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).");
|
|
34
208
|
const localizationsParam = z.record(z.record(z.unknown())).optional()
|
|
@@ -43,7 +217,7 @@ export function registerRowTools(server, platform) {
|
|
|
43
217
|
environment: environmentParam,
|
|
44
218
|
entityId: entityRef,
|
|
45
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'),
|
|
46
|
-
filters:
|
|
220
|
+
filters: rowFiltersParam,
|
|
47
221
|
language: languageParam,
|
|
48
222
|
includeTranslations: includeTranslationsParam,
|
|
49
223
|
...pagingParams
|
|
@@ -93,29 +267,34 @@ export function registerRowTools(server, platform) {
|
|
|
93
267
|
entityId: entityRef,
|
|
94
268
|
row: rowValues.optional(),
|
|
95
269
|
rows: z.array(z.record(z.unknown())).optional(),
|
|
270
|
+
path: pathParam,
|
|
271
|
+
relationsByName: relationsByNameParam,
|
|
96
272
|
localizations: localizationsParam,
|
|
97
273
|
language: languageParam,
|
|
98
274
|
rowContext: z.record(z.unknown()).optional()
|
|
99
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.")
|
|
100
276
|
},
|
|
101
277
|
annotations: writes
|
|
102
|
-
}, handle(config, async ({ project, environment, entityId, row, rows, localizations, rowContext, language }) => {
|
|
103
|
-
|
|
104
|
-
throw new Error("'row' ve 'rows' birlikte verilemez.");
|
|
278
|
+
}, handle(config, async ({ project, environment, entityId, row, rows, path, relationsByName, localizations, rowContext, language }) => {
|
|
279
|
+
assertOneSource('row_create', row, rows, path);
|
|
105
280
|
if (localizations && !row)
|
|
106
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: {…} } }].");
|
|
107
|
-
if (
|
|
108
|
-
throw new Error("'
|
|
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.");
|
|
109
284
|
const notes = [];
|
|
110
285
|
const headers = languageHeader(language, notes);
|
|
111
286
|
const wantedLocalizations = localizations && Object.keys(localizations).length > 0 ? localizations : undefined;
|
|
112
|
-
const
|
|
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' }));
|
|
113
290
|
const resolver = new RefResolver(platform, project, environment);
|
|
114
|
-
if (
|
|
291
|
+
if (bulkRows) {
|
|
115
292
|
const bulkEntityId = await resolveEntityId(resolver, entityId);
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
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))]);
|
|
119
298
|
}
|
|
120
299
|
const resolvedEntityId = await resolveEntityId(resolver, entityId);
|
|
121
300
|
const body = { row, ...(wantedLocalizations ? { localizations: wantedLocalizations } : {}), ...(rowContext ? { rowContext } : {}) };
|
|
@@ -158,33 +337,174 @@ export function registerRowTools(server, platform) {
|
|
|
158
337
|
entityId: entityRef,
|
|
159
338
|
row: rowValues.optional(),
|
|
160
339
|
rows: z.array(z.record(z.unknown())).optional(),
|
|
340
|
+
path: pathParam,
|
|
341
|
+
relationsByName: relationsByNameParam,
|
|
161
342
|
localizations: localizationsParam,
|
|
162
343
|
language: languageParam,
|
|
163
344
|
rowContext: z.record(z.unknown()).optional()
|
|
164
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.")
|
|
165
346
|
},
|
|
166
347
|
annotations: updates
|
|
167
|
-
}, handle(config, async ({ project, environment, entityId, row, rows, localizations, rowContext, language }) => {
|
|
168
|
-
|
|
169
|
-
throw new Error("'row' ve 'rows' birlikte verilemez.");
|
|
348
|
+
}, handle(config, async ({ project, environment, entityId, row, rows, path, relationsByName, localizations, rowContext, language }) => {
|
|
349
|
+
assertOneSource('row_update', row, rows, path);
|
|
170
350
|
if (localizations && !row)
|
|
171
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: {…} } }].");
|
|
172
|
-
if (
|
|
173
|
-
throw new Error("'
|
|
174
|
-
const shapeFindings = enforcePolicy(evaluatePolicy(config, policyRules, 'row', rows ? { rows } : { row }, { operation: 'update' }));
|
|
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.");
|
|
175
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' }));
|
|
176
358
|
const headers = languageHeader(language, notes);
|
|
177
359
|
const resolver = new RefResolver(platform, project, environment);
|
|
178
360
|
const resolvedEntityId = await resolveEntityId(resolver, entityId);
|
|
179
|
-
if (
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
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))]);
|
|
183
367
|
}
|
|
184
368
|
const body = { row, ...(localizations ? { localizations } : {}), ...(rowContext ? { rowContext } : {}) };
|
|
185
369
|
const drift = unmappedMembers('UpdateRowCommand', body);
|
|
186
370
|
return attachFindings(withUnmappedMembers(withNormalization(withResolved(await platform.tenant('PUT', project, `/entities/${resolvedEntityId}/row`, { environment, headers, body }), resolver.resolved), notes), drift), shapeFindings);
|
|
187
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);
|
|
507
|
+
}));
|
|
188
508
|
server.registerTool('row_delete', {
|
|
189
509
|
title: 'Satır sil',
|
|
190
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.",
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { invalidScreenEventNames, invalidWidgetTypeNodes, nestedRepeaterNodes, nestedRepeaterMessage, blockSizeNote, heightTypeNote, sizeStringNote } from '../knowledge/normalize.js';
|
|
2
|
+
import { FUNCTIONS_NAMESPACE } from '../knowledge/functionGuards.js';
|
|
2
3
|
import { applyCatalogLayoutRules } from './components.js';
|
|
3
4
|
import { PlatformApiError } from '../platform/errors.js';
|
|
4
5
|
const GUARD_CODES = {
|
|
5
6
|
page: { events: 'PAGE-011', widgets: 'PAGE-012', noun: 'sayfa', tree: 'schema' },
|
|
6
7
|
layout: { events: 'LAY-004', widgets: 'LAY-005', noun: 'layout', tree: 'views' }
|
|
7
8
|
};
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* JS tanımlayıcısı olmayan ya da AYRILMIŞ `name` alanlarını toplar; ad
|
|
11
|
+
* `const <ad> = Components["<ad>"]` üretimine girer.
|
|
12
|
+
*/
|
|
9
13
|
export function invalidComponentNames(nodes) {
|
|
10
14
|
const found = [];
|
|
11
15
|
for (const node of nodes ?? []) {
|
|
@@ -13,6 +17,11 @@ export function invalidComponentNames(nodes) {
|
|
|
13
17
|
if (typeof name === 'string' && name.length > 0 && !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name.replace(/-/g, ''))) {
|
|
14
18
|
found.push(`'${name}' (${node.elementId ?? '?'})`);
|
|
15
19
|
}
|
|
20
|
+
else if (typeof name === 'string' && name.replace(/-/g, '') === FUNCTIONS_NAMESPACE) {
|
|
21
|
+
// K1: preamble her komponent için `const <ad> = Components["<ad>"]` üretir; `Functions` adlı
|
|
22
|
+
// komponent kullanıcı fonksiyonlarının ad alanını ezer ve sayfadaki her Functions.x() ölür.
|
|
23
|
+
found.push(`'${name}' (${node.elementId ?? '?'}) — ayrılmış ad: ${FUNCTIONS_NAMESPACE} kullanıcı fonksiyonlarının ad alanıdır (K1)`);
|
|
24
|
+
}
|
|
16
25
|
found.push(...invalidComponentNames(node.children));
|
|
17
26
|
}
|
|
18
27
|
return found;
|
|
@@ -37,7 +46,7 @@ export function assertScreenWriteGuards(kind, screen) {
|
|
|
37
46
|
throw new PlatformApiError(`Geçersiz widget düğümü (istek gönderilmedi): ${badWidgets.join(' · ')}`, 400);
|
|
38
47
|
const badNames = invalidComponentNames(screen.schema);
|
|
39
48
|
if (badNames.length > 0) {
|
|
40
|
-
throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil (istek gönderilmedi): ${badNames.join(' · ')}. `
|
|
49
|
+
throw new PlatformApiError(`PAGE-009 — komponent adı JS tanımlayıcısı değil ya da ayrılmış (istek gönderilmedi): ${badNames.join(' · ')}. `
|
|
41
50
|
+ 'Runtime her komponent için `const <ad> = Components["<ad>"]` üretir; boşluklu ya da geçersiz bir ad '
|
|
42
51
|
+ 'SAYFADAKİ TÜM ifadeleri "Missing initializer in const declaration" ile öldürür (layout adları her sayfaya sızar). '
|
|
43
52
|
+ `name alanını elementId ile aynı yaz; okunabilir Türkçe başlık için props.title / props.text / props.label kullan (${codes.noun} ${codes.tree}).`, 400);
|
package/dist/tools/templates.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { guid, pagingParams, readOnly, writes, updates } from './common.js';
|
|
2
|
+
import { withNormalization } from '../knowledge/normalize.js';
|
|
2
3
|
import { handle } from '../platform/responses.js';
|
|
3
4
|
import { z } from 'zod';
|
|
5
|
+
/**
|
|
6
|
+
* `environmentId` verilmediğinde platform PRODUCTION'ı paketler
|
|
7
|
+
* (`CreateProjectAsTemplateCommand`: `EnvironmentId ??= ActiveEnvironments().First(f => f.IsProduction)`).
|
|
8
|
+
* Promote edilmemiş projede o environment boştur ve build yine 'success' döner; şablon seed
|
|
9
|
+
* kayıtlarından ibaret kalır. Çağıran bunu yanıttan görmediği için hangi environment'ın
|
|
10
|
+
* paketlendiğini adıyla söylüyoruz (2026-09-12 koşusu S-2).
|
|
11
|
+
*/
|
|
12
|
+
async function defaultTemplateEnvironmentNote(platform, projectId) {
|
|
13
|
+
const fallback = "environmentId verilmedi: platform PRODUCTION environment'ını paketler (varsayılan/development değil). Promote edilmemiş projede şablon boş çıkar; development içeriğini istiyorsanız o environment'ın id'sini açıkça verin.";
|
|
14
|
+
try {
|
|
15
|
+
const payload = await platform.org('GET', `/projects/${projectId}`);
|
|
16
|
+
const environments = Array.isArray(payload?.data?.environments)
|
|
17
|
+
? payload.data.environments
|
|
18
|
+
: [];
|
|
19
|
+
const production = environments.find(environment => environment.isProduction === true);
|
|
20
|
+
if (!production)
|
|
21
|
+
return fallback;
|
|
22
|
+
return `environmentId verilmedi: platform '${String(production.name)}' (production, id ${String(production.id)}) environment'ını paketler — varsayılan/development environment'ı DEĞİL. Oraya henüz promote etmediyseniz şablon yalnız seed kayıtlarını taşır; development içeriğini paketlemek için o environment'ın id'sini açıkça verin.`;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
4
28
|
export function registerTemplateTools(server, platform) {
|
|
5
29
|
const config = platform.config;
|
|
6
30
|
server.registerTool('template_list', {
|
|
@@ -24,31 +48,34 @@ export function registerTemplateTools(server, platform) {
|
|
|
24
48
|
description: "Şablonun bir modüldeki kayıtlarını listeler (project_create'in templateOptions.filters'ını doldurmak için).",
|
|
25
49
|
inputSchema: {
|
|
26
50
|
id: guid,
|
|
27
|
-
module: z.string().describe("Module değeri: entity | microservice | workflow | page | layout | component | query | view …")
|
|
51
|
+
module: z.string().describe("Module değeri: entity | microservice | workflow | page | layout | component | query | view | variable | global-function (fonksiyonlar, iki kaynak) | flow … (platform Module enum değeri, kebab-case)")
|
|
28
52
|
},
|
|
29
53
|
annotations: readOnly
|
|
30
54
|
}, handle(config, ({ id, module }) => platform.org('GET', `/templates/${id}/items`, { query: { module } })));
|
|
31
55
|
server.registerTool('template_create', {
|
|
32
56
|
title: 'Projeyi şablon olarak yayımla',
|
|
33
|
-
description:
|
|
57
|
+
description: "Mevcut projeden yeni bir şablon sürümü üretir. **Paketlenen environment `environmentId`'dir ve boş bırakılırsa platform PRODUCTION'ı paketler** (`CreateProjectAsTemplateCommand`: `EnvironmentId ??= ActiveEnvironments().First(f => f.IsProduction)`) — 'varsayılan environment' DEĞİL. Henüz promote edilmemiş projede production boştur: build 'success' döner ama şablon yalnız seed kayıtlarını taşır (microfunction/workflow/microservice sıfır). Geliştirmede yazdığınız içeriği paketlemek için development environment id'sini AÇIKÇA verin. Build asenkrondur: yanıt hemen döner, sürüm birkaç saniye sonra template_list'te görünür; içeriği template_items ile doğrulayın. Görsel yükleme bu tool'da desteklenmez (ikili aktarım kapsam dışı).",
|
|
34
58
|
inputSchema: {
|
|
35
59
|
projectId: guid,
|
|
36
60
|
name: z.string().min(1),
|
|
37
61
|
version: z.string().min(1).describe("Sürüm etiketi, ör. '1.0.0'"),
|
|
38
62
|
description: z.string().optional(),
|
|
39
63
|
changeLog: z.string().optional(),
|
|
40
|
-
environmentId: guid.optional().describe(
|
|
64
|
+
environmentId: guid.optional().describe("Hangi environment'ın içeriği paketlenecek. BOŞSA PRODUCTION paketlenir (projenin varsayılan/development environment'ı değil) — development içeriğini şablona koyacaksanız o environment'ın id'sini açıkça verin.")
|
|
41
65
|
},
|
|
42
66
|
annotations: writes
|
|
43
|
-
}, handle(config, ({ projectId, name, version, description, changeLog, environmentId }) => {
|
|
67
|
+
}, handle(config, async ({ projectId, name, version, description, changeLog, environmentId }) => {
|
|
44
68
|
const form = new URLSearchParams({ Name: name, Version: version });
|
|
69
|
+
const normalized = [];
|
|
45
70
|
if (description)
|
|
46
71
|
form.set('Description', description);
|
|
47
72
|
if (changeLog)
|
|
48
73
|
form.set('ChangeLog', changeLog);
|
|
49
74
|
if (environmentId)
|
|
50
75
|
form.set('EnvironmentId', environmentId);
|
|
51
|
-
|
|
76
|
+
else
|
|
77
|
+
normalized.push(await defaultTemplateEnvironmentNote(platform, projectId));
|
|
78
|
+
return withNormalization(await platform.org('POST', `/projects/${projectId}/templates`, { form }), normalized);
|
|
52
79
|
}));
|
|
53
80
|
server.registerTool('template_check_updates', {
|
|
54
81
|
title: 'Şablon güncellemesi var mı',
|
package/dist/tools/workflows.js
CHANGED
|
@@ -4,7 +4,7 @@ import { evaluatePolicy, enforcePolicy, attachFindings } from '../knowledge/poli
|
|
|
4
4
|
import { workflowOutline, resolvePart, resolvePatchOperations } from '../knowledge/outlines.js';
|
|
5
5
|
import { patchResult, baseVersionOf } from '../platform/patch.js';
|
|
6
6
|
import { writeDiff, withWriteDiff } from '../platform/writeDiff.js';
|
|
7
|
-
import { policyRules } from '../knowledge/rules.js';
|
|
7
|
+
import { policyRules, microfunctionStepFindings } from '../knowledge/rules.js';
|
|
8
8
|
import { languageHeader } from '../knowledge/languageCodes.js';
|
|
9
9
|
import { handle } from '../platform/responses.js';
|
|
10
10
|
import fastJsonPatch from 'fast-json-patch';
|
|
@@ -116,7 +116,10 @@ export function registerWorkflowTools(server, platform) {
|
|
|
116
116
|
annotations: writes
|
|
117
117
|
}, handle(config, async ({ project, environment, applicationId, definition, policyOverride }) => {
|
|
118
118
|
const normalized = normalizeWorkflowDefinition(definition);
|
|
119
|
-
const findings = enforcePolicy(
|
|
119
|
+
const findings = enforcePolicy([
|
|
120
|
+
...evaluatePolicy(config, policyRules, 'workflow', definition, { operation: 'create', workflowKind: definition.type }, policyOverride),
|
|
121
|
+
...await microfunctionStepFindings(platform, project, environment, definition, definition.type, policyOverride)
|
|
122
|
+
]);
|
|
120
123
|
const drift = unmappedMembers('CreateWorkflowCommand', definition);
|
|
121
124
|
const payload = await platform.tenant('POST', project, `/workflows/${applicationId}`, { environment, body: definition });
|
|
122
125
|
return attachFindings(withUnmappedMembers(withWriteDiff(withNormalization(payload, normalized), writeDiff(definition, payload)), drift), findings);
|
|
@@ -128,7 +131,10 @@ export function registerWorkflowTools(server, platform) {
|
|
|
128
131
|
annotations: updates
|
|
129
132
|
}, handle(config, async ({ project, environment, applicationId, id, definition, policyOverride }) => {
|
|
130
133
|
const normalized = normalizeWorkflowDefinition(definition);
|
|
131
|
-
const findings = enforcePolicy(
|
|
134
|
+
const findings = enforcePolicy([
|
|
135
|
+
...evaluatePolicy(config, policyRules, 'workflow', definition, { operation: 'update', workflowKind: definition.type }, policyOverride),
|
|
136
|
+
...await microfunctionStepFindings(platform, project, environment, definition, definition.type, policyOverride)
|
|
137
|
+
]);
|
|
132
138
|
const drift = unmappedMembers('UpdateWorkflowCommand', definition);
|
|
133
139
|
const payload = await platform.tenant('PUT', project, `/workflows/${applicationId}/${id}`, { environment, body: definition });
|
|
134
140
|
return attachFindings(withUnmappedMembers(withWriteDiff(withNormalization(payload, normalized), writeDiff(definition, payload)), drift), findings);
|
|
@@ -145,7 +151,10 @@ export function registerWorkflowTools(server, platform) {
|
|
|
145
151
|
const resolved = resolvePatchOperations(definition, operations);
|
|
146
152
|
const patched = fastJsonPatch.applyPatch(definition, resolved.operations, true, false).newDocument;
|
|
147
153
|
const normalized = normalizeWorkflowDefinition(patched);
|
|
148
|
-
const findings = enforcePolicy(
|
|
154
|
+
const findings = enforcePolicy([
|
|
155
|
+
...evaluatePolicy(config, policyRules, 'workflow', patched, { operation: 'update', workflowKind: patched.type }, policyOverride),
|
|
156
|
+
...await microfunctionStepFindings(platform, project, environment, patched, patched.type, policyOverride)
|
|
157
|
+
]);
|
|
149
158
|
const drift = introducedUnmappedMembers('UpdateWorkflowCommand', (current?.data ?? current), patched);
|
|
150
159
|
// Gövde değil FARK gönderilir: gateway normalizasyonları da op'a çevrilsin diye net diff
|
|
151
160
|
// okunan kayıt ile son hâl arasından hesaplanır (tam PUT yok, yalnız değişen alanlar yazılır).
|
|
@@ -176,12 +185,21 @@ export function registerWorkflowTools(server, platform) {
|
|
|
176
185
|
}, handle(config, ({ project, environment, id }) => platform.tenant('POST', project, `/workflows/clone/${id}`, { environment })));
|
|
177
186
|
server.registerTool('workflow_preview', {
|
|
178
187
|
title: 'Workflow önizle',
|
|
179
|
-
description: "Kaydedilmemiş workflow tanımını çalıştırır; kaydetmeden önce her akışı preview ile doğrulayın. definition = PreviewWorkflowCommand gövdesi. Test girdisi 'previewPayload' anahtarıyla verilir ('parameters'/'input' ÇALIŞMAZ), değişkenler 'previewVariables'. previewPayload STRING olmalıdır (JSON.stringify(...) ya da \"({ id: 1 })\") — nesne gönderilirse platform 'Unexpected end of input' ile 500 döner. Preview tek transaction'da koşar ve sonunda ROLLBACK edilir: table create/update step'leri kalıcı satır YAZMAZ (ölçüldü, İK koşusu W-1). Tek istisna code step içindeki dış HTTP çağrılarıdır — onlar geri alınamaz. Davranış kuralları: platform_guide('workflow'). " + constraintSummary('PreviewWorkflowCommand'),
|
|
188
|
+
description: "Kaydedilmemiş workflow tanımını çalıştırır; kaydetmeden önce her akışı preview ile doğrulayın. definition = PreviewWorkflowCommand gövdesi. Test girdisi 'previewPayload' anahtarıyla verilir ('parameters'/'input' ÇALIŞMAZ), değişkenler 'previewVariables'. previewPayload STRING olmalıdır (JSON.stringify(...) ya da \"({ id: 1 })\") — nesne gönderilirse platform 'Unexpected end of input' ile 500 döner. Akışın parameterDtoId'si YOKSA previewPayload verilmez (platform 400 döner; gateway alanı düşürüp gatewayNormalized ile bildirir). Preview tek transaction'da koşar ve sonunda ROLLBACK edilir: table create/update step'leri kalıcı satır YAZMAZ (ölçüldü, İK koşusu W-1). Tek istisna code step içindeki dış HTTP çağrılarıdır — onlar geri alınamaz. Davranış kuralları: platform_guide('workflow'). " + constraintSummary('PreviewWorkflowCommand'),
|
|
180
189
|
inputSchema: { project: projectParam, environment: environmentParam, applicationId: guid, definition: definitionParam, policyOverride: policyOverrideParam, language: languageParam },
|
|
181
190
|
annotations: writes
|
|
182
191
|
}, handle(config, async ({ project, environment, applicationId, definition, policyOverride, language }) => {
|
|
183
192
|
const normalized = normalizeWorkflowDefinition(definition);
|
|
184
|
-
|
|
193
|
+
// 33/S-6: parameterDtoId olmayan akışa previewPayload gönderilince platform 400 verir
|
|
194
|
+
// ("'previewPayload' requires a parameter DTO"). Girdisiz akışta alanın anlamı yok — düşürülür.
|
|
195
|
+
if (!definition.parameterDtoId && definition.previewPayload !== undefined) {
|
|
196
|
+
delete definition.previewPayload;
|
|
197
|
+
normalized.push("previewPayload düşürüldü — akışın parameterDtoId'si yok, platform 400 \"'previewPayload' requires a parameter DTO\" verir; girdi gerekiyorsa önce parameterDtoId + parameterBehavior tanımlayın");
|
|
198
|
+
}
|
|
199
|
+
const findings = enforcePolicy([
|
|
200
|
+
...evaluatePolicy(config, policyRules, 'workflow', definition, { operation: 'preview', workflowKind: definition.type }, policyOverride),
|
|
201
|
+
...await microfunctionStepFindings(platform, project, environment, definition, definition.type, policyOverride)
|
|
202
|
+
]);
|
|
185
203
|
const payload = await platform.tenant('POST', project, `/workflows/${applicationId}/preview`, { environment, body: definition, headers: languageHeader(language, normalized) });
|
|
186
204
|
return attachFindings(withNormalization(payload, normalized), findings);
|
|
187
205
|
}));
|