@appaflytech/zeyrek-ai-mcp 0.0.4 → 0.0.6
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 +8 -3
- package/.env.production.example +2 -0
- package/README.md +229 -193
- package/dist/config.js +20 -2
- package/dist/knowledge/backendConstraints.js +897 -0
- package/dist/knowledge/componentCatalog.js +196 -42
- package/dist/knowledge/coverage.js +43 -0
- package/dist/knowledge/flowGuards.js +30 -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 +449 -16
- package/dist/knowledge/outlines.js +136 -12
- package/dist/knowledge/propTypes.js +80 -0
- package/dist/knowledge/rules.js +618 -43
- package/dist/knowledge/schemaGuard.js +199 -0
- package/dist/knowledge/styleResolve.js +63 -4
- package/dist/observability/trace.js +27 -3
- package/dist/platform/errors.js +46 -4
- package/dist/platform/http.js +33 -4
- package/dist/platform/patch.js +27 -0
- package/dist/platform/refs.js +104 -0
- package/dist/platform/responses.js +9 -2
- package/dist/platform/screenshot.js +527 -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/layout-app-shell.json +1 -1
- package/dist/resources/examples/microservice-table-list.json +2 -4
- package/dist/resources/examples/page-dynamic-form.json +2 -2
- package/dist/resources/examples/page-flex-sections.json +99 -0
- package/dist/resources/examples/page-sign-in.json +3 -2
- package/dist/resources/guides/component.md +69 -8
- package/dist/resources/guides/entity.md +404 -16
- package/dist/resources/guides/expressions.md +44 -2
- package/dist/resources/guides/external-component.md +9 -0
- package/dist/resources/guides/getting-started.md +77 -7
- package/dist/resources/guides/layout.md +53 -10
- package/dist/resources/guides/microservice.md +226 -13
- package/dist/resources/guides/middleware.md +89 -1
- package/dist/resources/guides/page.md +383 -46
- package/dist/resources/guides/translation.md +126 -8
- package/dist/resources/guides/view.md +16 -1
- package/dist/resources/guides/workflow.md +198 -27
- 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/CreateBulkRowCommand.json +33 -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/CreateMicroserviceCommand.json +26 -2
- 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 +95 -14
- package/dist/resources/schemas/GetRowsQuery.json +3 -0
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +16 -0
- package/dist/resources/schemas/PreviewQueryCommand.json +5 -2
- package/dist/resources/schemas/PreviewWorkflowCommand.json +34 -9
- package/dist/resources/schemas/StepDto.json +23 -3
- package/dist/resources/schemas/UpdateBulkRowCommand.json +33 -0
- package/dist/resources/schemas/UpdateDatasourceCommand.json +15 -6
- package/dist/resources/schemas/UpdateEntityCommand.json +49 -10
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +28 -3
- 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 +95 -14
- package/dist/server.js +4 -0
- package/dist/tools/access.js +3 -3
- package/dist/tools/applications.js +1 -1
- package/dist/tools/automations.js +63 -30
- package/dist/tools/common.js +39 -0
- package/dist/tools/components.js +16 -7
- package/dist/tools/datasources.js +83 -21
- package/dist/tools/deployments.js +34 -2
- package/dist/tools/dtos.js +25 -12
- package/dist/tools/entities.js +113 -28
- package/dist/tools/externalComponents.js +9 -3
- package/dist/tools/files.js +20 -6
- package/dist/tools/flows.js +110 -11
- package/dist/tools/folders.js +7 -2
- package/dist/tools/globalFunctions.js +63 -0
- package/dist/tools/layouts.js +103 -29
- package/dist/tools/microservices.js +229 -38
- package/dist/tools/organizations.js +5 -6
- package/dist/tools/pages.js +365 -100
- package/dist/tools/platformInfo.js +152 -11
- package/dist/tools/projects.js +183 -33
- package/dist/tools/queries.js +32 -10
- package/dist/tools/rows.js +159 -45
- package/dist/tools/screenWrite.js +59 -0
- package/dist/tools/screens.js +28 -0
- package/dist/tools/translations.js +24 -8
- package/dist/tools/views.js +20 -10
- package/dist/tools/workflows.js +33 -16
- package/package.json +6 -3
|
@@ -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
|
+
}
|
|
@@ -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.",
|
|
@@ -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.",
|
|
@@ -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
|
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"synthetic": true,
|
|
4
|
+
"note": "Flex boyut modeli (2026-09-01) örneği: wrap:true satır kapsayıcı içinde üç genişlik modu bir arada — 'fixed'+widthString sabit yan sütun, 'fill' kalan alanı alan ana bölge, widthType'sız (eski 12 kolon payı) tam genişlik alt şerit. Hücreler Studio Quick Stack iskeletiyle aynı desendedir (hücre tipi box, states.isQuickStack). page_schema_patch'in ops alanına olduğu gibi verilebilir.",
|
|
5
|
+
"pairsWith": "layout-app-shell (kabuk tarafındaki fixed/fill kullanımı), page-detail-profile (ızgara yerleşimli sayfa)",
|
|
6
|
+
"sourceRef": "platform_guide('page') Kural 1f-2; platform_guide('component') §appearance; zeyrek-frontend packages/ng-apps-screen-builder/src/flex-layout.ts (getFlexChildStyle, normalizeCssSize), apps/studio/src/components/screen-builder/quick-stack.functions.ts, zeyrek-frontend/docs/features/quick-stack.md"
|
|
7
|
+
},
|
|
8
|
+
"_rules": {
|
|
9
|
+
"explicitWidthType": "Flex çocuğun genişliği appearance.widthType'tan gelir — öncelik fill > fixed(+widthString) > auto > eski blocks.size.w payı (widthType hiç yoksa). Şemayı sen kuruyorsan modu AÇIKÇA yaz; cell_footer'daki mod eksikliği bilinçli bir fallback gösterimidir.",
|
|
10
|
+
"stringNeedsFixedMode": "widthString/heightString YALNIZ ilgili *Type:'fixed' iken okunur. 'fill'/'auto' moda string yazmak ölüdür — gateway PAGE-017 uyarısı verir, Studio paneli mod değişiminde string'i siler.",
|
|
11
|
+
"cssSizeAcceptance": "Boyut string'i kabul kuralı (normalizeCssSize): çıplak sayı px sayılır, birimler px/%/vw/vh/vmin/vmax/rem/em/ch, calc(...) geçer. Negatif, pt, fr, 'auto' GEÇMEZ — platform 200 döner ama panel ve runtime değeri sessizce düşürür (PAGE-018).",
|
|
12
|
+
"wrapDefaultTrue": "wrap varsayılanı 2026-09-01'den beri AÇIK: w:12 çocuklar alt satıra sararak dikey akar. Bu örnekte de açık bırakıldı; kapatırsan üç hücre tek satıra sıkışır.",
|
|
13
|
+
"gapThemeVars": "gap preset'leri tema değişkenidir (small/medium/large → var(--ng-spacing-sm/md/lg)); alan allowCustom'dır, '18px' gibi ham CSS de geçer.",
|
|
14
|
+
"quickStackSkeleton": "Studio'nun Quick Stack üreteci hücre olarak DAİMA box kurar, ebeveyne layout:'flex' + direction:'row' + wrap:true yazar ve satır span'ları 12'ye tamamlanır. states.isQuickStack yalnız editör süsüdür (kesikli çerçeve) — runtime'da hiçbir etkisi yoktur.",
|
|
15
|
+
"orderFromBlocks": "Flex'te sıra blocks[].position'dan gelir (önce y, sonra x) ve y yalnız SIRADIR; boşluk çizilmez. Yine de bitişik ver ki grid'e dönüşte şema geçerli kalsın.",
|
|
16
|
+
"heightStringOnlyFixed": "heightType:'fixed' ölçüyü heightString'den alır ('30vh' gibi), o yoksa size.h×10px'ten. heightType:'auto' içerik kadar, 'fill' ebeveynin bıraktığı yer."
|
|
17
|
+
},
|
|
18
|
+
"schemaPatchOps": [
|
|
19
|
+
{
|
|
20
|
+
"op": "add_component",
|
|
21
|
+
"parent": "page_container",
|
|
22
|
+
"component": {
|
|
23
|
+
"elementId": "sections_row",
|
|
24
|
+
"name": "sections_row",
|
|
25
|
+
"kind": "component",
|
|
26
|
+
"type": "container",
|
|
27
|
+
"props": {
|
|
28
|
+
"title": { "type": "static", "ref": null, "data": "" },
|
|
29
|
+
"layout": { "type": "static", "ref": null, "data": "flex" },
|
|
30
|
+
"direction": { "type": "static", "ref": null, "data": "row" },
|
|
31
|
+
"horizontalAlign": { "type": "static", "ref": null, "data": "left" },
|
|
32
|
+
"verticalAlign": { "type": "static", "ref": null, "data": "stretch" },
|
|
33
|
+
"size": { "type": "static", "ref": null, "data": "shrink" },
|
|
34
|
+
"gap": { "type": "static", "ref": null, "data": "medium" },
|
|
35
|
+
"wrap": { "type": "static", "ref": null, "data": true }
|
|
36
|
+
},
|
|
37
|
+
"states": { "isSticky": false, "isDroppable": true, "isScrollable": false, "isWrapper": false },
|
|
38
|
+
"appearance": { "heightType": "auto", "widthType": "fill", "marginType": "none", "paddingType": "none" },
|
|
39
|
+
"blocks": [{ "breakpoint": 1024, "size": { "w": 12, "h": 24 }, "position": { "x": 0, "y": 0 } }],
|
|
40
|
+
"children": [
|
|
41
|
+
{
|
|
42
|
+
"elementId": "cell_sidebar",
|
|
43
|
+
"name": "cell_sidebar",
|
|
44
|
+
"kind": "component",
|
|
45
|
+
"type": "box",
|
|
46
|
+
"props": {
|
|
47
|
+
"layout": { "type": "static", "ref": null, "data": "flex" },
|
|
48
|
+
"direction": { "type": "static", "ref": null, "data": "column" }
|
|
49
|
+
},
|
|
50
|
+
"states": { "isSticky": false, "isDroppable": true, "isScrollable": false, "isWrapper": false, "isQuickStack": true },
|
|
51
|
+
"appearance": { "heightType": "fixed", "heightString": "30vh", "widthType": "fixed", "widthString": "240px" },
|
|
52
|
+
"blocks": [{ "breakpoint": 1024, "size": { "w": 4, "h": 6 }, "position": { "x": 0, "y": 0 } }],
|
|
53
|
+
"children": [],
|
|
54
|
+
"events": [],
|
|
55
|
+
"validations": [],
|
|
56
|
+
"stateBindings": []
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"elementId": "cell_main",
|
|
60
|
+
"name": "cell_main",
|
|
61
|
+
"kind": "component",
|
|
62
|
+
"type": "box",
|
|
63
|
+
"props": {
|
|
64
|
+
"layout": { "type": "static", "ref": null, "data": "flex" },
|
|
65
|
+
"direction": { "type": "static", "ref": null, "data": "column" }
|
|
66
|
+
},
|
|
67
|
+
"states": { "isSticky": false, "isDroppable": true, "isScrollable": false, "isWrapper": false, "isQuickStack": true },
|
|
68
|
+
"appearance": { "heightType": "auto", "widthType": "fill" },
|
|
69
|
+
"blocks": [{ "breakpoint": 1024, "size": { "w": 8, "h": 6 }, "position": { "x": 4, "y": 0 } }],
|
|
70
|
+
"children": [],
|
|
71
|
+
"events": [],
|
|
72
|
+
"validations": [],
|
|
73
|
+
"stateBindings": []
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"elementId": "cell_footer",
|
|
77
|
+
"name": "cell_footer",
|
|
78
|
+
"kind": "component",
|
|
79
|
+
"type": "box",
|
|
80
|
+
"props": {
|
|
81
|
+
"layout": { "type": "static", "ref": null, "data": "flex" },
|
|
82
|
+
"direction": { "type": "static", "ref": null, "data": "row" }
|
|
83
|
+
},
|
|
84
|
+
"states": { "isSticky": false, "isDroppable": true, "isScrollable": false, "isWrapper": false, "isQuickStack": true },
|
|
85
|
+
"appearance": { "heightType": "auto" },
|
|
86
|
+
"blocks": [{ "breakpoint": 1024, "size": { "w": 12, "h": 6 }, "position": { "x": 0, "y": 6 } }],
|
|
87
|
+
"children": [],
|
|
88
|
+
"events": [],
|
|
89
|
+
"validations": [],
|
|
90
|
+
"stateBindings": []
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"events": [],
|
|
94
|
+
"validations": [],
|
|
95
|
+
"stateBindings": []
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
"cookieNameContract": "setcookie'deki ad ile auth middleware'inin okuduğu ad BİREBİR aynı olmalı (burada APP_Token → middleware'de cookie.APP_Token). Uyuşmazlık 'her istek 401' olarak görünür ve teşhisi zordur.",
|
|
10
10
|
"signInEndpoint": "Sign-in ucu bir workflow'dur: kullanıcıyı e-postayla bulur, şifreyi karşılaştırır, system.generateToken({...claims}) ile token üretir. Claim anahtarları neyse middleware onları okur (claims.userId gibi) — token.data.id diye evrensel bir alan YOKTUR. Bkz. platform_guide('expressions') ve platform_guide('middleware') Tarif 1.",
|
|
11
11
|
"conditionAlways": "Token kontrolü condition ile yapılır; `always` dalı buton durumunu her koşumda sıfırlar, kural dalları başarı/hata yolunu ayırır.",
|
|
12
|
+
"submitButtonType": "Formun `submit` olayına akış bağlamak TEK BAŞINA yetmez: `button` varsayılan olarak `type=\"button\"` çizilir ve HTML form submit'i HİÇ doğmaz — tıklamak da alanda Enter da uca istek göndermez ve hiçbir hata çıkmaz (ölçüldü 2026-09-05; teşhis ancak gerçek tıklamayla mümkün oldu, `page_screenshot` `actions`). Bu yüzden butona `props.type = 'submit'` veriliyor. Alternatif: aynı akışı butonun `click` olayına da bağlamak. Gateway ikisi de yoksa FORM-003 ile uyarır.",
|
|
12
13
|
"rawRedirect": "redirect isRaw:true iken path ÇOK SATIRLI JS bloğudur ve son ifadenin değeri hedef adrestir. 'Korunan sayfaya gitmek isterken login'e düşen kullanıcıyı geri gönderme' deseni budur; yoksa sabit bir sayfaya gidilir (pageId + path).",
|
|
13
14
|
"objectProp": "Prop değeri nesne olabilir: icon → { type:'static', ref:{ name:'progress_activity' } }.",
|
|
14
15
|
"pageIsPublic": "Giriş sayfasına middlewareIds BAĞLANMAZ (guard'lı olsaydı kullanıcı giriş yapamazdı). Buna karşılık diğer sayfalar ve korunan uçlar middlewareIds ile bağlanır.",
|
|
15
|
-
"notificationIsHidden": "notification
|
|
16
|
+
"notificationIsHidden": "notification'ın şemasında isHidden 2026-08-26'dan beri VARDIR: props.isHidden'ı doğrudan bildirime yazabilirsin. Bu örnekteki box_message sarmalayıcısı o dönemin kalıntısıdır (crm koşumu B-10) ve tek bir bildirim için ARTIK GEREKMEZ — burada duruyor çünkü şeridi bir kaba sarıp birden çok parçayı birlikte gösterip gizliyor. appearance.isHidden ise hiçbir zaman çözüm DEĞİLDİR — runtime onu hiç okumaz.",
|
|
16
17
|
"validationShape": "Validation gövdesi DÖRT alan taşır: { type, message, value, isActive }. 'name' anahtarı JsonIgnore'dur — {name, value, message} yazarsan tip enum varsayılanına düşer ve alan sessizce REGEX doğrulamasına döner. Tip adları TİRELİDİR (max-length / min-length / max-value / min-value); 'maxLength' backend'e yazılır ama runtime doğrulayıcı onu tanımaz ve doğrulama hiç koşmaz. Gateway düzeltebildiğini düzeltir, kalanını PAGE-016 ile reddeder."
|
|
17
18
|
},
|
|
18
19
|
"pageCreateArgs": {
|
|
@@ -103,7 +104,7 @@
|
|
|
103
104
|
"parent": "sign_in_form",
|
|
104
105
|
"component": {
|
|
105
106
|
"elementId": "btn_sign_in", "kind": "component", "type": "button", "name": "btn_sign_in",
|
|
106
|
-
"props": { "text": { "type": "static", "data": "Giriş yap" } },
|
|
107
|
+
"props": { "text": { "type": "static", "data": "Giriş yap" }, "type": { "type": "static", "data": "submit" } },
|
|
107
108
|
"states": { "isSticky": false, "isDroppable": false, "isScrollable": false, "isWrapper": false },
|
|
108
109
|
"blocks": [{ "breakpoint": 1024, "size": { "h": 4, "w": 12 }, "position": { "x": 0, "y": 10 } }],
|
|
109
110
|
"children": [], "events": [], "validations": [], "stateBindings": []
|