@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.
- package/.env.example +2 -2
- package/.env.production.example +9 -1
- package/README.md +14 -38
- package/dist/config.js +36 -3
- package/dist/knowledge/backendConstraints.js +982 -0
- package/dist/knowledge/componentCatalog.js +549 -92
- package/dist/knowledge/componentEvents.js +120 -25
- package/dist/knowledge/coverage.js +43 -0
- package/dist/knowledge/flowGuards.js +30 -0
- package/dist/knowledge/functionGuards.js +134 -0
- package/dist/knowledge/guideSections.js +49 -0
- package/dist/knowledge/hydrate.js +97 -0
- package/dist/knowledge/languageCodes.js +36 -0
- package/dist/knowledge/languages.js +105 -0
- package/dist/knowledge/layoutSummary.js +10 -0
- package/dist/knowledge/localization.js +51 -0
- package/dist/knowledge/normalize.js +500 -16
- package/dist/knowledge/outlines.js +167 -12
- package/dist/knowledge/policy.js +14 -5
- package/dist/knowledge/propTypes.js +80 -0
- package/dist/knowledge/rules.js +961 -44
- package/dist/knowledge/schemaGuard.js +199 -0
- package/dist/knowledge/styleResolve.js +63 -4
- package/dist/observability/trace.js +27 -3
- package/dist/platform/csv.js +123 -0
- package/dist/platform/errors.js +107 -6
- package/dist/platform/http.js +34 -4
- package/dist/platform/patch.js +27 -0
- package/dist/platform/refs.js +104 -0
- package/dist/platform/responses.js +25 -6
- package/dist/platform/screenshot.js +544 -16
- package/dist/platform/secrets.js +40 -0
- package/dist/platform/tokenVault.js +89 -0
- package/dist/platform/writeDiff.js +281 -0
- package/dist/resources/examples/flow-call-function.json +66 -0
- package/dist/resources/examples/function-total.json +70 -0
- package/dist/resources/examples/layout-app-shell.json +1 -1
- package/dist/resources/examples/microfunction-node-sum.json +56 -0
- package/dist/resources/examples/microservice-table-list.json +2 -4
- package/dist/resources/examples/page-dynamic-form.json +3 -3
- package/dist/resources/examples/page-flex-sections.json +99 -0
- package/dist/resources/examples/page-sign-in.json +4 -3
- package/dist/resources/guides/component.md +81 -8
- package/dist/resources/guides/entity.md +712 -20
- package/dist/resources/guides/expressions.md +47 -2
- package/dist/resources/guides/external-component.md +9 -0
- package/dist/resources/guides/functions.md +262 -0
- package/dist/resources/guides/getting-started.md +82 -7
- package/dist/resources/guides/layout.md +53 -10
- package/dist/resources/guides/merge.md +231 -0
- package/dist/resources/guides/microfunction.md +166 -0
- package/dist/resources/guides/microservice.md +287 -15
- package/dist/resources/guides/middleware.md +89 -1
- package/dist/resources/guides/modify-existing.md +23 -0
- package/dist/resources/guides/page.md +402 -49
- package/dist/resources/guides/translation.md +150 -8
- package/dist/resources/guides/view.md +16 -1
- package/dist/resources/guides/workflow.md +199 -28
- package/dist/resources/knowledge.js +5 -0
- package/dist/resources/rules.js +27 -0
- package/dist/resources/schema-fingerprint.json +8 -0
- package/dist/resources/schemas/AnalyzeImportQuery.json +51 -0
- package/dist/resources/schemas/ApplyMergePlanCommand.json +19 -0
- package/dist/resources/schemas/CreateBulkRowCommand.json +36 -0
- package/dist/resources/schemas/CreateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +19 -8
- package/dist/resources/schemas/CreateEntityCommand.json +52 -12
- 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 +145 -3
- package/dist/resources/schemas/CreateQueryCommand.json +56 -3
- package/dist/resources/schemas/CreateRowCommand.json +38 -0
- package/dist/resources/schemas/CreateViewCommand.json +13 -10
- package/dist/resources/schemas/CreateWorkflowCommand.json +106 -14
- 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 +6 -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 +135 -1
- package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
- package/dist/resources/schemas/PreviewWorkflowCommand.json +45 -9
- 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 +34 -3
- package/dist/resources/schemas/UpdateBulkRowCommand.json +36 -0
- package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/UpdateEntityCommand.json +52 -11
- 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 +147 -4
- package/dist/resources/schemas/UpdateQueryCommand.json +13 -3
- package/dist/resources/schemas/UpdateRowCommand.json +40 -0
- package/dist/resources/schemas/UpdateViewCommand.json +15 -10
- package/dist/resources/schemas/UpdateWorkflowCommand.json +106 -14
- package/dist/server.js +17 -1
- package/dist/tools/access.js +3 -3
- package/dist/tools/activities.js +40 -0
- package/dist/tools/applications.js +1 -1
- package/dist/tools/automations.js +76 -34
- package/dist/tools/common.js +55 -0
- package/dist/tools/components.js +16 -7
- package/dist/tools/datasources.js +83 -21
- package/dist/tools/dependencies.js +1 -1
- package/dist/tools/deployments.js +34 -2
- package/dist/tools/dtos.js +25 -12
- package/dist/tools/entities.js +113 -28
- package/dist/tools/environments.js +52 -0
- package/dist/tools/externalComponents.js +9 -3
- package/dist/tools/files.js +57 -42
- package/dist/tools/flows.js +205 -13
- package/dist/tools/folders.js +7 -2
- package/dist/tools/functions.js +331 -0
- package/dist/tools/globalFunctions.js +63 -0
- package/dist/tools/importSchemas.js +79 -0
- package/dist/tools/imports.js +241 -0
- package/dist/tools/layouts.js +103 -29
- package/dist/tools/merges.js +113 -0
- package/dist/tools/microfunctions.js +155 -0
- package/dist/tools/microservices.js +270 -40
- package/dist/tools/organizations.js +5 -6
- package/dist/tools/pages.js +421 -104
- package/dist/tools/platformInfo.js +155 -11
- package/dist/tools/projects.js +183 -33
- package/dist/tools/queries.js +32 -10
- package/dist/tools/revisions.js +94 -0
- package/dist/tools/rows.js +484 -50
- package/dist/tools/screenWrite.js +68 -0
- package/dist/tools/screens.js +28 -0
- package/dist/tools/templates.js +32 -5
- package/dist/tools/translations.js +24 -8
- package/dist/tools/views.js +20 -10
- package/dist/tools/workflows.js +56 -21
- package/package.json +7 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { PlatformApiError } from './errors.js';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
/**
|
|
4
|
+
* `page_screenshot`/`page_runtime_get` açıklaması "jetonu uygulamanın kendi giriş ucundan alın
|
|
5
|
+
* (`microservice_execute`)" diyor, ama gizli alan maskesi `access_token`'ı `***` yapıyor ve o yol
|
|
6
|
+
* kapanıyor (ölçüldü 2026-09-04, benchmark 4. koşu — jeton geçici olarak maskelenmeyen bir alana
|
|
7
|
+
* taşınıp geri alınarak çalışıldı, yani uygulamanın yanıt sözleşmesi bozuldu).
|
|
8
|
+
*
|
|
9
|
+
* Kasa bu çelişkiyi jetonu bağlama HİÇ sokmadan çözer: maskelenen JWT gateway sürecinde tutulur,
|
|
10
|
+
* yanıta yalnız kısa ömürlü bir referans (`tok_…`) döner ve kimlik isteyen tool'lar referansı
|
|
11
|
+
* çözer. Jeton ne yanıtta ne günlükte açık görünür.
|
|
12
|
+
*/
|
|
13
|
+
const ttlMs = 15 * 60 * 1000;
|
|
14
|
+
const maxEntries = 64;
|
|
15
|
+
const vault = new Map();
|
|
16
|
+
export const tokenRefPattern = /^tok_[0-9a-f]{16}$/;
|
|
17
|
+
/** Üç base64url parçalı JWT — parola ya da API anahtarı bu şekle uymaz. */
|
|
18
|
+
export function looksLikeJwt(value) {
|
|
19
|
+
return typeof value === 'string' && /^[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}$/.test(value);
|
|
20
|
+
}
|
|
21
|
+
function sweep(now) {
|
|
22
|
+
for (const [ref, entry] of vault) {
|
|
23
|
+
if (entry.expires <= now)
|
|
24
|
+
vault.delete(ref);
|
|
25
|
+
}
|
|
26
|
+
while (vault.size >= maxEntries) {
|
|
27
|
+
const oldest = vault.keys().next();
|
|
28
|
+
if (oldest.done)
|
|
29
|
+
break;
|
|
30
|
+
vault.delete(oldest.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function rememberToken(token) {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
sweep(now);
|
|
36
|
+
for (const [ref, entry] of vault) {
|
|
37
|
+
if (entry.token === token) {
|
|
38
|
+
entry.expires = now + ttlMs;
|
|
39
|
+
return ref;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const ref = `tok_${randomBytes(8).toString('hex')}`;
|
|
43
|
+
vault.set(ref, { token, expires: now + ttlMs });
|
|
44
|
+
return ref;
|
|
45
|
+
}
|
|
46
|
+
export function resolveToken(reference) {
|
|
47
|
+
const entry = vault.get(reference);
|
|
48
|
+
if (!entry)
|
|
49
|
+
return undefined;
|
|
50
|
+
if (entry.expires <= Date.now()) {
|
|
51
|
+
vault.delete(reference);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return entry.token;
|
|
55
|
+
}
|
|
56
|
+
export function clearTokenVault() {
|
|
57
|
+
vault.clear();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `tok_…` referansı kasadan çözülür, ham JWT olduğu gibi geçer. Süresi dolmuş referans sessizce
|
|
61
|
+
* başlıksız isteğe DÜŞMEZ — o durumda çağrı anonim gider ve "korumalı uç 401 döndü" diye yanlış
|
|
62
|
+
* teşhis üretir. Sayfa ve uç araçları aynı sözleşmeyi paylaşsın diye burada yaşar (G-20).
|
|
63
|
+
*/
|
|
64
|
+
export function resolveIdentityToken(value) {
|
|
65
|
+
if (value === undefined || !tokenRefPattern.test(value))
|
|
66
|
+
return value;
|
|
67
|
+
const token = resolveToken(value);
|
|
68
|
+
if (token === undefined) {
|
|
69
|
+
throw new PlatformApiError(`'${value}' referansı geçersiz ya da süresi dolmuş (15 dk). Uygulamanın giriş ucunu microservice_execute ile yeniden çağırıp yanıttaki tokenRefs değerini kullanın.`, 400);
|
|
70
|
+
}
|
|
71
|
+
return token;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Kimlik iki kanalda birden taşınır: `Authorization` başlığı VE `NG_AccessToken` çerezi. Middleware
|
|
75
|
+
* jetonu çerezden okuyor (`cookie.CORE_Token || cookie.NG_AccessToken`), gateway ise yalnız başlık
|
|
76
|
+
* gönderiyordu; bu yüzden uca middleware bağlandığı anda preview/debug/execute 401'e düşüyordu
|
|
77
|
+
* (ölçüldü 2026-09-05, Corecruiter koşusu `S-21`). Uygulamanın kendi çerez adı için `cookies`.
|
|
78
|
+
*/
|
|
79
|
+
export function identityHeaders(asUser, cookies, callerHeaders) {
|
|
80
|
+
const token = resolveIdentityToken(asUser);
|
|
81
|
+
const cookieText = Object.entries(cookies ?? {}).map(([name, value]) => `${name}=${value}`).join('; ');
|
|
82
|
+
if (token === undefined && cookieText === '')
|
|
83
|
+
return callerHeaders;
|
|
84
|
+
return {
|
|
85
|
+
...(token === undefined ? {} : { authorization: `Bearer ${token}` }),
|
|
86
|
+
...callerHeaders,
|
|
87
|
+
...(cookieText === '' ? {} : { cookie: cookieText })
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { logStep } from '../observability/trace.js';
|
|
2
|
+
const maxDrops = 20;
|
|
3
|
+
const maxDepth = 12;
|
|
4
|
+
const missing = Symbol('missing');
|
|
5
|
+
/**
|
|
6
|
+
* Platformun kendi doldurduğu alanlar. Yalnız gönderilen tarafta da varsalar (patch yolunda gövde
|
|
7
|
+
* okunup geri yazılır) fark üretirlerdi ve bu fark hep gürültüdür: `audit.updatedAt` her yazmada
|
|
8
|
+
* değişir. 09-05 koşusunda `microservice_patch` yanıtındaki iki satırdan biri buydu.
|
|
9
|
+
*/
|
|
10
|
+
const serverManagedKeys = new Set([
|
|
11
|
+
'audit', 'createdAt', 'updatedAt', 'createdUserId', 'createdUserName', 'updatedUserId', 'updatedUserName'
|
|
12
|
+
]);
|
|
13
|
+
/**
|
|
14
|
+
* Platformun KENDİ ürettiği kimlik. Gönderilen id kaydedilenden farklıysa bu bir kayıp değildir:
|
|
15
|
+
* öğe kaydedilmiştir, yalnız kimliğini platform atamıştır (`args.iteratees[].id`,
|
|
16
|
+
* `args.parameters[].id` — 09-05 koşusunda `flow_create` 19, `flow_patch` 21 sahte satır üretti ve
|
|
17
|
+
* ajan hepsini elle ayıkladı). Kaydedilen tarafta id HİÇ yoksa bu muafiyet geçmez; o gerçek bir
|
|
18
|
+
* kayıptır. Adım ve dal id'leri zaten gönderildiği gibi kaydedilir, dolayısıyla fark üretmezler.
|
|
19
|
+
*/
|
|
20
|
+
const platformAssignedKeys = new Set(['id']);
|
|
21
|
+
/**
|
|
22
|
+
* Dizi öğelerini indeksle değil KİMLİKLE eşleştirmek için aranan alanlar, öncelik sırasıyla.
|
|
23
|
+
* Platform diziyi kendi sırasında geri döndürüyor (`flow_get` adımları kayıt sırasında vermiyor);
|
|
24
|
+
* indeksle karşılaştırma o durumda her öğeyi "değişmiş" gösterir ve diff okunamaz hâle gelir.
|
|
25
|
+
*
|
|
26
|
+
* `id` listenin SONUNDADIR: platform onu yeniden atıyor (`platformAssignedKeys`), dolayısıyla
|
|
27
|
+
* kimlik olarak en zayıf aday odur. `fieldId` başta çünkü `sourceParameters.selectFields`
|
|
28
|
+
* elemanlarının tek kararlı anahtarı odur — 2026-09-06 koşusunda o dizi başka sırada geri döndü
|
|
29
|
+
* ve indeks tabanlı karşılaştırma altı sahte satır üretti (`S-1`).
|
|
30
|
+
*/
|
|
31
|
+
const identityKeys = ['fieldId', 'stepId', 'elementId', 'slug', 'name', 'key', 'id'];
|
|
32
|
+
/**
|
|
33
|
+
* Platformun KABUL EDİP saklamadığı kolaylık alanları. Yanıtta ya hiç yoklar ya da `null`
|
|
34
|
+
* dönerler; ikisi de kayıp değildir, alan zaten türetilebilir. Her satır hangi ölçümle eklendi:
|
|
35
|
+
*
|
|
36
|
+
* - `fieldSlug` — 2026-09-06 koşusu, üç `workflow_create` yanıtında 24 satır
|
|
37
|
+
* (`gönderilen "id" ↔ kaydedilen null`). Platform slug'ı `fieldId`'den çözüyor, saklamıyor.
|
|
38
|
+
*
|
|
39
|
+
* Muafiyet YALNIZ saklanmama durumunu kapsar: alan dolu ama BAŞKA değerle döndüyse fark yine
|
|
40
|
+
* bildirilir — orası gerçek bir yeniden yazmadır.
|
|
41
|
+
*/
|
|
42
|
+
export const unstoredKeys = new Set(['fieldSlug']);
|
|
43
|
+
/**
|
|
44
|
+
* Platformun kanonik adıyla sakladığı eşdeğer değerler. Fark gerçektir ama KAYIP değildir:
|
|
45
|
+
* `silentlyDropped` yerine `gatewayNormalized` ile bildirilir. Her satır hangi ölçümle eklendi:
|
|
46
|
+
*
|
|
47
|
+
* - `type: string ↔ text` — 2026-09-06 koşusu, `queryParameters.type` (`S-1`).
|
|
48
|
+
*/
|
|
49
|
+
const canonicalValues = {
|
|
50
|
+
type: [['string', 'text']]
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Yolun son alan adı; `tags[0]` ya da `fields[fieldId=x]` gibi indeks/anahtar ekini atar.
|
|
54
|
+
*/
|
|
55
|
+
function leafKey(path) {
|
|
56
|
+
return (path.split('.').pop() ?? '').replace(/\[[^\]]*\]$/, '');
|
|
57
|
+
}
|
|
58
|
+
function render(value) {
|
|
59
|
+
const text = JSON.stringify(value) ?? String(value);
|
|
60
|
+
return text.length > 160 ? `${text.slice(0, 157)}…` : text;
|
|
61
|
+
}
|
|
62
|
+
function identityKeyFor(entries) {
|
|
63
|
+
const objects = entries.filter((entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry));
|
|
64
|
+
if (objects.length === 0 || objects.length !== entries.length)
|
|
65
|
+
return undefined;
|
|
66
|
+
return identityKeys.find(key => {
|
|
67
|
+
const values = objects
|
|
68
|
+
.map(entry => entry[key])
|
|
69
|
+
.filter(value => typeof value === 'string' || typeof value === 'number')
|
|
70
|
+
.map(String);
|
|
71
|
+
return values.length === objects.length && new Set(values).size === objects.length;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function lookup(saved, key) {
|
|
75
|
+
if (typeof key === 'number')
|
|
76
|
+
return Array.isArray(saved) && key < saved.length ? saved[key] : missing;
|
|
77
|
+
if (!saved || typeof saved !== 'object' || Array.isArray(saved))
|
|
78
|
+
return missing;
|
|
79
|
+
const record = saved;
|
|
80
|
+
return Object.hasOwn(record, key) ? record[key] : missing;
|
|
81
|
+
}
|
|
82
|
+
function compare(sent, saved, path, sink, depth) {
|
|
83
|
+
if (sink.dropped.length >= maxDrops || depth > maxDepth)
|
|
84
|
+
return;
|
|
85
|
+
if (Array.isArray(sent)) {
|
|
86
|
+
if (saved === missing) {
|
|
87
|
+
sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const key = Array.isArray(saved) ? identityKeyFor(sent) : undefined;
|
|
91
|
+
const savedByIdentity = key
|
|
92
|
+
? new Map(saved
|
|
93
|
+
.filter((entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry))
|
|
94
|
+
.map(entry => [String(entry[key]), entry]))
|
|
95
|
+
: undefined;
|
|
96
|
+
// Hiçbir kimlik örtüşmüyorsa anahtar bu dizide anlamlı değildir (öğe yeniden adlandırılmış ya
|
|
97
|
+
// da dizi baştan yazılmış olabilir); indeksle karşılaştırmak orada daha okunur bir fark üretir.
|
|
98
|
+
if (key && savedByIdentity && sent.some(entry => savedByIdentity.has(String(entry[key])))) {
|
|
99
|
+
for (const entry of sent) {
|
|
100
|
+
const identity = String(entry[key]);
|
|
101
|
+
compare(entry, savedByIdentity.has(identity) ? savedByIdentity.get(identity) : missing, `${path}[${key}=${identity}]`, sink, depth + 1);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const before = sink.dropped.length;
|
|
106
|
+
sent.forEach((entry, index) => compare(entry, lookup(saved, index), `${path}[${index}]`, sink, depth + 1));
|
|
107
|
+
// Sıra tabanlı karşılaştırmada "değişmiş" görünen öğe aslında yer değiştirmiş olabilir. Tek
|
|
108
|
+
// elemanlı dizide sıra farkı imkânsız olduğu için not düşülmez (2026-09-06 · S-1).
|
|
109
|
+
if (Array.isArray(saved) && sent.length > 1 && saved.length > 1 && sink.dropped.length > before) {
|
|
110
|
+
sink.dropped.splice(before, 0, `${path}: (sıra tabanlı karşılaştırma — anahtar yok) aşağıdaki farklar platform diziyi başka sırada sakladıysa yanıltıcıdır`);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (sent && typeof sent === 'object') {
|
|
115
|
+
if (saved === missing) {
|
|
116
|
+
sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const [key, entry] of Object.entries(sent)) {
|
|
120
|
+
if (serverManagedKeys.has(key))
|
|
121
|
+
continue;
|
|
122
|
+
compare(entry, lookup(saved, key), path === '' ? key : `${path}.${key}`, sink, depth + 1);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (sent === null || sent === undefined)
|
|
127
|
+
return;
|
|
128
|
+
const field = leafKey(path);
|
|
129
|
+
if (unstoredKeys.has(field) && (saved === missing || saved === null))
|
|
130
|
+
return;
|
|
131
|
+
if (saved === missing) {
|
|
132
|
+
sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen yok`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (sent === saved)
|
|
136
|
+
return;
|
|
137
|
+
if (platformAssignedKeys.has(field))
|
|
138
|
+
return;
|
|
139
|
+
if (canonicalValues[field]?.some(group => group.includes(String(sent)) && group.includes(String(saved)))) {
|
|
140
|
+
sink.normalized.push(`${path}: gönderilen ${render(sent)} platformun kanonik adıyla ${render(saved)} olarak saklandı — eşdeğer, kayıp değil`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
sink.dropped.push(`${path}: gönderilen ${render(sent)} ↔ kaydedilen ${render(saved)}`);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Sol taraf platforma GİDEN gövdedir (gateway normalizasyonu uygulandıktan sonrası), sağ taraf
|
|
147
|
+
* platformun döndürdüğü kayıt. Yalnız sol tarafta var olan yollar dolaşılır; sunucunun eklediği
|
|
148
|
+
* id/createdAt gibi alanlar fark üretmez. Kök hiç örtüşmüyorsa (yanıt bir kabul/kuyruk gövdesiyse)
|
|
149
|
+
* karşılaştırma yapılmaz — "her şey düştü" gürültüsü yanlış alarmdır.
|
|
150
|
+
*
|
|
151
|
+
* İki liste döner: `dropped` gerçekten kaybolan alanlar, `normalized` platformun kanonik adıyla
|
|
152
|
+
* sakladığı eşdeğerler. İkincisi `gatewayNormalized`a gider, uyarıya değil.
|
|
153
|
+
*/
|
|
154
|
+
export function writeDiff(sent, saved, aliases = {}) {
|
|
155
|
+
const sink = { dropped: [], normalized: [] };
|
|
156
|
+
if (!sent || typeof sent !== 'object')
|
|
157
|
+
return sink;
|
|
158
|
+
const wrapper = saved && typeof saved === 'object' && !Array.isArray(saved)
|
|
159
|
+
? saved
|
|
160
|
+
: undefined;
|
|
161
|
+
const inner = wrapper?.data;
|
|
162
|
+
const root = inner && typeof inner === 'object' ? inner : saved;
|
|
163
|
+
// Bazı uçlar gönderilen alanı BAŞKA ADLA geri döner (query: parameters → parameterScheme).
|
|
164
|
+
// Alias verilmezse alan "sessizce düştü" diye raporlanır; bu yanlış alarm 09-04 koşusunda
|
|
165
|
+
// ajanı parametreleri kaldırmaya yöneltti ve kayıt 500 ile reddedildi (B-15).
|
|
166
|
+
const comparable = Object.fromEntries(Object.entries(sent)
|
|
167
|
+
.filter(([key]) => aliases[key] === undefined || lookup(root, aliases[key]) === missing));
|
|
168
|
+
const keys = Object.keys(comparable);
|
|
169
|
+
const shared = keys.filter(key => lookup(root, key) !== missing);
|
|
170
|
+
if (keys.length > 0 && shared.length === 0)
|
|
171
|
+
return sink;
|
|
172
|
+
compare(comparable, root, '', sink, 0);
|
|
173
|
+
if (sink.dropped.length >= maxDrops) {
|
|
174
|
+
sink.dropped.push(`… ilk ${maxDrops} fark listelendi, daha fazla fark var — gövdeyi *_get ile okuyup karşılaştırın.`);
|
|
175
|
+
}
|
|
176
|
+
return sink;
|
|
177
|
+
}
|
|
178
|
+
export function silentlyDropped(sent, saved, aliases = {}) {
|
|
179
|
+
return writeDiff(sent, saved, aliases).dropped;
|
|
180
|
+
}
|
|
181
|
+
export function withWriteDiff(payload, diff) {
|
|
182
|
+
const drops = Array.isArray(diff) ? diff : diff.dropped;
|
|
183
|
+
const normalized = Array.isArray(diff) ? [] : diff.normalized;
|
|
184
|
+
if (drops.length > 0) {
|
|
185
|
+
logStep('sessiz düşme ⚠', `${drops.length} alan gönderildi ama kayda geçmedi — platform hata vermeden yok saydı, yanıta silentlyDropped olarak eklenir`, drops);
|
|
186
|
+
}
|
|
187
|
+
if (normalized.length > 0) {
|
|
188
|
+
logStep('düzeltme', `platform ${normalized.length} alanı kanonik adıyla sakladı — eşdeğer, yanıta gatewayNormalized olarak eklenir`, normalized);
|
|
189
|
+
}
|
|
190
|
+
if (!payload || typeof payload !== 'object')
|
|
191
|
+
return payload;
|
|
192
|
+
if (drops.length === 0 && normalized.length === 0)
|
|
193
|
+
return payload;
|
|
194
|
+
const existing = payload.gatewayNormalized;
|
|
195
|
+
return {
|
|
196
|
+
...payload,
|
|
197
|
+
...(drops.length > 0 ? { silentlyDropped: drops } : {}),
|
|
198
|
+
...(normalized.length > 0
|
|
199
|
+
? { gatewayNormalized: [...(Array.isArray(existing) ? existing : []), ...normalized] }
|
|
200
|
+
: {})
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Platform şemada her değeri bir Prop nesnesine sarar (`{ type, data, ref? }`) ve iç içe sözlükleri
|
|
205
|
+
* de sarar: `style: { padding: '40px' }` kaydedilince `style.data.padding = { type: 'static',
|
|
206
|
+
* data: '40px' }` olur. Micro-op ise HAM değer gönderir. Karşılaştırmadan önce sarmalı açmazsak
|
|
207
|
+
* doğru kaydedilmiş her prop "değişmiş" görünür.
|
|
208
|
+
*/
|
|
209
|
+
export function unwrapProps(value) {
|
|
210
|
+
if (Array.isArray(value))
|
|
211
|
+
return value.map(unwrapProps);
|
|
212
|
+
if (!value || typeof value !== 'object')
|
|
213
|
+
return value;
|
|
214
|
+
const record = value;
|
|
215
|
+
const isProp = typeof record.type === 'string' && (Object.hasOwn(record, 'data') || Object.hasOwn(record, 'ref'));
|
|
216
|
+
if (isProp) {
|
|
217
|
+
// `code` ve `translation` prop'larında taşıyıcı alan `ref`'tir; `data` yalnız tasarım zamanı
|
|
218
|
+
// yedeğidir (platform_guide('translation')).
|
|
219
|
+
const carrier = record.type === 'code' || record.type === 'translation'
|
|
220
|
+
? (Object.hasOwn(record, 'ref') ? record.ref : record.data)
|
|
221
|
+
: (Object.hasOwn(record, 'data') ? record.data : record.ref);
|
|
222
|
+
return unwrapProps(carrier);
|
|
223
|
+
}
|
|
224
|
+
return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, unwrapProps(entry)]));
|
|
225
|
+
}
|
|
226
|
+
function findByElementId(nodes, elementId) {
|
|
227
|
+
if (!Array.isArray(nodes))
|
|
228
|
+
return undefined;
|
|
229
|
+
for (const node of nodes) {
|
|
230
|
+
if (!node || typeof node !== 'object')
|
|
231
|
+
continue;
|
|
232
|
+
const record = node;
|
|
233
|
+
if (record.elementId === elementId)
|
|
234
|
+
return record;
|
|
235
|
+
const found = findByElementId(record.children, elementId);
|
|
236
|
+
if (found)
|
|
237
|
+
return found;
|
|
238
|
+
}
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Micro-op'un YAZMAK İSTEDİĞİ ile şemada KAYITLI olanı karşılaştırır. Tam gövde yazmalarında
|
|
243
|
+
* `silentlyDropped` gönderilen gövdeyi dönen kayıtla karşılaştırabiliyor; micro-op yolunda dönen
|
|
244
|
+
* yanıt yalnız `appliedPatch` olduğu için karşılaştırma yamadan SONRA okunan şemaya dayanır.
|
|
245
|
+
*/
|
|
246
|
+
export function schemaOpDrops(schema, ops) {
|
|
247
|
+
if (!Array.isArray(ops))
|
|
248
|
+
return [];
|
|
249
|
+
const drops = [];
|
|
250
|
+
for (const op of ops) {
|
|
251
|
+
if (op.op === 'add_component') {
|
|
252
|
+
const component = (op.component ?? {});
|
|
253
|
+
const elementId = String(component.elementId ?? '');
|
|
254
|
+
const saved = elementId ? findByElementId(schema, elementId) : undefined;
|
|
255
|
+
if (!elementId)
|
|
256
|
+
continue;
|
|
257
|
+
if (!saved) {
|
|
258
|
+
drops.push(`add_component '${elementId}': komponent şemada bulunamadı — yama 200 döndü ama düğüm kaydedilmemiş.`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
for (const entry of silentlyDropped(unwrapProps(component), unwrapProps(saved))) {
|
|
262
|
+
drops.push(`add_component '${elementId}' → ${entry}`);
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (op.op === 'replace_prop' && typeof op.target === 'string' && typeof op.prop === 'string') {
|
|
267
|
+
const saved = findByElementId(schema, op.target);
|
|
268
|
+
if (!saved)
|
|
269
|
+
continue;
|
|
270
|
+
const props = (saved.props ?? {});
|
|
271
|
+
if (!Object.hasOwn(props, op.prop)) {
|
|
272
|
+
drops.push(`replace_prop '${op.target}.${op.prop}': prop şemada yok — değer kaydedilmemiş.`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
for (const entry of silentlyDropped({ [op.prop]: unwrapProps(op.value) }, { [op.prop]: unwrapProps(props[op.prop]) })) {
|
|
276
|
+
drops.push(`replace_prop '${op.target}' → ${entry}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return drops.slice(0, maxDrops);
|
|
281
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"synthetic": true,
|
|
4
|
+
"note": "Bir butonun click olayına bağlanan akış: 'function' adımı kullanıcı fonksiyonunu çağırır (fiyat komponent prop'undan, adet sabit), sonraki setter sonucu Steps.<adımAdı>.data'dan okuyup metne yazar. Canlı kanıt: 2026-09-08/09 fonksiyon-demo sayfası (kabul senaryosu 2 → 30). Şekil backend StepManagementSchema + ng-apps-flow tasks/function/function.ts okunarak kuruldu.",
|
|
5
|
+
"harvestedFrom": "kod + canlı doğrulama: zeyrek-frontend packages/ng-apps-flow/src/tasks/function, zeyrek-backend Contexts/Studio/Flows/StepArgument (UiFunctionId/UiFunctionName) (2026-09-09)"
|
|
6
|
+
},
|
|
7
|
+
"_rules": {
|
|
8
|
+
"functionId": "args.functionId ZORUNLU ve uygulamada var olmalı — backend 422 UiFunctionNotFound, gateway FLOW-024 (block). Id'yi function_list'ten alın. Runtime fonksiyonu ÖNCE id ile çözer (K9); functionName gösterim ve yedektir.",
|
|
9
|
+
"parameters": "parameters[].name bildirimdeki parametre adıyla eşleşir; value şekli microservice adımıyla aynı (static · property · storage · state · variable · code · page · item). Eksik zorunlu parametre adımı 'Functions.<ad>: \"<param>\" is required' ile düşürür; tanınmayan ad sessizce düşer — FLOW-024 ikisini de uyarır.",
|
|
10
|
+
"output": "Çıktı Steps.<adım ADI>.data (stepId değil — machine.setStorage(task.name, …)); hata Steps.<ad>.error.message (nesne). Setter'ın storage okuması mapping.source: 'hesapla.data' ile yapılır (setter.ts:161 mapping.source doluysa value.ref okunmaz).",
|
|
11
|
+
"branchName": "Dal adı 'always' (motor anahtarı, FLOW-004). Son adımın dalı nextStepId: null ile biter; 'end' adımı koyma (FLOW-023).",
|
|
12
|
+
"binding": "Akışı olaya bağlamak ayrı adımdır: page_schema_patch set_event { target: 'btn_hesapla', event: 'click', eventValue: '<flow id>' }.",
|
|
13
|
+
"async": "isAsync=true fonksiyon bu adımdan güvenle çağrılır (Promise beklenir); {{ }} bağlamasında ise '[object Promise]' olur."
|
|
14
|
+
},
|
|
15
|
+
"flowCreateArgs": {
|
|
16
|
+
"host": "page",
|
|
17
|
+
"hostId": "<page id>",
|
|
18
|
+
"name": "btn_hesapla - click",
|
|
19
|
+
"slug": "btn-hesapla-click",
|
|
20
|
+
"steps": [
|
|
21
|
+
{
|
|
22
|
+
"stepId": "start",
|
|
23
|
+
"name": "start",
|
|
24
|
+
"type": "start",
|
|
25
|
+
"kind": "normal",
|
|
26
|
+
"args": {},
|
|
27
|
+
"branches": [ { "name": "always", "nextStepId": "hesapla" } ]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"stepId": "hesapla",
|
|
31
|
+
"name": "hesapla",
|
|
32
|
+
"type": "function",
|
|
33
|
+
"kind": "normal",
|
|
34
|
+
"args": {
|
|
35
|
+
"functionId": "<function_list id — toplamHesapla>",
|
|
36
|
+
"functionName": "toplamHesapla",
|
|
37
|
+
"parameters": [
|
|
38
|
+
{ "name": "fiyat", "value": { "type": "property", "ref": { "source": "component", "element": "num_fiyat", "property": "value" } } },
|
|
39
|
+
{ "name": "adet", "value": { "type": "static", "ref": 3 } }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"branches": [ { "name": "always", "nextStepId": "yaz" } ]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"stepId": "yaz",
|
|
46
|
+
"name": "yaz",
|
|
47
|
+
"type": "setter",
|
|
48
|
+
"kind": "normal",
|
|
49
|
+
"args": {
|
|
50
|
+
"iteratees": [
|
|
51
|
+
{
|
|
52
|
+
"type": "change",
|
|
53
|
+
"element": "txt_sonuc",
|
|
54
|
+
"property": "content",
|
|
55
|
+
"source": "component",
|
|
56
|
+
"mapping": { "source": "hesapla.data", "mappings": {} },
|
|
57
|
+
"value": { "type": "storage", "ref": "" }
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"branches": [ { "name": "always", "nextStepId": null } ]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"bindEvent": { "op": "set_event", "target": "btn_hesapla", "event": "click", "eventValue": "<flow id>" }
|
|
66
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"synthetic": true,
|
|
4
|
+
"note": "toplamHesapla(fiyat, adet = 1) = fiyat * adet — aynı gövde İKİ KAYNAKTA: source:'studio' (uygulama kapsamlı, tarayıcı: {{ }} bağlaması ve UI flow 'code'/'function' adımı) ve source:'workflow' (sunucu: workflow/middleware/automation ifadesinde çıplak ad). Kod ve parametre biçimi aynıdır, fark nerede çalıştığıdır. Canlı kanıt: 2026-09-10 fonksiyon-demo sayfası ({{ Functions.toplamHesapla(10, 3) }} → 30) ve workflow_preview code adımı (return toplamHesapla(10, 3) → 30).",
|
|
5
|
+
"harvestedFrom": "kod + canlı doğrulama: zeyrek-backend Contexts/Studio/GlobalFunctions, zeyrek-frontend packages/ng-apps-flow/src/functions (2026-09-10)"
|
|
6
|
+
},
|
|
7
|
+
"_rules": {
|
|
8
|
+
"body": "Gövde DÜZ deyimlerdir, iki kaynakta aynı: 'return fiyat * adet;'. function (…) { } sarmalı ya da arrow yazılmaz. return serbesttir; dönüşsüz gövde undefined döner. Platform kaydetmeden sözdizimini ayrıştırır (422 GlobalFunctionBodyInvalid).",
|
|
9
|
+
"parameters": "Bildirim SIRALIDIR ve çağrı KONUMSALDIR (K5): toplamHesapla(10, 3) → fiyat=10, adet=3. defaultValue argüman undefined kalınca uygulanır; required kontrolü ondan SONRA çalışır ve 'toplamHesapla: \"fiyat\" is required' (studio'da 'Functions.toplamHesapla: …') fırlatır. type yalnız Studio imzası içindir, çalışma zamanı tip denetlemez.",
|
|
10
|
+
"scope": "Yalnız source:'studio'. application: her sayfada (tarayıcı). page: yalnız o sayfanın tarayıcı bağlamında, pageId zorunlu. Ad uygulama içinde kapsamlar arası benzersizdir (K3); workflow adı tenant içinde benzersizdir. Aynı ad iki kaynakta yan yana olabilir.",
|
|
11
|
+
"isAsync": "Yalnız studio'da anlamlı; false bırakın: true olsaydı {{ }} bağlaması '[object Promise]' çizerdi. await gerekmeyen gövdede isAsync:true FN-004 uyarısı alır. workflow kaynağında true FN-004 block (motor senkron).",
|
|
12
|
+
"naming": "Ad JS tanımlayıcısı; ayrılmış sözcük olamaz. studio'da 'Functions' yasak (FN-001, block); workflow'da motor global adları (system, input, Now, crypto…) yasak (FN-001, block). Uygulama fonksiyonunda sayfaya özgü ad (komponent adı, state.page), workflow fonksiyonunda adım adı kullanmayın — çağrıldığı yerde yoksa 'X is not defined'.",
|
|
13
|
+
"source": "workflow: SUNUCU, tenant geneli, applicationId/scope/pageId VERİLMEZ, isAsync olamaz, çıplak adla çağrılır. studio: TARAYICI, applicationId + scope zorunlu (application: her sayfada; page: yalnız o sayfa, pageId zorunlu), Functions.<ad>() ile çağrılır. Kaynak sonradan değişmez; sunucu ifadesinde Functions.<ad>( → WF-030 block."
|
|
14
|
+
},
|
|
15
|
+
"functionCreateArgs": {
|
|
16
|
+
"source": "studio",
|
|
17
|
+
"applicationId": "<application id>",
|
|
18
|
+
"scope": "application",
|
|
19
|
+
"name": "toplamHesapla",
|
|
20
|
+
"description": "fiyat × adet; adet verilmezse 1 sayılır",
|
|
21
|
+
"isAsync": false,
|
|
22
|
+
"parameters": [
|
|
23
|
+
{
|
|
24
|
+
"name": "fiyat",
|
|
25
|
+
"type": "number",
|
|
26
|
+
"required": true,
|
|
27
|
+
"description": "Birim fiyat"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "adet",
|
|
31
|
+
"type": "number",
|
|
32
|
+
"defaultValue": 1,
|
|
33
|
+
"description": "Adet; verilmezse 1"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"body": "return fiyat * adet;"
|
|
37
|
+
},
|
|
38
|
+
"workflowFunctionCreateArgs": {
|
|
39
|
+
"source": "workflow",
|
|
40
|
+
"name": "toplamHesapla",
|
|
41
|
+
"description": "fiyat × adet; adet verilmezse 1 sayılır",
|
|
42
|
+
"parameters": [
|
|
43
|
+
{
|
|
44
|
+
"name": "fiyat",
|
|
45
|
+
"type": "number",
|
|
46
|
+
"required": true,
|
|
47
|
+
"description": "Birim fiyat"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "adet",
|
|
51
|
+
"type": "number",
|
|
52
|
+
"defaultValue": 1,
|
|
53
|
+
"description": "Adet; verilmezse 1"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"body": "return fiyat * adet;"
|
|
57
|
+
},
|
|
58
|
+
"usage": {
|
|
59
|
+
"propBinding": {
|
|
60
|
+
"op": "set_binding",
|
|
61
|
+
"target": "txt_toplam",
|
|
62
|
+
"prop": "content",
|
|
63
|
+
"expression": "{{ Functions.toplamHesapla(10, 3) }}"
|
|
64
|
+
},
|
|
65
|
+
"uiFlowCodeStep": "const toplam = Functions.toplamHesapla(num_fiyat.value, num_adet.value);\nstate.page.sonuc = toplam;",
|
|
66
|
+
"uiFlowFunctionStep": "ngapps://examples/flow-call-function",
|
|
67
|
+
"workflowCodeStep": "return { toplam: toplamHesapla(10, 3) };",
|
|
68
|
+
"automationCondition": "toplamHesapla(row.fiyat, row.adet) > 1000"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"slotRowIsOpenEnded": "Slot'un grid satırı açık uçludur (runtime 'y / auto' yazar): sayfa uzadıkça slot uzar. Slot'a sabit yükseklik vermeye çalışma.",
|
|
13
13
|
"positionComesFromBlocks": "Konum ve boyut blocks[]'ten gelir. layout-region'daki dock prop'u konum DEĞİL biçim belirler; bu örnekte region hiç kullanılmıyor.",
|
|
14
14
|
"gridStacksFlexDoesNot": "Bu örnek layout:'flex' — fill'in 'kalanı doldur' anlamı flex'te tanımlıdır (sütunda flex:1 1 auto, satırda align-self:stretch). Bedeli MOBİL: runtime'ın mobil kuralı yalnız ızgara özelliklerini ezer, flex satırı telefonda yan yana kalır (375px'te ölçüldü: 49/310px). Telefonda alt alta düşmesi gereken kabuk için kökü layout:'grid' kur; o zaman yükseklik yine blocks satırlarından gelir.",
|
|
15
|
-
"fixedPxWidthIsFlexOnly": "Bu örnek flex olduğu için yan menüye appearance.
|
|
15
|
+
"fixedPxWidthIsFlexOnly": "Bu örnek flex olduğu için yan menüye appearance.widthType:'fixed' + appearance.widthString:'240px' verilebilir (serbest CSS uzunluğu; string yalnız 'fixed' modda okunur). Öteki modlar: 'fill' kabın tüm genişliği, 'auto' içerik kadar; widthType hiç verilmezse genişlik kolon payıdır (12'de 2 ≈ 1440px tasarım genişliğinde 240px). Izgara kabında widthString okunmaz. Üç modun bir arada örneği: ngapps://examples/page-flex-sections.",
|
|
16
16
|
"navigationsStyleIsDead": "navigations'ın style prop'u hiçbir şey yapmaz (Navigations.svelte style prop'u kabul etmiyor) — menü satırı biçimlendirmeye çalışma.",
|
|
17
17
|
"verifyWithRuntime": "Layout'un kendi serve URL'i yok: gerçek çıktıyı layout'a bağlı bir sayfada page_runtime_get ile, slot kurgusunu page_state.layoutSlots ile doğrula.",
|
|
18
18
|
"wrapperRoot": "Kök, sayfanın page_container'ının yerleşim ikizidir: states.isWrapper:true (çerçevesiz, başlıksız, saydam) + blocks x0/w12. Sarmalayıcı olmayan bir kök kenarlıklı beyaz bir kart olarak çizilir.",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"synthetic": false,
|
|
4
|
+
"note": "test-project/development ortamında gerçek uçta doğrulandı (2026-09-11): publish:true ile kayıt 'publishing' döndü ve 'published' oldu (publishedReference zf-test-project-development-...), microfunction_invoke {a:1,b:2} → ok:true output {total:3, environment:'development'}, aynı akışa bağlı microservice_execute → 200 {total:3}. Kurallar: inline kaynakta entrypoint GÖNDERİLMEZ (sabit function.handler); timeoutSeconds ve memoryMb non-nullable int'tir, eksik gönderilirse platform 422 döner; adım rule.microfunctionId YALNIZ published kaydı gösterebilir (WF-MF-001)."
|
|
5
|
+
},
|
|
6
|
+
"microfunction": {
|
|
7
|
+
"name": "toplam",
|
|
8
|
+
"description": "İki sayıyı toplar ve hangi ortamda koştuğunu döner.",
|
|
9
|
+
"language": "node",
|
|
10
|
+
"provider": "built-in",
|
|
11
|
+
"sourceKind": "inline",
|
|
12
|
+
"code": "exports.handler = async (args, context) => {\n return { total: (args.a ?? 0) + (args.b ?? 0), environment: context.environment }\n}\n",
|
|
13
|
+
"parameterDtoId": "<girdi DTO id: { a: number, b: number }>",
|
|
14
|
+
"parameterBehavior": "object",
|
|
15
|
+
"returnDtoId": "<çıktı DTO id: { total: number, environment: string }>",
|
|
16
|
+
"returnBehavior": "object",
|
|
17
|
+
"timeoutSeconds": 30,
|
|
18
|
+
"memoryMb": 256,
|
|
19
|
+
"publish": true
|
|
20
|
+
},
|
|
21
|
+
"workflowStep": {
|
|
22
|
+
"id": "<GUID-3>",
|
|
23
|
+
"name": "toplam",
|
|
24
|
+
"type": "microfunction",
|
|
25
|
+
"description": "Girdideki iki sayıyı microfunction'da toplar.",
|
|
26
|
+
"rule": {
|
|
27
|
+
"microfunctionId": "<yukarıdaki kaydın id'si; status published olmalı>",
|
|
28
|
+
"expression": "({ a: input.a, b: input.b })"
|
|
29
|
+
},
|
|
30
|
+
"branches": [
|
|
31
|
+
{
|
|
32
|
+
"id": "<GUID-4>",
|
|
33
|
+
"name": "success",
|
|
34
|
+
"type": "success",
|
|
35
|
+
"nextStepId": "<response adımının id'si>"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "<GUID-5>",
|
|
39
|
+
"name": "fail",
|
|
40
|
+
"type": "fail",
|
|
41
|
+
"nextStepId": null
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"responseStep": {
|
|
46
|
+
"id": "<GUID-6>",
|
|
47
|
+
"name": "Respond",
|
|
48
|
+
"type": "response",
|
|
49
|
+
"description": "Toplamı çağırana döner.",
|
|
50
|
+
"rule": {
|
|
51
|
+
"dataTransferObjectId": "<çıktı DTO id>",
|
|
52
|
+
"expression": "({ total: Steps.toplam.data.total })"
|
|
53
|
+
},
|
|
54
|
+
"branches": []
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_meta": {
|
|
3
3
|
"synthetic": true,
|
|
4
|
-
"note": "hr + sms projelerinde doğrulanmış şekil. Kurallar: operation 'list' YOK → get + isSingle:false; kökte entityId zorunlu; selectFields'ta fieldId zorunlu + alias = entity slug; join'li alanlarda path = ilişki alanı id zinciri (kök alanlarda path yazılmaz), joins[].path = kökEntityId + aynı zincir — path boş bırakılırsa runtime çalışır ama Studio join alanlarını seçili gösteremez; sort/limit her zaman verilir yoksa Studio formu null'da patlar; filtre value alanı JavaScript ifadesidir."
|
|
4
|
+
"note": "hr + sms projelerinde doğrulanmış şekil. Kurallar: operation 'list' YOK → get + isSingle:false; kökte entityId zorunlu; selectFields'ta fieldId zorunlu + alias = entity slug; join'li alanlarda path = ilişki alanı id zinciri (kök alanlarda path yazılmaz), joins[].path = kökEntityId + aynı zincir — path boş bırakılırsa runtime çalışır ama Studio join alanlarını seçili gösteremez; sort/limit her zaman verilir yoksa Studio formu null'da patlar; filtre value alanı JavaScript ifadesidir. isReturnSchemaArray/isParameterSchemaArray örnekten ÇIKARILDI (2026-09-07): ikisi de yalnız okuma modelinde (MicroserviceDto) var, Create/UpdateMicroserviceCommand'da yok — gönderilirse sessizce yok sayılırlar."
|
|
5
5
|
},
|
|
6
6
|
"definition": {
|
|
7
7
|
"name": "Get Products",
|
|
@@ -43,8 +43,6 @@
|
|
|
43
43
|
"source": "query-string"
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
|
-
"entityId": "<products entity id>"
|
|
47
|
-
"isReturnSchemaArray": true,
|
|
48
|
-
"isParameterSchemaArray": false
|
|
46
|
+
"entityId": "<products entity id>"
|
|
49
47
|
}
|
|
50
48
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"twoWayBinding": "Çocuk komponentin prop'u {type:'code', ref:'{{ item.<alan> }}'} kalıbına TAM uyuyorsa kullanıcı değiştirdiğinde runtime değeri rows.data[<index>].<alan>'a geri yazar (repeater.handler.ts → handleRepeaterItemPropertySet). Flow gerekmez. Kalıp bozulursa ('{{ item.answer || \"\" }}') okur ama YAZMAZ.",
|
|
12
12
|
"rowId": "Satırlara kalıcı bir id ver: getRepeaterRowId sırayla row._id / row.id / index'e bakar; id yoksa index'e düşer ve liste değişince girilen değerler kayar.",
|
|
13
13
|
"variantsSameCell": "Satır tipine göre farklı alan göstermek için TÜM varyantları AYNI blocks[].position'a koy ve isHidden ile ayır — alt alta dizersen gizli olanların ızgara satırları boş kalır. Bu yalnız page_schema_patch yolunda güvenlidir (micro-op hattı çakışma normalizasyonu uygulamaz).",
|
|
14
|
-
"isHiddenGap": "isHidden
|
|
14
|
+
"isHiddenGap": "isHidden artık 101 kaydın 99'unda tanımlıdır (2026-08-26 toplu eklemesi; tanımlamayan yalnız slot ve widget-area) — text ve multi-select dahil çoğu komponente doğrudan yazılabilir. Sarmalayıcı yalnız birden çok düğümü TEK koşulla gizlemek gerektiğinde kullanılır; o zaman states.isWrapper:false olmalı (isWrapper → min-height:80vh).",
|
|
15
15
|
"readBack": "Toplu okuma flow'da <repeaterAdı>.rows'tur; microservice parametresinin value.type:'code' ifadesinde ve condition expression'ında komponentlere ADIYLA erişilir. Kod bağlamında satırlar HAM gelir; machine.getNode ile bakarsan {data,type} sarılı görünür.",
|
|
16
16
|
"perRowValidation": "Satır BAŞINA validation kurulamaz — validations komponentin kendisindedir ve tüm satırlar paylaşır. Zorunluluğu satır verisinde taşı (required) ve gönderim akışının ilk condition adımında denetle (guard adımı).",
|
|
17
17
|
"flowGuids": "branches[].id ve iteratees[].id GUID'leri TÜM projede tekildir. Başka bir akıştan kopyaladığın gövdeyi aynı GUID'lerle gönderirsen platform RowAlreadyExists döner ve hata slug çakışması gibi okunur — bütün id alanlarını yenile.",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"page_container (container, isWrapper:true — tema değişkenleri ve ortalanmış sütun BURADA)",
|
|
26
26
|
"├─ t_title (text, h1)",
|
|
27
27
|
"├─ t_desc (text, p)",
|
|
28
|
-
"├─ q_form (form, isWrapper:false,
|
|
28
|
+
"├─ q_form (form, isWrapper:false, style: border none + padding 0)",
|
|
29
29
|
"│ └─ q_list (repeater, isWrapper:false, rows:[], autoRowSize:true, verticalSpacing:14)",
|
|
30
30
|
"│ └─ q_card (container, isWrapper:false) ← ŞABLONUN KÖKÜ",
|
|
31
31
|
"│ ├─ q_head (text) y:0 content: {{ item.no + '. ' + item.text + (item.required ? ' *' : '') }}",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"│ ├─ a_choice (radio) y:2 isHidden: {{ !(item.is_single || item.is_yesno) }}",
|
|
34
34
|
"│ ├─ a_text (textarea) y:2 isHidden: {{ !item.is_text }}",
|
|
35
35
|
"│ └─ box_multi (container) y:2 isHidden: {{ !item.is_multi }}",
|
|
36
|
-
"│ └─ a_multi (multi-select) ← kendi isHidden'ı
|
|
36
|
+
"│ └─ a_multi (multi-select) ← kendi isHidden'ı artık VAR; sarmal bu örneğin geçmişinden kaldı",
|
|
37
37
|
"├─ box_state (container, isHidden:true) → t_state (text) ← hata şeridi",
|
|
38
38
|
"└─ btn_submit (button, type:'button')"
|
|
39
39
|
]
|