@appaflytech/zeyrek-ai-mcp 0.0.1
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 +46 -0
- package/README.md +260 -0
- package/dist/config.js +128 -0
- package/dist/index.js +88 -0
- package/dist/knowledge/componentCatalog.js +438 -0
- package/dist/knowledge/componentEvents.js +164 -0
- package/dist/knowledge/coverage.js +60 -0
- package/dist/knowledge/normalize.js +1309 -0
- package/dist/knowledge/outlines.js +167 -0
- package/dist/knowledge/policy.js +44 -0
- package/dist/knowledge/reservedTables.js +75 -0
- package/dist/knowledge/rules.js +1725 -0
- package/dist/observability/trace.js +337 -0
- package/dist/platform/errors.js +132 -0
- package/dist/platform/http.js +169 -0
- package/dist/platform/responses.js +94 -0
- package/dist/platform/token.js +79 -0
- package/dist/resources/examples/entity-pair-m2m.json +105 -0
- package/dist/resources/examples/flow-list-to-form.json +65 -0
- package/dist/resources/examples/flow-load-table.json +178 -0
- package/dist/resources/examples/microservice-table-list.json +49 -0
- package/dist/resources/examples/middleware-authenticate.json +194 -0
- package/dist/resources/examples/page-detail-profile.json +95 -0
- package/dist/resources/examples/page-dynamic-form.json +221 -0
- package/dist/resources/examples/page-form-upsert.json +279 -0
- package/dist/resources/examples/page-html-embed.json +106 -0
- package/dist/resources/examples/page-sign-in.json +194 -0
- package/dist/resources/examples/workflow-basic.json +94 -0
- package/dist/resources/guides/component.md +108 -0
- package/dist/resources/guides/entity.md +348 -0
- package/dist/resources/guides/expressions.md +139 -0
- package/dist/resources/guides/getting-started.md +109 -0
- package/dist/resources/guides/html.md +106 -0
- package/dist/resources/guides/microservice.md +313 -0
- package/dist/resources/guides/middleware.md +271 -0
- package/dist/resources/guides/modify-existing.md +35 -0
- package/dist/resources/guides/page.md +1574 -0
- package/dist/resources/guides/view.md +89 -0
- package/dist/resources/guides/workflow.md +359 -0
- package/dist/resources/knowledge.js +44 -0
- package/dist/resources/schemas/CreateDatasourceCommand.json +54 -0
- package/dist/resources/schemas/CreateDiscoveredEntitiesCommand.json +133 -0
- package/dist/resources/schemas/CreateEntityCommand.json +387 -0
- package/dist/resources/schemas/CreateMicroserviceCommand.json +528 -0
- package/dist/resources/schemas/CreateQueryCommand.json +43 -0
- package/dist/resources/schemas/CreateViewCommand.json +158 -0
- package/dist/resources/schemas/CreateWorkflowCommand.json +1719 -0
- package/dist/resources/schemas/Filter.json +124 -0
- package/dist/resources/schemas/GetRowsQuery.json +122 -0
- package/dist/resources/schemas/PreviewMicroserviceCommand.json +565 -0
- package/dist/resources/schemas/PreviewQueryCommand.json +74 -0
- package/dist/resources/schemas/PreviewWorkflowCommand.json +981 -0
- package/dist/resources/schemas/StepDto.json +900 -0
- package/dist/resources/schemas/UpdateDatasourceCommand.json +58 -0
- package/dist/resources/schemas/UpdateEntityCommand.json +679 -0
- package/dist/resources/schemas/UpdateMicroserviceCommand.json +512 -0
- package/dist/resources/schemas/UpdateQueryCommand.json +86 -0
- package/dist/resources/schemas/UpdateViewCommand.json +153 -0
- package/dist/resources/schemas/UpdateWorkflowCommand.json +1719 -0
- package/dist/resources/schemas.js +40 -0
- package/dist/server.js +63 -0
- package/dist/tools/access.js +27 -0
- package/dist/tools/applications.js +59 -0
- package/dist/tools/automations.js +103 -0
- package/dist/tools/common.js +18 -0
- package/dist/tools/components.js +280 -0
- package/dist/tools/datasources.js +82 -0
- package/dist/tools/dependencies.js +17 -0
- package/dist/tools/deployments.js +17 -0
- package/dist/tools/dtos.js +54 -0
- package/dist/tools/entities.js +166 -0
- package/dist/tools/files.js +138 -0
- package/dist/tools/flows.js +226 -0
- package/dist/tools/folders.js +51 -0
- package/dist/tools/layouts.js +105 -0
- package/dist/tools/microservices.js +198 -0
- package/dist/tools/organizations.js +65 -0
- package/dist/tools/pages.js +597 -0
- package/dist/tools/platformInfo.js +44 -0
- package/dist/tools/projects.js +152 -0
- package/dist/tools/queries.js +48 -0
- package/dist/tools/rows.js +115 -0
- package/dist/tools/screens.js +97 -0
- package/dist/tools/templates.js +65 -0
- package/dist/tools/variables.js +94 -0
- package/dist/tools/views.js +58 -0
- package/dist/tools/workflows.js +196 -0
- package/package.json +49 -0
|
@@ -0,0 +1,1725 @@
|
|
|
1
|
+
import { reservedTenantTables } from './reservedTables.js';
|
|
2
|
+
const terminalStepTypes = new Set(['response', 'convert', 'revert', 'end']);
|
|
3
|
+
const stepKindMatrix = {
|
|
4
|
+
start: ['workflow', 'automation', 'middleware'],
|
|
5
|
+
condition: ['workflow', 'automation', 'middleware'],
|
|
6
|
+
code: ['workflow', 'automation', 'middleware'],
|
|
7
|
+
loop: ['workflow', 'automation', 'middleware'],
|
|
8
|
+
table: ['workflow', 'automation', 'middleware'],
|
|
9
|
+
query: ['workflow', 'automation', 'middleware'],
|
|
10
|
+
http: ['workflow', 'automation', 'middleware'],
|
|
11
|
+
workflow: ['workflow', 'automation', 'middleware'],
|
|
12
|
+
email: ['workflow', 'automation', 'middleware'],
|
|
13
|
+
'file-upload': ['workflow', 'automation', 'middleware'],
|
|
14
|
+
variable: ['middleware'],
|
|
15
|
+
'set-user': ['middleware'],
|
|
16
|
+
setcookie: ['middleware'],
|
|
17
|
+
redirect: ['middleware'],
|
|
18
|
+
response: ['workflow', 'middleware'],
|
|
19
|
+
convert: ['workflow'],
|
|
20
|
+
revert: ['automation'],
|
|
21
|
+
end: ['workflow', 'automation', 'middleware']
|
|
22
|
+
};
|
|
23
|
+
function asArray(value) {
|
|
24
|
+
return Array.isArray(value) ? value : [];
|
|
25
|
+
}
|
|
26
|
+
function workflowBody(definition, kind) {
|
|
27
|
+
if (kind === 'automation' && definition.automation && typeof definition.automation === 'object') {
|
|
28
|
+
const automationBody = definition.automation;
|
|
29
|
+
const automationSteps = asArray(automationBody.steps);
|
|
30
|
+
return automationSteps.length > 0 ? automationBody : { ...automationBody, steps: definition.steps };
|
|
31
|
+
}
|
|
32
|
+
return definition;
|
|
33
|
+
}
|
|
34
|
+
function workflowSteps(definition) {
|
|
35
|
+
return asArray(definition.steps).filter((step) => Boolean(step) && typeof step === 'object');
|
|
36
|
+
}
|
|
37
|
+
function stepLabel(step) {
|
|
38
|
+
return step.name || step.type || step.id || '?';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* "Guard'lı mı?" sorusunun tek doğru cevabı YAZIM BİÇİMİ DEĞİL, SIRA: kökün kendisine (üye
|
|
42
|
+
* erişimi olmadan) bir kez dokunulduğu an — `Page.user &&`, `Page.user || {}`, `!Page.user`,
|
|
43
|
+
* `typeof Page.user`, `Page.user?.` — ifade guard'lanmıştır. Eski kural yalnız
|
|
44
|
+
* `Page.user && Page.user.x` kalıbını tanıyordu ve araya parantez giren
|
|
45
|
+
* `(Page.user && (Page.user.roles_csv || ''))` — rehberin KENDİ önerdiği yazım — yanlış pozitif
|
|
46
|
+
* üretiyordu (crm koşumu B-13). Guard'dan ÖNCE gelen üye erişimi hâlâ bulgudur.
|
|
47
|
+
*/
|
|
48
|
+
export function readsBeforeGuard(source, root) {
|
|
49
|
+
const escaped = root.replace(/\./g, '\\.');
|
|
50
|
+
const guardIndex = source.search(new RegExp(`${escaped}(?!\\s*\\.)`));
|
|
51
|
+
const limit = guardIndex === -1 ? Number.POSITIVE_INFINITY : guardIndex;
|
|
52
|
+
return [...source.matchAll(new RegExp(`${escaped}\\s*\\.\\s*[A-Za-z_]`, 'g'))].some(match => match.index < limit);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sayfa akışında dal ADI bir etikettir, motorun anahtarıdır: Runtime.svelte:329 branch.id'yi
|
|
56
|
+
* branch.name'den üretir, task'lar `engine.next('always')` çağırır (start.ts:4, setter.ts:258,
|
|
57
|
+
* code.ts:49). Eşleşme yoksa flow-engine.ts:79 sessizce durur. Studio tarafında da zincir
|
|
58
|
+
* `sourceBranch === 'always'` ile çizilir (EventActionsStart.svelte:47, EventActionsItem.svelte:298).
|
|
59
|
+
* condition dalları rule id'sidir (serbest), microservice'te 'success' yalnız eski şema için
|
|
60
|
+
* yaşatılır (microservice.ts:144).
|
|
61
|
+
*/
|
|
62
|
+
const flowBranchNames = {
|
|
63
|
+
start: ['always'],
|
|
64
|
+
microservice: ['always', 'success'],
|
|
65
|
+
confirm: ['yes', 'no', 'always'],
|
|
66
|
+
end: []
|
|
67
|
+
};
|
|
68
|
+
const flowFreeBranchTypes = new Set(['condition']);
|
|
69
|
+
const flowSetterIterateeTypes = new Set(['change', 'fill']);
|
|
70
|
+
function flowStepList(value) {
|
|
71
|
+
return asArray(value).filter((step) => Boolean(step) && typeof step === 'object');
|
|
72
|
+
}
|
|
73
|
+
function flowAllSteps(definition) {
|
|
74
|
+
return [...flowStepList(definition.steps), ...flowStepList(definition.errorSteps)];
|
|
75
|
+
}
|
|
76
|
+
function flowStepLabel(step) {
|
|
77
|
+
return step.stepId || step.name || step.type || '?';
|
|
78
|
+
}
|
|
79
|
+
function flowBranches(step) {
|
|
80
|
+
return asArray(step.branches).filter((branch) => Boolean(branch) && typeof branch === 'object');
|
|
81
|
+
}
|
|
82
|
+
function flowIteratees(step) {
|
|
83
|
+
return asArray((step.args ?? {}).iteratees).filter((iteratee) => Boolean(iteratee) && typeof iteratee === 'object');
|
|
84
|
+
}
|
|
85
|
+
function flowValueOf(iteratee) {
|
|
86
|
+
return (iteratee.value ?? {});
|
|
87
|
+
}
|
|
88
|
+
function flowMappingOf(iteratee) {
|
|
89
|
+
return (iteratee.mapping ?? {});
|
|
90
|
+
}
|
|
91
|
+
function flowMappingsOf(iteratee) {
|
|
92
|
+
const mappings = flowMappingOf(iteratee).mappings;
|
|
93
|
+
return Boolean(mappings) && typeof mappings === 'object' && !Array.isArray(mappings) ? mappings : {};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* setter.ts:161 — storage dalında `mapping.source` doluysa storage anahtarı ODUR ve `value.ref`
|
|
97
|
+
* hiç okunmaz (Studio'nun ürettiği şekil budur: mapping.source dolu, value.ref boş). Ref
|
|
98
|
+
* denetimleri bu yüzden ikisinden hangisi kullanılacaksa onun üzerinden yürümelidir.
|
|
99
|
+
*/
|
|
100
|
+
function flowStorageReference(iteratee) {
|
|
101
|
+
const mappingSource = flowMappingOf(iteratee).source;
|
|
102
|
+
if (typeof mappingSource === 'string' && mappingSource.length > 0)
|
|
103
|
+
return { reference: mappingSource, origin: 'mapping.source' };
|
|
104
|
+
const reference = flowValueOf(iteratee).ref;
|
|
105
|
+
return { reference: typeof reference === 'string' ? reference : '', origin: 'value.ref' };
|
|
106
|
+
}
|
|
107
|
+
/** Storage anahtarı adımın ADIdır (microservice.ts:89 `machine.setStorage(task.name, …)`), stepId değil. */
|
|
108
|
+
function flowStorageRoot(reference) {
|
|
109
|
+
return typeof reference === 'string' ? reference.split('.')[0] ?? '' : '';
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Akıştaki bütün JS ifadeleri: code adımının gövdesi (code.ts:18 `task.argument.value`), setter
|
|
113
|
+
* iteratee'sinin `value.type='code'` ref'i (setter.ts:193), mapping satırlarının kod ref'leri
|
|
114
|
+
* (setter.ts:91) ve microservice parametrelerinin kod ref'leri (microservice.ts:248). Hepsi aynı
|
|
115
|
+
* `runCode` sandbox'ında koşar.
|
|
116
|
+
*/
|
|
117
|
+
function flowCodeExpressions(step) {
|
|
118
|
+
const args = (step.args ?? {});
|
|
119
|
+
const expressions = [];
|
|
120
|
+
if (step.type === 'code' && typeof args.value === 'string')
|
|
121
|
+
expressions.push({ expression: args.value, origin: 'args.value' });
|
|
122
|
+
for (const iteratee of flowIteratees(step)) {
|
|
123
|
+
const value = flowValueOf(iteratee);
|
|
124
|
+
const target = `${String(iteratee.element ?? '?')}.${String(iteratee.property ?? '?')}`;
|
|
125
|
+
if (value.type === 'code' && typeof value.ref === 'string')
|
|
126
|
+
expressions.push({ expression: value.ref, origin: `iteratees['${target}'].value.ref` });
|
|
127
|
+
for (const [key, mapping] of Object.entries(flowMappingsOf(iteratee))) {
|
|
128
|
+
const mappingValue = (mapping ?? {});
|
|
129
|
+
if (mappingValue.type === 'code' && typeof mappingValue.ref === 'string') {
|
|
130
|
+
expressions.push({ expression: mappingValue.ref, origin: `mapping.mappings['${key}']` });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const parameter of asArray(args.parameters).filter((parameter) => Boolean(parameter) && typeof parameter === 'object')) {
|
|
135
|
+
const value = (parameter.value ?? {});
|
|
136
|
+
if (value.type === 'code' && typeof value.ref === 'string')
|
|
137
|
+
expressions.push({ expression: value.ref, origin: `parameters['${String(parameter.name ?? '?')}']` });
|
|
138
|
+
}
|
|
139
|
+
return expressions;
|
|
140
|
+
}
|
|
141
|
+
/** `Steps.ad.headers` / `Steps["ad"].headers` yakalar, `Steps.ad?.headers` guard'lısını yakalamaz. */
|
|
142
|
+
const flowHeadersAccess = /Steps\s*(?:\.\s*([A-Za-z_$][\w$]*)|\[\s*['"]([^'"]+)['"]\s*\])\s*\.\s*headers\b/g;
|
|
143
|
+
const writeOperations = new Set(['create', 'update', 'delete', 'upsert']);
|
|
144
|
+
const filterOperators = new Set(['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'contains', 'icontains', 'in', 'notin', 'startswith', 'endswith', 'istartswith', 'iendswith']);
|
|
145
|
+
function flattenFilterTree(filters) {
|
|
146
|
+
return filters
|
|
147
|
+
.filter((filter) => Boolean(filter) && typeof filter === 'object')
|
|
148
|
+
.flatMap(filter => [filter, ...flattenFilterTree(asArray(filter.children))]);
|
|
149
|
+
}
|
|
150
|
+
function unknownOperatorFindings(ruleId, owner, filters) {
|
|
151
|
+
const unknownOperators = flattenFilterTree(filters)
|
|
152
|
+
.filter(filter => filter.operator !== undefined && filter.operator !== null && !filterOperators.has(String(filter.operator).toLowerCase()))
|
|
153
|
+
.map(filter => `${String(filter.fieldSlug ?? filter.fieldId ?? '?')}: '${String(filter.operator)}'`);
|
|
154
|
+
return unknownOperators.length > 0
|
|
155
|
+
? [{
|
|
156
|
+
ruleId,
|
|
157
|
+
severity: 'block',
|
|
158
|
+
message: `${owner}: bilinmeyen filtre operatörü — ${unknownOperators.join(', ')}. Platform bilinmeyen operatörü 400 ile REDDETMEZ, sessizce 'eq'e düşürür (EnumExtensions.GetValueFromDescription null → default) ve uç testten geçip yanlış çalışır. Geçerli sözlük: ${[...filterOperators].join(' · ')} ('like' yok — metin araması için 'contains'/'icontains').`,
|
|
159
|
+
guide: 'ngapps://guides/microservice'
|
|
160
|
+
}]
|
|
161
|
+
: [];
|
|
162
|
+
}
|
|
163
|
+
function isReadOperation(parameters) {
|
|
164
|
+
const operation = String(parameters?.operation ?? '').toLowerCase();
|
|
165
|
+
return !writeOperations.has(operation);
|
|
166
|
+
}
|
|
167
|
+
function isFileField(field) {
|
|
168
|
+
return String(field.dbType ?? '') === 'FILE' || String(field.commonType ?? '').toLowerCase().includes('file');
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Çok değerli enum'un rehberde belgelenen (ve canlı doğrulanan) şekli: `VARCHAR` + `many-to-many` +
|
|
172
|
+
* `valueOptions`, `relationId` VERİLMEZ — platform enum değerleri için kendi junction/değer
|
|
173
|
+
* entity'sini üretir ve `relationMetadata.enumValuesEntityId`'yi kendisi doldurur. ENT-001 bunu
|
|
174
|
+
* "ilişki alanı yanlış kurulmuş" diye üç kez uyardı (etkinlik turu H5); yanlış pozitifler gerçek
|
|
175
|
+
* uyarıları gölgelediği için FILE alanlarındaki muafiyetin (G-02) ikizi olarak burada da muaf.
|
|
176
|
+
*/
|
|
177
|
+
function isMultiEnumField(field) {
|
|
178
|
+
const valueOptions = asArray(field.valueOptions);
|
|
179
|
+
return valueOptions.length > 0 || field.isEnum === true;
|
|
180
|
+
}
|
|
181
|
+
export function isJunctionField(field) {
|
|
182
|
+
const relationType = String(field.relationType ?? '');
|
|
183
|
+
const commonType = String(field.commonType ?? '');
|
|
184
|
+
return relationType === 'many-to-many' || commonType === 'multiRelation' || commonType === 'multiFile' || commonType === 'multiArray';
|
|
185
|
+
}
|
|
186
|
+
function tableSelectionFindings(ruleId, owner, parameters, guide) {
|
|
187
|
+
if (!parameters)
|
|
188
|
+
return [];
|
|
189
|
+
const joins = asArray(parameters.joins);
|
|
190
|
+
const selectFields = asArray(parameters.selectFields);
|
|
191
|
+
if (joins.length > 0 && selectFields.length === 0) {
|
|
192
|
+
return [{
|
|
193
|
+
ruleId,
|
|
194
|
+
severity: 'block',
|
|
195
|
+
message: `${owner}: join tanımlı ama selectFields boş — boş bırakmak TÜM alanları döndürür ve join'li sorguda maliyeti katlar. Yalnız ihtiyaç duyulan alanları selectFields'a ekleyin (TableSelect: { alias, fieldId, path, fieldSlug }).`,
|
|
196
|
+
guide
|
|
197
|
+
}];
|
|
198
|
+
}
|
|
199
|
+
if (selectFields.length === 0 && String(parameters.operation ?? '').toLowerCase() !== 'delete') {
|
|
200
|
+
return [{
|
|
201
|
+
ruleId,
|
|
202
|
+
severity: 'warn',
|
|
203
|
+
message: `${owner}: selectFields boş — tüm alanlar dönecek. Bilinçli değilse yalnız ihtiyaç duyulan alanları seçin.`,
|
|
204
|
+
guide
|
|
205
|
+
}];
|
|
206
|
+
}
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
export const policyRules = [
|
|
210
|
+
{
|
|
211
|
+
id: 'MS-001',
|
|
212
|
+
module: 'microservice',
|
|
213
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
214
|
+
sourceRef: 'KAPI testi; CreateMicroserviceCommand.sourceParameters (selectFields/joins). Yalnız okuma operasyonlarında (get/count/boş): write yolunda platform selectFields\'i zaten yok sayar — TableMicroserviceExecutor.ByCreateAsync:215 (klinik K-003/K-007)',
|
|
215
|
+
check: definition => {
|
|
216
|
+
const parameters = definition.sourceParameters;
|
|
217
|
+
if (!isReadOperation(parameters))
|
|
218
|
+
return [];
|
|
219
|
+
if (definition.source === 'table') {
|
|
220
|
+
return tableSelectionFindings('MS-001', 'sourceParameters', parameters, 'ngapps://guides/microservice');
|
|
221
|
+
}
|
|
222
|
+
if (definition.source === 'view') {
|
|
223
|
+
return tableSelectionFindings('MS-001', 'sourceParameters (view)', parameters, 'ngapps://guides/microservice')
|
|
224
|
+
.map(finding => ({ ...finding, severity: 'warn', message: `${finding.message} [view source canlı testte doğrulanmadı — warn]` }));
|
|
225
|
+
}
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'MS-002',
|
|
231
|
+
module: 'microservice',
|
|
232
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
233
|
+
sourceRef: 'KAPI testi; source enum query|table|workflow|view',
|
|
234
|
+
check: definition => definition.source === 'query'
|
|
235
|
+
? [{
|
|
236
|
+
ruleId: 'MS-002',
|
|
237
|
+
severity: 'warn',
|
|
238
|
+
message: "source='query' seçildi: table kaynağı, query'nin yapabildiği neredeyse her şeyi platform-native yapar (filtre, join, sıralama, alan seçimi). Ham SQL gerçekten şartsa devam edin; değilse source='table' kullanın.",
|
|
239
|
+
guide: 'ngapps://guides/microservice'
|
|
240
|
+
}]
|
|
241
|
+
: []
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 'WF-001',
|
|
245
|
+
module: 'workflow',
|
|
246
|
+
appliesTo: ['create', 'update'],
|
|
247
|
+
sourceRef: 'KAPI testi; StepDto.branches[].nextStepId; Steps/IStep.cs StepOutput.BranchId',
|
|
248
|
+
check: (definition, context) => {
|
|
249
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
250
|
+
const steps = workflowSteps(body);
|
|
251
|
+
if (steps.length === 0) {
|
|
252
|
+
return [{ ruleId: 'WF-001', severity: 'block', message: 'steps boş — en az bir start step\'i ve ona bağlı akış gerekir.', guide: 'ngapps://guides/workflow' }];
|
|
253
|
+
}
|
|
254
|
+
const startStep = steps.find(step => step.type === 'start');
|
|
255
|
+
if (!startStep) {
|
|
256
|
+
return [{ ruleId: 'WF-001', severity: 'block', message: "type='start' step'i yok — her akış start ile başlar.", guide: 'ngapps://guides/workflow' }];
|
|
257
|
+
}
|
|
258
|
+
const stepsById = new Map(steps.filter(step => step.id).map(step => [String(step.id), step]));
|
|
259
|
+
const reached = new Set();
|
|
260
|
+
const queue = [String(startStep.id ?? '')];
|
|
261
|
+
while (queue.length > 0) {
|
|
262
|
+
const currentId = queue.pop();
|
|
263
|
+
if (!currentId || reached.has(currentId))
|
|
264
|
+
continue;
|
|
265
|
+
reached.add(currentId);
|
|
266
|
+
for (const branch of asArray(stepsById.get(currentId)?.branches)) {
|
|
267
|
+
if (branch?.nextStepId)
|
|
268
|
+
queue.push(String(branch.nextStepId));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const unreachable = steps.filter(step => step.id && !reached.has(String(step.id)));
|
|
272
|
+
if (unreachable.length > 0) {
|
|
273
|
+
return [{
|
|
274
|
+
ruleId: 'WF-001',
|
|
275
|
+
severity: 'block',
|
|
276
|
+
message: `Start'tan erişilemeyen step'ler var: ${unreachable.map(stepLabel).join(', ')}. Step'ler branches[].nextStepId ile bağlanır — önceki step'in uygun branch'ine (success/fail/else) bu step'lerin id'sini yazın.`,
|
|
277
|
+
guide: 'ngapps://guides/workflow'
|
|
278
|
+
}];
|
|
279
|
+
}
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'WF-002',
|
|
285
|
+
module: 'workflow',
|
|
286
|
+
appliesTo: ['create', 'update'],
|
|
287
|
+
sourceRef: 'KAPI testi; iş kuralı: workflow tipinde ReturnDto zorunlu (automation/middleware muaf)',
|
|
288
|
+
check: (definition, context) => {
|
|
289
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
290
|
+
const kind = context.workflowKind ?? body.type;
|
|
291
|
+
if (kind === 'workflow' && !body.returnDtoId) {
|
|
292
|
+
return [{
|
|
293
|
+
ruleId: 'WF-002',
|
|
294
|
+
severity: 'block',
|
|
295
|
+
message: "type='workflow' için returnDtoId zorunludur (backend nullable bırakır ama iş kuralı zorunlu; automation ve middleware muaftır). Önce DataTransferObject oluşturup id'sini returnDtoId'ye verin.",
|
|
296
|
+
guide: 'ngapps://guides/workflow'
|
|
297
|
+
}];
|
|
298
|
+
}
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: 'WF-003',
|
|
304
|
+
module: 'workflow',
|
|
305
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
306
|
+
sourceRef: 'DTO-önce akışı; WorkflowTableForm input.name placeholder\'ları',
|
|
307
|
+
check: (definition, context) => {
|
|
308
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
309
|
+
if (!body.parameterDtoId && JSON.stringify(body.steps ?? '').includes('input.')) {
|
|
310
|
+
return [{
|
|
311
|
+
ruleId: 'WF-003',
|
|
312
|
+
severity: 'warn',
|
|
313
|
+
message: "Step'lerde 'input.' referansı var ama parameterDtoId tanımlı değil — girdiler DataTransferObject ile tanımlanmalı ki input.<alan> referansları bir şeye bağlansın.",
|
|
314
|
+
guide: 'ngapps://guides/workflow'
|
|
315
|
+
}];
|
|
316
|
+
}
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'WF-004',
|
|
322
|
+
module: 'workflow',
|
|
323
|
+
appliesTo: ['create', 'update'],
|
|
324
|
+
sourceRef: 'UI WORKFLOW_STEPS: Response modülleri workflow+middleware',
|
|
325
|
+
check: (definition, context) => {
|
|
326
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
327
|
+
const kind = context.workflowKind ?? body.type;
|
|
328
|
+
if (kind === 'workflow' && !workflowSteps(body).some(step => step.type === 'response')) {
|
|
329
|
+
return [{
|
|
330
|
+
ruleId: 'WF-004',
|
|
331
|
+
severity: 'warn',
|
|
332
|
+
message: "type='workflow' ama response step'i yok — akış yanıt döndürmeyecek. Bilinçli değilse akışın sonuna response step'i bağlayın.",
|
|
333
|
+
guide: 'ngapps://guides/workflow'
|
|
334
|
+
}];
|
|
335
|
+
}
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: 'WF-005',
|
|
341
|
+
module: 'workflow',
|
|
342
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
343
|
+
sourceRef: 'NgApps.UI apps/studio features/workflow/workflow.constants.ts WORKFLOW_STEPS[].modules',
|
|
344
|
+
check: (definition, context) => {
|
|
345
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
346
|
+
const kind = context.workflowKind ?? body.type;
|
|
347
|
+
if (!kind)
|
|
348
|
+
return [];
|
|
349
|
+
return workflowSteps(body)
|
|
350
|
+
.filter(step => {
|
|
351
|
+
const allowedKinds = stepKindMatrix[String(step.type)];
|
|
352
|
+
return allowedKinds && !allowedKinds.includes(kind);
|
|
353
|
+
})
|
|
354
|
+
.map(step => ({
|
|
355
|
+
ruleId: 'WF-005',
|
|
356
|
+
severity: 'block',
|
|
357
|
+
message: `'${stepLabel(step)}' step'i (${step.type}) ${kind} tipinde kullanılamaz. İzin matrisi: variable/set-user/setcookie/redirect yalnız middleware; convert yalnız workflow; revert yalnız automation; response workflow+middleware.`,
|
|
358
|
+
guide: 'ngapps://guides/workflow'
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
id: 'WF-006',
|
|
364
|
+
module: 'workflow',
|
|
365
|
+
appliesTo: ['create', 'update'],
|
|
366
|
+
sourceRef: 'UI WORKFLOW_STEPS: terminal step\'lerde branches boş, diğerlerinde success/fail',
|
|
367
|
+
check: (definition, context) => {
|
|
368
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
369
|
+
const findings = [];
|
|
370
|
+
for (const step of workflowSteps(body)) {
|
|
371
|
+
const branches = asArray(step.branches);
|
|
372
|
+
if (terminalStepTypes.has(String(step.type)) && branches.length > 0) {
|
|
373
|
+
findings.push({
|
|
374
|
+
ruleId: 'WF-006',
|
|
375
|
+
severity: 'warn',
|
|
376
|
+
message: `'${stepLabel(step)}' (${step.type}) terminal bir step — branch taşımamalı.`,
|
|
377
|
+
guide: 'ngapps://guides/workflow'
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (!terminalStepTypes.has(String(step.type)) && step.type !== 'start' && branches.length === 0) {
|
|
381
|
+
findings.push({
|
|
382
|
+
ruleId: 'WF-006',
|
|
383
|
+
severity: 'warn',
|
|
384
|
+
message: `'${stepLabel(step)}' (${step.type}) step'inin hiç branch'i yok — success/fail branch'leri tanımlayıp nextStepId bağlayın, yoksa akış burada kopar.`,
|
|
385
|
+
guide: 'ngapps://guides/workflow'
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return findings;
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: 'WF-007',
|
|
394
|
+
module: 'workflow',
|
|
395
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
396
|
+
sourceRef: 'MS-001 kuralının workflow table step karşılığı; StepDto.rule.selectFields/joins. Yalnız okuma operasyonlarında — write step\'te platform selectFields\'i yok sayar, çıktı returnFields\'tır (TableMicroserviceExecutor.ByCreateAsync:215; klinikrandevu 2. tur H-002)',
|
|
397
|
+
check: (definition, context) => {
|
|
398
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
399
|
+
return workflowSteps(body)
|
|
400
|
+
.filter(step => step.type === 'table' && isReadOperation(step.rule))
|
|
401
|
+
.flatMap(step => tableSelectionFindings('WF-007', `'${stepLabel(step)}' table step'i (rule)`, step.rule, 'ngapps://guides/workflow')
|
|
402
|
+
.map(finding => ({ ...finding, severity: 'warn' })));
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
id: 'WF-008',
|
|
407
|
+
module: 'workflow',
|
|
408
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
409
|
+
sourceRef: 'KAPI testi; MS-002 kuralının step karşılığı',
|
|
410
|
+
check: (definition, context) => {
|
|
411
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
412
|
+
const queryStepNames = workflowSteps(body).filter(step => step.type === 'query').map(stepLabel);
|
|
413
|
+
return queryStepNames.length > 0
|
|
414
|
+
? [{
|
|
415
|
+
ruleId: 'WF-008',
|
|
416
|
+
severity: 'warn',
|
|
417
|
+
message: `Query step kullanılmış (${queryStepNames.join(', ')}): table step, query'nin yapabildiği neredeyse her şeyi platform-native yapar. Ham SQL şart değilse table step kullanın.`,
|
|
418
|
+
guide: 'ngapps://guides/workflow'
|
|
419
|
+
}]
|
|
420
|
+
: [];
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: 'WF-010',
|
|
425
|
+
module: 'workflow',
|
|
426
|
+
appliesTo: ['create', 'update'],
|
|
427
|
+
sourceRef: 'WorkflowRepository.FindAutomationsByEntityAsync: IsQueue==queued + Operation==type filtresi; null asla eşleşmez. state=unready automation için NORMALDİR (qm\'deki çalışan automation\'lar da unready; IsReady() hiçbir yerde çağrılmıyor)',
|
|
428
|
+
check: (definition, context) => {
|
|
429
|
+
if (context.workflowKind !== 'automation')
|
|
430
|
+
return [];
|
|
431
|
+
const automationSettings = (definition.automation ?? {});
|
|
432
|
+
const findings = [];
|
|
433
|
+
if (automationSettings.isQueue === undefined || automationSettings.isQueue === null) {
|
|
434
|
+
findings.push({
|
|
435
|
+
ruleId: 'WF-010',
|
|
436
|
+
severity: 'block',
|
|
437
|
+
message: "automation.isQueue verilmedi — tetikleme sorgusu IsQueue == false/true ile filtreler, null HİÇBİR ZAMAN eşleşmez ve automation sessizce ölü kalır. Anlık çalışma için isQueue: false, kuyruklu için true verin.",
|
|
438
|
+
guide: 'ngapps://guides/workflow'
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (!automationSettings.operation) {
|
|
442
|
+
findings.push({
|
|
443
|
+
ruleId: 'WF-010',
|
|
444
|
+
severity: 'block',
|
|
445
|
+
message: "automation.operation verilmedi (create | update | delete ...) — tetikleme sorgusu Operation üzerinden eşleşir, boş bırakılan automation tetiklenmez.",
|
|
446
|
+
guide: 'ngapps://guides/workflow'
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (definition.returnDtoId || automationSettings.returnDtoId) {
|
|
450
|
+
findings.push({
|
|
451
|
+
ruleId: 'WF-010',
|
|
452
|
+
severity: 'block',
|
|
453
|
+
message: "Automation'da returnDtoId YASAKTIR (platform validator: 'Return Dto Id' must be empty). state=unready görünmesi normaldir ve tetiklemeyi ETKİLEMEZ — returnDtoId atamaya çalışmayın.",
|
|
454
|
+
guide: 'ngapps://guides/workflow'
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return findings;
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
id: 'WF-011',
|
|
462
|
+
module: 'workflow',
|
|
463
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
464
|
+
sourceRef: "hr günlüğü #10/#14: step/branch id'leri gerçek GUID olmalı (System.Guid çevrimi) ve DB'de satır oldukları için global benzersiz",
|
|
465
|
+
check: (definition, context) => {
|
|
466
|
+
const guidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
467
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
468
|
+
const invalidIds = workflowSteps(body)
|
|
469
|
+
.flatMap(step => [step.id, ...asArray(step.branches).map(branch => branch?.id)])
|
|
470
|
+
.filter(id => id !== undefined && id !== null && !guidPattern.test(String(id)));
|
|
471
|
+
return invalidIds.length > 0
|
|
472
|
+
? [{
|
|
473
|
+
ruleId: 'WF-011',
|
|
474
|
+
severity: 'block',
|
|
475
|
+
message: `Step/branch id'leri gerçek GUID olmalı; geçersiz: ${[...new Set(invalidIds.map(String))].join(', ')}. Ayrıca id'ler veritabanında satırdır — başka bir akıştaki GUID'i tekrar kullanmak RowAlreadyExists üretir, her akışa taze GUID'ler verin.`,
|
|
476
|
+
guide: 'ngapps://guides/workflow'
|
|
477
|
+
}]
|
|
478
|
+
: [];
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: 'WF-012',
|
|
483
|
+
module: 'workflow',
|
|
484
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
485
|
+
sourceRef: "hr günlüğü #13: table step'te aynı alan hem fields hem selectFields'ta olamaz (duplicate key)",
|
|
486
|
+
check: (definition, context) => {
|
|
487
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
488
|
+
return workflowSteps(body)
|
|
489
|
+
.filter(step => step.type === 'table' && step.rule)
|
|
490
|
+
.flatMap(step => {
|
|
491
|
+
const writeSlugs = new Set(asArray(step.rule.fields)
|
|
492
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
493
|
+
.map(field => String(field.fieldSlug ?? field.slug ?? '')));
|
|
494
|
+
const overlapping = asArray(step.rule.selectFields)
|
|
495
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
496
|
+
.map(field => String(field.fieldSlug ?? field.slug ?? ''))
|
|
497
|
+
.filter(slug => slug.length > 0 && writeSlugs.has(slug));
|
|
498
|
+
return overlapping.length > 0
|
|
499
|
+
? [{
|
|
500
|
+
ruleId: 'WF-012',
|
|
501
|
+
severity: 'warn',
|
|
502
|
+
message: `'${stepLabel(step)}' table step'inde aynı alan hem fields (yazılacak) hem selectFields (okunacak) içinde: ${overlapping.join(', ')} — platform 'An item with the same key has already been added' üretir; alanı birinden çıkarın.`,
|
|
503
|
+
guide: 'ngapps://guides/workflow'
|
|
504
|
+
}]
|
|
505
|
+
: [];
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: 'MS-006',
|
|
511
|
+
module: 'microservice',
|
|
512
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
513
|
+
sourceRef: "hr günlüğü #8, düzeltilmiş: selectFields[].fieldId zorunlu (kök datasourceId iddiası YANLIŞTI — CreateMicroserviceCommand'da öyle bir property yok, STJ yok sayar); eksik fieldId'yi platform validator'ı artık 400 ile erken yakalar",
|
|
514
|
+
check: definition => {
|
|
515
|
+
if (definition.source !== 'table')
|
|
516
|
+
return [];
|
|
517
|
+
const findings = [];
|
|
518
|
+
const parameters = (definition.sourceParameters ?? {});
|
|
519
|
+
const missingFieldIds = asArray(parameters.selectFields)
|
|
520
|
+
.filter((entry) => Boolean(entry) && typeof entry === 'object')
|
|
521
|
+
.filter(entry => !entry.fieldId)
|
|
522
|
+
.map(entry => String(entry.fieldSlug ?? '?'));
|
|
523
|
+
if (missingFieldIds.length > 0) {
|
|
524
|
+
findings.push({
|
|
525
|
+
ruleId: 'MS-006',
|
|
526
|
+
severity: 'block',
|
|
527
|
+
message: `selectFields'ta fieldId eksik (yalnız fieldSlug yetmez): ${missingFieldIds.join(', ')}. Alan id'lerini entity_get view:'outline' çıktısından alın.`,
|
|
528
|
+
guide: 'ngapps://guides/microservice'
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
return findings;
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
id: 'AUT-001',
|
|
536
|
+
module: 'workflow',
|
|
537
|
+
appliesTo: ['create', 'update'],
|
|
538
|
+
sourceRef: 'ik-hrm teslim notu PLT-007 (hr\'de teyit): steps hem üst seviyede hem automation içinde verilmeli',
|
|
539
|
+
check: (definition, context) => {
|
|
540
|
+
if (context.workflowKind !== 'automation' || !definition.automation || typeof definition.automation !== 'object')
|
|
541
|
+
return [];
|
|
542
|
+
const topLevelCount = asArray(definition.steps).length;
|
|
543
|
+
const nestedCount = asArray(definition.automation.steps).length;
|
|
544
|
+
if (nestedCount > 0 && topLevelCount === 0) {
|
|
545
|
+
return [{
|
|
546
|
+
ruleId: 'AUT-001',
|
|
547
|
+
severity: 'warn',
|
|
548
|
+
message: "steps yalnız 'automation' nesnesinin içinde — platform üst seviyedeki steps'i okur. steps dizisini hem üst seviyeye hem automation içine koyun (PLT-007).",
|
|
549
|
+
guide: 'ngapps://guides/workflow'
|
|
550
|
+
}];
|
|
551
|
+
}
|
|
552
|
+
if (topLevelCount > 0 && nestedCount === 0) {
|
|
553
|
+
return [{
|
|
554
|
+
ruleId: 'AUT-001',
|
|
555
|
+
severity: 'warn',
|
|
556
|
+
message: "steps yalnız üst seviyede — aynı steps dizisini 'automation' nesnesinin içine de koyun (PLT-007: iki seviyede birden gerekir).",
|
|
557
|
+
guide: 'ngapps://guides/workflow'
|
|
558
|
+
}];
|
|
559
|
+
}
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: 'MS-004',
|
|
565
|
+
module: 'microservice',
|
|
566
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
567
|
+
sourceRef: 'ik-hrm teslim notu PLT-006: table source üst seviye entityId ister. View kaynağı MUAF (etkinlik turu, yanlış pozitif): MicroserviceService:519 view entity\'lerini ViewId üzerinden çözer, entityId beklemez — onun karşılığı viewId',
|
|
568
|
+
check: definition => {
|
|
569
|
+
if (definition.source === 'table' && !definition.entityId) {
|
|
570
|
+
return [{
|
|
571
|
+
ruleId: 'MS-004',
|
|
572
|
+
severity: 'warn',
|
|
573
|
+
message: "source='table' için üst seviye 'entityId' de zorunludur (sourceParameters.entityId tek başına yetmez).",
|
|
574
|
+
guide: 'ngapps://guides/microservice'
|
|
575
|
+
}];
|
|
576
|
+
}
|
|
577
|
+
if (definition.source === 'view' && !definition.viewId) {
|
|
578
|
+
return [{
|
|
579
|
+
ruleId: 'MS-004',
|
|
580
|
+
severity: 'warn',
|
|
581
|
+
message: "source='view' ucu entity'leri VIEW'den çözer (MicroserviceService:519 `viewService.GetAsync(microservice.ViewId)` → view.Tables); üst seviye 'entityId' değil **viewId** zorunludur.",
|
|
582
|
+
guide: 'ngapps://guides/microservice'
|
|
583
|
+
}];
|
|
584
|
+
}
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
id: 'MS-005',
|
|
590
|
+
module: 'microservice',
|
|
591
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
592
|
+
sourceRef: 'PostgreSqlRelationHandler.HandleSelect: alias\'sız join/select TryAdd(null)/GetValueOrDefault(null) ile 500 "null key" üretir; yalnız table source (view alias düzeni farklı, test edilmedi)',
|
|
593
|
+
check: definition => {
|
|
594
|
+
if (definition.source !== 'table')
|
|
595
|
+
return [];
|
|
596
|
+
const parameters = definition.sourceParameters;
|
|
597
|
+
if (!parameters)
|
|
598
|
+
return [];
|
|
599
|
+
const findings = [];
|
|
600
|
+
const flattenJoins = (joins) => joins
|
|
601
|
+
.filter((join) => Boolean(join) && typeof join === 'object')
|
|
602
|
+
.flatMap(join => [join, ...flattenJoins(asArray(join.joins))]);
|
|
603
|
+
const allJoins = flattenJoins(asArray(parameters.joins));
|
|
604
|
+
for (const join of allJoins) {
|
|
605
|
+
const missing = ['alias', 'entityId', 'fieldId'].filter(key => !join[key]);
|
|
606
|
+
if (missing.length > 0) {
|
|
607
|
+
findings.push({
|
|
608
|
+
ruleId: 'MS-005',
|
|
609
|
+
severity: 'block',
|
|
610
|
+
message: `Join tanımında zorunlu alan eksik (${missing.join(', ')}): ${JSON.stringify({ alias: join.alias, path: join.path })}. Alias'sız join platformda 500 'null key' (ArgumentNullException) üretir — bug değil, doğrulanmayan girdi.`,
|
|
611
|
+
guide: 'ngapps://guides/microservice'
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const joinAliases = allJoins.map(join => String(join.alias ?? '')).filter(alias => alias.length > 0);
|
|
616
|
+
if (new Set(joinAliases).size !== joinAliases.length) {
|
|
617
|
+
findings.push({
|
|
618
|
+
ruleId: 'MS-005',
|
|
619
|
+
severity: 'block',
|
|
620
|
+
message: `Join alias'ları benzersiz olmalı: ${joinAliases.join(', ')}. Aynı alias sessizce ilk join'e eşlenir (TryAdd) ve diğer join'in alanları yanlış tabloya bağlanır — hata bile alınmaz.`,
|
|
621
|
+
guide: 'ngapps://guides/microservice'
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
const validAliases = new Set([String(parameters.alias ?? ''), ...joinAliases].filter(alias => alias.length > 0));
|
|
625
|
+
for (const select of asArray(parameters.selectFields).filter((entry) => Boolean(entry) && typeof entry === 'object')) {
|
|
626
|
+
const selectAlias = String(select.alias ?? '');
|
|
627
|
+
if (selectAlias.length === 0) {
|
|
628
|
+
findings.push({
|
|
629
|
+
ruleId: 'MS-005',
|
|
630
|
+
severity: 'block',
|
|
631
|
+
message: `selectFields[${select.fieldSlug ?? select.fieldId ?? '?'}] alias'sız — ana tablo alanları için alias = entity slug'ı (${parameters.alias ?? '<sourceParameters.alias>'}) ve path: '' kullanın.`,
|
|
632
|
+
guide: 'ngapps://guides/microservice'
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
else if (validAliases.size > 0 && !validAliases.has(selectAlias)) {
|
|
636
|
+
findings.push({
|
|
637
|
+
ruleId: 'MS-005',
|
|
638
|
+
severity: 'block',
|
|
639
|
+
message: `selectFields[${select.fieldSlug ?? select.fieldId ?? '?'}] alias'ı '${selectAlias}' tanımlı değil — geçerli alias'lar: ${[...validAliases].join(', ')}. Yanlış alias SQL'de 'missing FROM-clause entry' üretir.`,
|
|
640
|
+
guide: 'ngapps://guides/microservice'
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return findings;
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
id: 'ENT-001',
|
|
649
|
+
module: 'entity',
|
|
650
|
+
appliesTo: ['create', 'update'],
|
|
651
|
+
sourceRef: "hr günlüğü #7 (DB doğrulaması: 57 ilişki alanı): doğru şekil dbType 'RELATION' + commonType 'relation' + relationId; relation alt nesnesini platform kendisi doldurur. FILE alanları kapsam DIŞI: relationId verilmez, Files entity'sini platform bağlar (klinik K-007)",
|
|
652
|
+
check: definition => asArray(definition.fields)
|
|
653
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
654
|
+
.flatMap(field => {
|
|
655
|
+
const relationType = String(field.relationType ?? 'none');
|
|
656
|
+
if (relationType === 'none' || relationType === 'None')
|
|
657
|
+
return [];
|
|
658
|
+
if (isFileField(field))
|
|
659
|
+
return [];
|
|
660
|
+
if (isMultiEnumField(field))
|
|
661
|
+
return [];
|
|
662
|
+
const findings = [];
|
|
663
|
+
if (!field.relationId) {
|
|
664
|
+
findings.push({
|
|
665
|
+
ruleId: 'ENT-001',
|
|
666
|
+
severity: 'warn',
|
|
667
|
+
message: `'${field.name ?? field.slug ?? '?'}' alanında relationType='${field.relationType}' ama relationId boş — hedef entity'nin id'sini verin.`,
|
|
668
|
+
guide: 'ngapps://guides/entity'
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
if (field.dbType !== undefined && field.dbType !== 'RELATION') {
|
|
672
|
+
findings.push({
|
|
673
|
+
ruleId: 'ENT-001',
|
|
674
|
+
severity: 'warn',
|
|
675
|
+
message: `'${field.name ?? field.slug ?? '?'}' ilişki alanında dbType='${field.dbType}' — ilişki alanının doğru şekli dbType: 'RELATION' + commonType: 'relation'; relation alt nesnesini (primaryKeyId vb.) platform kendisi doldurur, elle vermeyin.`,
|
|
676
|
+
guide: 'ngapps://guides/entity'
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
if (field.commonType !== undefined && field.commonType !== 'relation') {
|
|
680
|
+
findings.push({
|
|
681
|
+
ruleId: 'ENT-001',
|
|
682
|
+
severity: 'warn',
|
|
683
|
+
message: `'${field.name ?? field.slug ?? '?'}' ilişki alanında commonType='${field.commonType}' — 'relation' olmalı.`,
|
|
684
|
+
guide: 'ngapps://guides/entity'
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
return findings;
|
|
688
|
+
})
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
id: 'ENT-002',
|
|
692
|
+
module: 'entity',
|
|
693
|
+
appliesTo: ['create'],
|
|
694
|
+
sourceRef: 'sms oturumu (2026-08-15) — eski PLT-004 iddiası ÇÜRÜTÜLDÜ: bağsız entity\'de row_create/row_list sorunsuz çalışıyor (16 satırlık bağsız grades tablosunda doğrulandı). Bağın gerçek etkisi: EntityRepository.cs:167 entity_list applicationId filtresi (Studio\'nun application entity listesi) ve MicroserviceRepository.cs:556 bağımlılık sorgusu. Auto page ise EntityApplications\'a DEĞİL PageSettings\'e bakar (UpdatePageWhenEntityUpdatedEventHandler.cs:42) — sms\'te 7 bağsız entity, 0 page_settings, 0 auto page',
|
|
695
|
+
check: definition => asArray(definition.applicationIds).length === 0
|
|
696
|
+
? [{
|
|
697
|
+
ruleId: 'ENT-002',
|
|
698
|
+
severity: 'warn',
|
|
699
|
+
message: "applicationIds boş: satır uçları bundan ETKİLENMEZ (bağsız entity'de row_create/row_list çalışır), ama entity Studio'nun application bazlı entity listesinde görünmez. Studio'dan yönetilecekse applicationIds: ['<application id>'] verin; yalnız API'den kullanılacaksa boş bırakmak geçerli bir tercihtir.",
|
|
700
|
+
guide: 'ngapps://guides/entity'
|
|
701
|
+
}]
|
|
702
|
+
: []
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
id: 'ENT-003',
|
|
706
|
+
module: 'entity',
|
|
707
|
+
appliesTo: ['create', 'update'],
|
|
708
|
+
sourceRef: "hr günlüğü #2-4 (probe ile saptandı, DB doğrulandı): tip listesi BÜYÜK HARF ve ondalık tip yok. 'Incremental' PostgreSqlConnector.cs'te ayrı bir DataTypeGroup'tur (fiziksel tip TEXT), destek masası koşumu B-10'da yanlış pozitif verdi",
|
|
709
|
+
check: definition => {
|
|
710
|
+
const allowedDbTypes = new Set(['UUID', 'VARCHAR', 'TEXT', 'BOOL', 'INT2', 'INT4', 'INT8', 'DATE', 'TIME', 'TIMESTAMP', 'TIMESTAMPTZ', 'JSONB', 'FILE', 'RELATION', 'Incremental']);
|
|
711
|
+
const findings = [];
|
|
712
|
+
for (const field of asArray(definition.fields).filter((entry) => Boolean(entry) && typeof entry === 'object')) {
|
|
713
|
+
if (field.dbType === undefined)
|
|
714
|
+
continue;
|
|
715
|
+
const dbType = String(field.dbType);
|
|
716
|
+
if (allowedDbTypes.has(dbType))
|
|
717
|
+
continue;
|
|
718
|
+
if (dbType.toLowerCase() === 'incremental') {
|
|
719
|
+
findings.push({
|
|
720
|
+
ruleId: 'ENT-003',
|
|
721
|
+
severity: 'warn',
|
|
722
|
+
message: `'${field.slug ?? field.name}' artan kod alanının dbType'ı tam olarak 'Incremental' yazılır (tek büyük harf), commonType 'incremental' olur.`,
|
|
723
|
+
guide: 'ngapps://guides/entity'
|
|
724
|
+
});
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (allowedDbTypes.has(dbType.toUpperCase())) {
|
|
728
|
+
findings.push({
|
|
729
|
+
ruleId: 'ENT-003',
|
|
730
|
+
severity: 'warn',
|
|
731
|
+
message: `'${field.slug ?? field.name}' dbType='${dbType}' — kontrol büyük/küçük harfe duyarlı, '${dbType.toUpperCase()}' yazın.`,
|
|
732
|
+
guide: 'ngapps://guides/entity'
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
findings.push({
|
|
737
|
+
ruleId: 'ENT-003',
|
|
738
|
+
severity: 'warn',
|
|
739
|
+
message: `'${field.slug ?? field.name}' dbType='${dbType}' desteklenmiyor. Geçerli tipler (BÜYÜK HARF): UUID, VARCHAR, TEXT, BOOL, INT2, INT4, INT8, DATE, TIME, TIMESTAMP, TIMESTAMPTZ, JSONB, FILE, RELATION — ayrıca sıralı insan-okuyabilir kod için 'Incremental' (commonType 'incremental'). Ondalık tip YOK (NUMERIC/FLOAT/MONEY/DECIMAL reddedilir) — para için INT8+kuruş, yüzde için INT4×100 gibi birim konvansiyonu kullanın ve birimi description'a yazın.`,
|
|
740
|
+
guide: 'ngapps://guides/entity'
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return findings;
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
id: 'ENT-005',
|
|
749
|
+
module: 'entity',
|
|
750
|
+
appliesTo: ['create', 'update'],
|
|
751
|
+
sourceRef: "hr günlüğü #2: commonType olmadan UUID/BOOL çözümlenemiyor; eşleme qm projesinden çıkarıldı. TEXT+incremental çifti destek masası koşumu B-10'da yanlış pozitif verdi: platform kaydettikten sonra dbType'ı TEXT'e normalize eder, commonType 'incremental' kalır",
|
|
752
|
+
check: definition => {
|
|
753
|
+
const commonTypeByDbType = {
|
|
754
|
+
UUID: 'guid', VARCHAR: 'text', TEXT: 'text', BOOL: 'bool',
|
|
755
|
+
INT2: 'number', INT4: 'number', INT8: 'number',
|
|
756
|
+
DATE: 'date', TIME: 'time', TIMESTAMP: 'datetime', TIMESTAMPTZ: 'datetime',
|
|
757
|
+
JSONB: 'custom', FILE: 'file', RELATION: 'relation', Incremental: 'incremental'
|
|
758
|
+
};
|
|
759
|
+
const findings = [];
|
|
760
|
+
for (const field of asArray(definition.fields).filter((entry) => Boolean(entry) && typeof entry === 'object')) {
|
|
761
|
+
const dbType = String(field.dbType ?? '');
|
|
762
|
+
const expected = commonTypeByDbType[dbType];
|
|
763
|
+
if (!expected)
|
|
764
|
+
continue;
|
|
765
|
+
if (field.commonType === 'incremental' && (dbType === 'TEXT' || dbType === 'VARCHAR' || dbType === 'Incremental'))
|
|
766
|
+
continue;
|
|
767
|
+
if (field.commonType === undefined) {
|
|
768
|
+
findings.push({
|
|
769
|
+
ruleId: 'ENT-005',
|
|
770
|
+
severity: 'warn',
|
|
771
|
+
message: `'${field.slug ?? field.name}' alanında commonType eksik — dbType '${field.dbType}' için commonType: '${expected}' verin (commonType'sız bazı tipler 'isn't a supported database type' hatasıyla reddedilir).`,
|
|
772
|
+
guide: 'ngapps://guides/entity'
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
else if (field.commonType !== expected) {
|
|
776
|
+
findings.push({
|
|
777
|
+
ruleId: 'ENT-005',
|
|
778
|
+
severity: 'warn',
|
|
779
|
+
message: `'${field.slug ?? field.name}' alanında commonType='${field.commonType}' — dbType '${field.dbType}' için beklenen: '${expected}'.`,
|
|
780
|
+
guide: 'ngapps://guides/entity'
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return findings;
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
id: 'ENT-009',
|
|
789
|
+
module: 'entity',
|
|
790
|
+
appliesTo: ['create'],
|
|
791
|
+
sourceRef: "SeedData.AddEntities her projeye users/roles/user_roles/permissions/role_permissions entity'lerini SABİT id'lerle açar (Defaults.UserEntityId = 01917ede-330e-7254-8544-eef0db3693af); users gerçek bir datastore entity'sidir (altın örnek middleware-authenticate onu okur) ama entity_list applicationId ile çağrılınca görünmez — seed EntityApplication kaydı açmaz. Bilgi amaçlı warn: karar ajana ait",
|
|
792
|
+
check: definition => {
|
|
793
|
+
const fields = asArray(definition.fields).filter((entry) => Boolean(entry) && typeof entry === 'object');
|
|
794
|
+
const slugs = fields.map(field => String(field.slug ?? field.name ?? '').toLowerCase());
|
|
795
|
+
const hasIdentifier = slugs.some(slug => /^(e[_-]?mail|email|kullanici_adi|username|user_name)$/.test(slug));
|
|
796
|
+
const hasSecret = slugs.some(slug => /^(password|parola|sifre|sifre_hash|password_hash|passwordhash)$/.test(slug));
|
|
797
|
+
if (!hasIdentifier || !hasSecret)
|
|
798
|
+
return [];
|
|
799
|
+
return [{
|
|
800
|
+
ruleId: 'ENT-009',
|
|
801
|
+
severity: 'warn',
|
|
802
|
+
message: `'${definition.slug ?? definition.name ?? '?'}' entity'sinde hem kimlik (${slugs.filter(slug => /mail|user_?name|kullanici_adi/.test(slug)).join(', ')}) hem parola alanı var — İKİNCİ bir kimlik sistemi kuruyor olabilirsiniz. Platform her projeye hazır bir 'users' entity'si açar (sabit id 01917ede-330e-7254-8544-eef0db3693af; id/email/password/first_name/last_name, kendi DTO'su, auth middleware'inin okuduğu tablo) ve entity_list'i applicationId ile çağırdığınız için listede GÖRMEMİŞ olabilirsiniz — applicationId'siz çağırıp bakın. Kimlik alanlarını entity_patch ile users'a ekleyip domain verisini ayrı entity'de tutmak, roller/middlewareIds yetkilendirmesiyle konuşan tek bir kimlik bırakır. Bilerek ayrı bir kimlik kuruyorsanız (dış sistemle eşleşen hesaplar, users'a dokunulamayan senaryo) bu uyarıyı geçin. Ayrıntı: platform_guide('entity') §'Hazır entity'ler'.`,
|
|
803
|
+
guide: 'ngapps://guides/entity'
|
|
804
|
+
}];
|
|
805
|
+
}
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
id: 'ENT-004',
|
|
809
|
+
module: 'entity',
|
|
810
|
+
appliesTo: ['create', 'update'],
|
|
811
|
+
sourceRef: 'ik-hrm teslim notu PLT-002: display alanı tekil',
|
|
812
|
+
check: definition => {
|
|
813
|
+
const displayFields = asArray(definition.fields)
|
|
814
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
815
|
+
.filter(field => field.display === true);
|
|
816
|
+
return displayFields.length > 1
|
|
817
|
+
? [{
|
|
818
|
+
ruleId: 'ENT-004',
|
|
819
|
+
severity: 'warn',
|
|
820
|
+
message: `Birden çok display alanı var (${displayFields.map(field => field.slug ?? field.name).join(', ')}) — entity başına yalnız BİR display alanı olabilir.`,
|
|
821
|
+
guide: 'ngapps://guides/entity'
|
|
822
|
+
}]
|
|
823
|
+
: [];
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
id: 'MS-007',
|
|
828
|
+
module: 'microservice',
|
|
829
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
830
|
+
sourceRef: 'sms oturumu: MicroserviceFormTableRelations.svelte seçili join alanlarını `item.path === getPath(relationFieldId)` ile bulur; MicroserviceFormTableFiltersItem join path\'inin ilk parçasını (kök entityId) atar. path boşken runtime ÇALIŞIR ama Studio hiçbir join alanını seçili gösteremez',
|
|
831
|
+
check: definition => {
|
|
832
|
+
if (definition.source !== 'table')
|
|
833
|
+
return [];
|
|
834
|
+
const parameters = definition.sourceParameters;
|
|
835
|
+
if (!parameters)
|
|
836
|
+
return [];
|
|
837
|
+
const rootEntityId = String(parameters.entityId ?? definition.entityId ?? '');
|
|
838
|
+
const findings = [];
|
|
839
|
+
const joinPaths = new Map();
|
|
840
|
+
const walk = (joins, chain) => {
|
|
841
|
+
for (const join of joins) {
|
|
842
|
+
if (!join || typeof join !== 'object')
|
|
843
|
+
continue;
|
|
844
|
+
const entry = join;
|
|
845
|
+
const alias = String(entry.alias ?? '');
|
|
846
|
+
const nextChain = [...chain, String(entry.fieldId ?? '')];
|
|
847
|
+
const expected = [rootEntityId, ...nextChain].join('.');
|
|
848
|
+
if (!entry.path) {
|
|
849
|
+
findings.push({
|
|
850
|
+
ruleId: 'MS-007',
|
|
851
|
+
severity: 'block',
|
|
852
|
+
message: `'${alias}' join'inde 'path' boş. Beklenen: '${expected}' (kök entityId + ilişki alanı zinciri). Boş path'te sorgu çalışır ama Studio bu join'in alanlarını seçili gösteremez.`,
|
|
853
|
+
guide: 'ngapps://guides/microservice'
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
else if (String(entry.path) !== expected) {
|
|
857
|
+
findings.push({
|
|
858
|
+
ruleId: 'MS-007',
|
|
859
|
+
severity: 'block',
|
|
860
|
+
message: `'${alias}' join'inin path'i beklenenden farklı: '${entry.path}' ≠ '${expected}'.`,
|
|
861
|
+
guide: 'ngapps://guides/microservice'
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
if (alias)
|
|
865
|
+
joinPaths.set(alias, nextChain.join('.'));
|
|
866
|
+
walk(asArray(entry.joins), nextChain);
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
walk(asArray(parameters.joins), []);
|
|
870
|
+
if (joinPaths.size === 0)
|
|
871
|
+
return findings;
|
|
872
|
+
for (const list of [asArray(parameters.selectFields), asArray(parameters.returnFields)]) {
|
|
873
|
+
for (const entry of list) {
|
|
874
|
+
if (!entry || typeof entry !== 'object')
|
|
875
|
+
continue;
|
|
876
|
+
const select = entry;
|
|
877
|
+
const alias = String(select.alias ?? '');
|
|
878
|
+
const expected = joinPaths.get(alias);
|
|
879
|
+
if (expected === undefined)
|
|
880
|
+
continue;
|
|
881
|
+
if (String(select.path ?? '') !== expected) {
|
|
882
|
+
findings.push({
|
|
883
|
+
ruleId: 'MS-007',
|
|
884
|
+
severity: 'block',
|
|
885
|
+
message: `'${alias}' alias'lı seçili alanın ('${select.fieldSlug ?? select.fieldId}') path'i '${select.path ?? ''}' — join'li alanlarda path kök entityId OLMADAN ilişki alanı zinciri olmalı: '${expected}'.`,
|
|
886
|
+
guide: 'ngapps://guides/microservice'
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return findings;
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
id: 'MS-008',
|
|
896
|
+
module: 'microservice',
|
|
897
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
898
|
+
sourceRef: 'sms oturumu: MicroserviceFormTableSortContent.svelte data.sort.name / data.sort.sortValue değerlerini guard\'sız okur; sort null olan kayıt Studio sayfasını TypeError ile komple düşürür',
|
|
899
|
+
check: definition => {
|
|
900
|
+
if (definition.source !== 'table')
|
|
901
|
+
return [];
|
|
902
|
+
const parameters = definition.sourceParameters;
|
|
903
|
+
if (!parameters)
|
|
904
|
+
return [];
|
|
905
|
+
const missing = ['sort', 'limit'].filter(key => !parameters[key]);
|
|
906
|
+
return missing.length > 0
|
|
907
|
+
? [{
|
|
908
|
+
ruleId: 'MS-008',
|
|
909
|
+
severity: 'block',
|
|
910
|
+
message: `sourceParameters.${missing.join(' ve ')} eksik. Platform null bırakır, Studio formu guard'sız okuyup sayfayı düşürür. Varsayılanları verin: sort { name:'sortField', type:'sortType', source:'query-string' }, limit { name:'limit', offset:'offset', source:'query-string' }.`,
|
|
911
|
+
guide: 'ngapps://guides/microservice'
|
|
912
|
+
}]
|
|
913
|
+
: [];
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
id: 'MS-010',
|
|
918
|
+
module: 'microservice',
|
|
919
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
920
|
+
sourceRef: "CreateMicroserviceCommandValidator:76-86 — EnableDynamicFilters true iken Source=Table, Operation=Get, IsSingle=false ve TÜM single filtrelerin Source'u Expression olmak zorunda (PrefilterMustBeExpression). Runtime tarafı: TableMicroserviceExecutor:166 DynamicFilter.MapToFilters + Combine",
|
|
921
|
+
check: definition => {
|
|
922
|
+
if (!definition.enableDynamicFilters)
|
|
923
|
+
return [];
|
|
924
|
+
const findings = [];
|
|
925
|
+
const parameters = (definition.sourceParameters ?? {});
|
|
926
|
+
const flattenFilters = (filters) => filters
|
|
927
|
+
.filter((filter) => Boolean(filter) && typeof filter === 'object')
|
|
928
|
+
.flatMap(filter => [filter, ...flattenFilters(asArray(filter.children))]);
|
|
929
|
+
if (definition.source !== 'table' || parameters.operation !== 'get' || parameters.isSingle === true) {
|
|
930
|
+
findings.push({
|
|
931
|
+
ruleId: 'MS-010',
|
|
932
|
+
severity: 'block',
|
|
933
|
+
message: "enableDynamicFilters yalnız source:'table' + operation:'get' + isSingle:false uçlarda kullanılabilir (validator reddeder).",
|
|
934
|
+
guide: 'ngapps://guides/microservice'
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
const staticFilters = flattenFilters(asArray(parameters.filters))
|
|
938
|
+
.filter(filter => filter.filterType === 'single' && filter.source !== 'expression')
|
|
939
|
+
.map(filter => String(filter.fieldSlug ?? filter.fieldId ?? '?'));
|
|
940
|
+
if (staticFilters.length > 0) {
|
|
941
|
+
findings.push({
|
|
942
|
+
ruleId: 'MS-010',
|
|
943
|
+
severity: 'block',
|
|
944
|
+
message: `enableDynamicFilters açıkken single filtrelerin hepsi source:'expression' olmalı; şu filtreler değil: ${staticFilters.join(', ')}. Platform 'PrefilterMustBeExpression' ile reddeder. Dinamik filtre uçlarında query-string filtresi YOKTUR — süzme isteği çağrıdaki 'filters' parametresiyle gelir; expression filtreleri yalnız sabit prefilter (ör. tenant/aktiflik) içindir.`,
|
|
945
|
+
guide: 'ngapps://guides/microservice'
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
return findings;
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
id: 'MS-011',
|
|
953
|
+
module: 'microservice',
|
|
954
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
955
|
+
sourceRef: "Middleware değişken akışı: ExecuteMicroserviceBehavior.HandleMiddlewaresAsync set-user/variable step'leriyle workflowContext.AddVariable; expression filtreleri aynı Jint motorundan okur (TableMicroserviceExecutor:449-515). qm kanıtı: sy-users-get-my-profile id eq user.id + Authenticate middlewareIds. middlewareIds boşken user.* runtime'da 'user is not defined' 500'ü verir",
|
|
956
|
+
check: definition => {
|
|
957
|
+
const middlewareIds = asArray(definition.middlewareIds);
|
|
958
|
+
if (middlewareIds.length > 0)
|
|
959
|
+
return [];
|
|
960
|
+
const parameters = (definition.sourceParameters ?? {});
|
|
961
|
+
const flattenFilters = (filters) => filters
|
|
962
|
+
.filter((filter) => Boolean(filter) && typeof filter === 'object')
|
|
963
|
+
.flatMap(filter => [filter, ...flattenFilters(asArray(filter.children))]);
|
|
964
|
+
const expressionEntries = [
|
|
965
|
+
...flattenFilters(asArray(parameters.filters)),
|
|
966
|
+
...asArray(parameters.fields).filter((field) => Boolean(field) && typeof field === 'object')
|
|
967
|
+
].filter(entry => String(entry.source ?? '') === 'expression' && /\buser\./.test(String(entry.value ?? '')));
|
|
968
|
+
return expressionEntries.length > 0
|
|
969
|
+
? [{
|
|
970
|
+
ruleId: 'MS-011',
|
|
971
|
+
severity: 'warn',
|
|
972
|
+
message: `Expression'lar 'user.' okuyor (${expressionEntries.map(entry => String(entry.fieldSlug ?? entry.fieldId ?? '?')).join(', ')}) ama middlewareIds boş — 'user' değişkenini yalnız bir middleware'in set-user step'i kurar, bağlanmazsa runtime 'user is not defined' 500'ü döner. Auth middleware'ini definition.middlewareIds ile bağlayın (platform_guide('middleware')).`,
|
|
973
|
+
guide: 'ngapps://guides/middleware'
|
|
974
|
+
}]
|
|
975
|
+
: [];
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
id: 'MS-012',
|
|
980
|
+
module: 'microservice',
|
|
981
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
982
|
+
sourceRef: "EnumExtensions.cs:37-62 GetValueFromDescription unknown string'de null döner, StringEnumConverter<OperatorType> null'u default(Equal)='eq' yapar — kutuphane turu H4: operator:'like' sessizce eq'e düştü, arama ucu testten geçip çalışmadı. Sözlük: EntityConstants.cs OperatorType Description'ları",
|
|
983
|
+
check: definition => {
|
|
984
|
+
const parameters = (definition.sourceParameters ?? {});
|
|
985
|
+
return unknownOperatorFindings('MS-012', 'sourceParameters.filters', asArray(parameters.filters));
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
id: 'WF-015',
|
|
990
|
+
module: 'workflow',
|
|
991
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
992
|
+
sourceRef: "MS-012'nin workflow table step karşılığı — step rule.filters aynı OperatorType sözlüğünü kullanır",
|
|
993
|
+
check: (definition, context) => {
|
|
994
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
995
|
+
return workflowSteps(body)
|
|
996
|
+
.filter(step => step.type === 'table')
|
|
997
|
+
.flatMap(step => unknownOperatorFindings('WF-015', `'${stepLabel(step)}' table step'i (rule.filters)`, asArray((step.rule ?? {}).filters)));
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
id: 'WF-016',
|
|
1002
|
+
module: 'workflow',
|
|
1003
|
+
appliesTo: ['preview'],
|
|
1004
|
+
sourceRef: "PreviewWorkflowCommandHandler: jsEngine.Eval(null, request.PreviewPayload.ToString()) — alan JavaScript KAYNAK METNİdir, sonucu nesne değilse evaluatedInput.ToListDictionary() boş sözlük üretir ve preview 'input: {}' ile koşar. Destek masası koşumu B-09: hata mesajı adımı suçluyor ('Please provide the X parameter'), asıl sorun girdinin hiç bağlanmamış olması",
|
|
1005
|
+
check: definition => {
|
|
1006
|
+
const payload = definition.previewPayload;
|
|
1007
|
+
if (payload === undefined || payload === null)
|
|
1008
|
+
return [];
|
|
1009
|
+
if (typeof payload !== 'string') {
|
|
1010
|
+
return [{
|
|
1011
|
+
ruleId: 'WF-016',
|
|
1012
|
+
severity: 'block',
|
|
1013
|
+
message: `previewPayload bir ${Array.isArray(payload) ? 'dizi' : typeof payload} — STRING olmalı. Platform alanı jsEngine.Eval ile çalıştırır; nesne gönderilirse 'Unexpected end of input' ile 500 döner. Kanonik biçim: previewPayload: "({ email: 'a@b.c', password: '1234' })".`,
|
|
1014
|
+
guide: 'ngapps://guides/workflow'
|
|
1015
|
+
}];
|
|
1016
|
+
}
|
|
1017
|
+
const source = payload.trim();
|
|
1018
|
+
if (source.length === 0)
|
|
1019
|
+
return [];
|
|
1020
|
+
if (/^JSON\.stringify\s*\(/.test(source)) {
|
|
1021
|
+
return [{
|
|
1022
|
+
ruleId: 'WF-016',
|
|
1023
|
+
severity: 'block',
|
|
1024
|
+
message: `previewPayload metni "${source.slice(0, 40)}…" ile başlıyor: bu ifade değerlendirilince bir STRING üretir, nesne değil — preview sessizce input: {} ile koşar ve adımlar 'parametre eksik' hatası verir (hata mesajı adımı suçlar, asıl sorun budur). JSON.stringify çağıran taraftaki JS kodudur; gövdeye ÜRETTİĞİ metni koyun: previewPayload: "({ ... })".`,
|
|
1025
|
+
guide: 'ngapps://guides/workflow'
|
|
1026
|
+
}];
|
|
1027
|
+
}
|
|
1028
|
+
if (source.startsWith('{') && !source.startsWith('({')) {
|
|
1029
|
+
return [{
|
|
1030
|
+
ruleId: 'WF-016',
|
|
1031
|
+
severity: 'block',
|
|
1032
|
+
message: `previewPayload "{" ile başlıyor: JavaScript bunu nesne değil BLOK olarak ayrıştırır ve preview input: {} ile koşar. Parantez içine alın: previewPayload: "(${source})".`,
|
|
1033
|
+
guide: 'ngapps://guides/workflow'
|
|
1034
|
+
}];
|
|
1035
|
+
}
|
|
1036
|
+
return [];
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
id: 'WF-013',
|
|
1041
|
+
module: 'workflow',
|
|
1042
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1043
|
+
sourceRef: 'MS-008 kuralının workflow karşılığı; WorkflowTableFormSort isFlowStep dalında data.sort.sortValue okur',
|
|
1044
|
+
check: (definition, context) => {
|
|
1045
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
1046
|
+
return workflowSteps(body)
|
|
1047
|
+
.filter(step => step.type === 'table')
|
|
1048
|
+
.flatMap(step => {
|
|
1049
|
+
const rule = (step.rule ?? {});
|
|
1050
|
+
const missing = ['sort', 'limit'].filter(key => !rule[key]);
|
|
1051
|
+
return missing.length > 0
|
|
1052
|
+
? [{
|
|
1053
|
+
ruleId: 'WF-013',
|
|
1054
|
+
severity: 'block',
|
|
1055
|
+
message: `'${stepLabel(step)}' table step'inde rule.${missing.join(' ve ')} eksik — Studio'da workflow sayfası 'Cannot read properties of null (reading sortValue)' ile açılmaz.`,
|
|
1056
|
+
guide: 'ngapps://guides/workflow'
|
|
1057
|
+
}]
|
|
1058
|
+
: [];
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
id: 'ENT-006',
|
|
1064
|
+
module: 'entity',
|
|
1065
|
+
appliesTo: ['create', 'update'],
|
|
1066
|
+
sourceRef: 'sms oturumu: kullanıcının intprimarykey/guidprimarykey referans tabloları — PK identity + properties.generationType:0 (Always) ile otomatik üretilir; dbType↔commonType eşleşmesi zorunlu',
|
|
1067
|
+
check: definition => {
|
|
1068
|
+
const primaryKeys = asArray(definition.fields)
|
|
1069
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
1070
|
+
.filter(field => field.primaryKey === true);
|
|
1071
|
+
const findings = [];
|
|
1072
|
+
for (const key of primaryKeys) {
|
|
1073
|
+
const properties = (key.properties ?? {});
|
|
1074
|
+
const dbType = String(key.dbType ?? '');
|
|
1075
|
+
const expected = dbType === 'UUID' ? 'guid' : ['INT2', 'INT4', 'INT8'].includes(dbType) ? 'number' : null;
|
|
1076
|
+
if (key.identity !== true || properties.generationType === undefined) {
|
|
1077
|
+
findings.push({
|
|
1078
|
+
ruleId: 'ENT-006',
|
|
1079
|
+
severity: 'warn',
|
|
1080
|
+
message: `'${key.slug ?? key.name}' primary key'i otomatik üretilmiyor: identity:true + properties.generationType:0 (Always) verin, yoksa her satırda id'yi elle göndermek gerekir.`,
|
|
1081
|
+
guide: 'ngapps://guides/entity'
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
if (expected === null) {
|
|
1085
|
+
findings.push({
|
|
1086
|
+
ruleId: 'ENT-006',
|
|
1087
|
+
severity: 'warn',
|
|
1088
|
+
message: `'${key.slug ?? key.name}' primary key'i için dbType '${dbType}' beklenmiyor — UUID (guid) ya da INT2/INT4/INT8 (number) kullanın.`,
|
|
1089
|
+
guide: 'ngapps://guides/entity'
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
else if (key.commonType !== expected) {
|
|
1093
|
+
findings.push({
|
|
1094
|
+
ruleId: 'ENT-006',
|
|
1095
|
+
severity: 'warn',
|
|
1096
|
+
message: `'${key.slug ?? key.name}' primary key'inde dbType '${dbType}' ile commonType '${key.commonType}' uyuşmuyor — '${expected}' olmalı.`,
|
|
1097
|
+
guide: 'ngapps://guides/entity'
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return findings;
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
id: 'ENT-007',
|
|
1106
|
+
module: 'entity',
|
|
1107
|
+
appliesTo: ['create', 'update'],
|
|
1108
|
+
sourceRef: 'sms oturumu: enum alanı = valueOptions dolu + isEnum true; valueOptions PG CHECK constraint üretir, mevcut veri kanonik değilse migration düşer. Multi-enum (many-to-many + valueOptions) kapsam DIŞI: platform <slug>_values entity\'sini + junction\'ı kendisi üretir, isEnum aranmaz (allm2m canlı verisi)',
|
|
1109
|
+
check: definition => asArray(definition.fields)
|
|
1110
|
+
.filter((field) => Boolean(field) && typeof field === 'object')
|
|
1111
|
+
.flatMap(field => {
|
|
1112
|
+
const options = asArray(field.valueOptions);
|
|
1113
|
+
if (options.length > 0 && isJunctionField(field))
|
|
1114
|
+
return [];
|
|
1115
|
+
if (options.length > 0 && field.isEnum !== true) {
|
|
1116
|
+
return [{
|
|
1117
|
+
ruleId: 'ENT-007',
|
|
1118
|
+
severity: 'warn',
|
|
1119
|
+
message: `'${field.slug ?? field.name}' alanında valueOptions dolu ama isEnum true değil — ikisi birlikte verilmeli.`,
|
|
1120
|
+
guide: 'ngapps://guides/entity'
|
|
1121
|
+
}];
|
|
1122
|
+
}
|
|
1123
|
+
if (field.isEnum === true && options.length === 0) {
|
|
1124
|
+
return [{
|
|
1125
|
+
ruleId: 'ENT-007',
|
|
1126
|
+
severity: 'warn',
|
|
1127
|
+
message: `'${field.slug ?? field.name}' alanı isEnum:true ama valueOptions boş — [{ value, icon }] listesi verin.`,
|
|
1128
|
+
guide: 'ngapps://guides/entity'
|
|
1129
|
+
}];
|
|
1130
|
+
}
|
|
1131
|
+
const malformed = options.filter((option) => Boolean(option) && typeof option === 'object').filter(option => !option.value);
|
|
1132
|
+
return malformed.length > 0
|
|
1133
|
+
? [{
|
|
1134
|
+
ruleId: 'ENT-007',
|
|
1135
|
+
severity: 'warn',
|
|
1136
|
+
message: `'${field.slug ?? field.name}' alanının valueOptions öğelerinde 'value' eksik — { value: 'Aktif', icon: '#47ad10' } biçimi bekleniyor.`,
|
|
1137
|
+
guide: 'ngapps://guides/entity'
|
|
1138
|
+
}]
|
|
1139
|
+
: [];
|
|
1140
|
+
})
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
id: 'ENT-008',
|
|
1144
|
+
module: 'entity',
|
|
1145
|
+
appliesTo: ['create'],
|
|
1146
|
+
sourceRef: "klinik K-001: entity slug'ları environment şemasındaki ~73 platform tablosuyla aynı isim uzayında; çakışmada 'Migration failed … relation \"X\" already exists' döner ve entity kaydı HİÇ oluşmaz. Liste TenantContextModelSnapshot'tan üretilir (npm run generate-reserved-tables)",
|
|
1147
|
+
check: definition => {
|
|
1148
|
+
const slug = String(definition.slug ?? '').toLowerCase();
|
|
1149
|
+
if (!slug || !reservedTenantTables.includes(slug))
|
|
1150
|
+
return [];
|
|
1151
|
+
return [{
|
|
1152
|
+
ruleId: 'ENT-008',
|
|
1153
|
+
severity: 'block',
|
|
1154
|
+
message: `slug '${slug}' bir platform tablosunun adı — environment şeması bu tabloyu zaten taşıyor. Migration 'relation "${slug}" already exists' ile düşer ve entity kaydı hiç oluşmaz. Eş anlamlı bir ad seçin (ör. şube/branş için 'branches' yerine 'specialties').`,
|
|
1155
|
+
guide: 'ngapps://guides/entity'
|
|
1156
|
+
}];
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
id: 'MS-009',
|
|
1161
|
+
module: 'microservice',
|
|
1162
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1163
|
+
sourceRef: 'klinik K-003 + kod: TableMicroserviceExecutor.ByCreateAsync:215 sonucu FilterByKeys([PK]) ile buduyor; write operasyonunda selectFields kaydedilirken sessizce boşaltılır, dönecek alanlar returnFields ile verilir',
|
|
1164
|
+
check: definition => {
|
|
1165
|
+
const parameters = definition.sourceParameters;
|
|
1166
|
+
if (definition.source !== 'table' || !parameters || isReadOperation(parameters))
|
|
1167
|
+
return [];
|
|
1168
|
+
if (String(parameters.operation ?? '').toLowerCase() === 'delete')
|
|
1169
|
+
return [];
|
|
1170
|
+
return asArray(parameters.returnFields).length === 0
|
|
1171
|
+
? [{
|
|
1172
|
+
ruleId: 'MS-009',
|
|
1173
|
+
severity: 'warn',
|
|
1174
|
+
message: `sourceParameters.operation='${parameters.operation}' ama returnFields boş — yanıt yalnız primary key döner. Çağıranın göreceği alanları returnFields'a ekleyin (write operasyonunda selectFields yok sayılır).`,
|
|
1175
|
+
guide: 'ngapps://guides/microservice'
|
|
1176
|
+
}]
|
|
1177
|
+
: [];
|
|
1178
|
+
}
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
id: 'WF-014',
|
|
1182
|
+
module: 'workflow',
|
|
1183
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1184
|
+
sourceRef: "klinik K-010 + UI kodu: kenar sourceHandle = branch.name (workflow.helpers.ts:90-110), condition tutamağı items[].branchId (WorkflowCondition.svelte:141). Normalizasyon adı id'ye çeker; burada kalan uyuşmazlık gerçek hatadır",
|
|
1185
|
+
check: (definition, context) => {
|
|
1186
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
1187
|
+
return workflowSteps(body)
|
|
1188
|
+
.filter(step => step.type === 'condition')
|
|
1189
|
+
.flatMap(step => {
|
|
1190
|
+
const branchIds = new Set(asArray(step.branches)
|
|
1191
|
+
.filter((branch) => Boolean(branch) && typeof branch === 'object')
|
|
1192
|
+
.map(branch => String(branch.id ?? '')));
|
|
1193
|
+
const dangling = asArray((step.rule ?? {}).items)
|
|
1194
|
+
.filter((item) => Boolean(item) && typeof item === 'object')
|
|
1195
|
+
.map(item => String(item.branchId ?? ''))
|
|
1196
|
+
.filter(branchId => branchId.length > 0 && !branchIds.has(branchId));
|
|
1197
|
+
return dangling.length > 0
|
|
1198
|
+
? [{
|
|
1199
|
+
ruleId: 'WF-014',
|
|
1200
|
+
severity: 'block',
|
|
1201
|
+
message: `'${stepLabel(step)}' condition step'inde rule.items[].branchId '${dangling.join("', '")}' hiçbir branch'in id'siyle eşleşmiyor — koşul satırı hiçbir dala bağlı değil. branches[].id değerlerini kullanın.`,
|
|
1202
|
+
guide: 'ngapps://guides/workflow'
|
|
1203
|
+
}]
|
|
1204
|
+
: [];
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
id: 'FLOW-004',
|
|
1210
|
+
module: 'flow',
|
|
1211
|
+
appliesTo: ['create', 'update'],
|
|
1212
|
+
sourceRef: 'UI: Runtime.svelte:329 (branch.id = branch.name), start.ts:4 / setter.ts:258 engine.next("always"), flow-engine.ts:79 sessiz durma, EventActionsStart.svelte:47 (sourceBranch === "always")',
|
|
1213
|
+
check: definition => flowAllSteps(definition).flatMap(step => {
|
|
1214
|
+
const type = String(step.type ?? '');
|
|
1215
|
+
if (flowFreeBranchTypes.has(type))
|
|
1216
|
+
return [];
|
|
1217
|
+
const allowed = flowBranchNames[type] ?? ['always'];
|
|
1218
|
+
const wrong = flowBranches(step)
|
|
1219
|
+
.map(branch => String(branch.name ?? ''))
|
|
1220
|
+
.filter(name => !allowed.includes(name));
|
|
1221
|
+
if (wrong.length === 0)
|
|
1222
|
+
return [];
|
|
1223
|
+
return [{
|
|
1224
|
+
ruleId: 'FLOW-004',
|
|
1225
|
+
severity: 'block',
|
|
1226
|
+
message: `'${flowStepLabel(step)}' (type='${type}') adımının dal adı '${wrong.join("', '")}' geçersiz — ${allowed.length > 0 ? `beklenen: '${allowed.join("' | '")}'` : 'bu adım tipi dal taşımaz'}. Dal adı etiket değil motorun anahtarıdır: runtime branch.id'yi branch.name'den üretip engine.next('always') ile arar, bulamazsa akış SESSİZCE durur; Studio da zinciri sourceBranch === 'always' ile çizdiği için panel Start kartından sonrası BOŞ görünür.`,
|
|
1227
|
+
guide: 'ngapps://guides/page'
|
|
1228
|
+
}];
|
|
1229
|
+
})
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
id: 'FLOW-005',
|
|
1233
|
+
module: 'flow',
|
|
1234
|
+
appliesTo: ['create', 'update'],
|
|
1235
|
+
sourceRef: 'StepManagementSchema.ToDomain (allSteps.TryGetValue → eşleşmeyen nextStepId sessizce null); flow-engine.ts:115 start arama',
|
|
1236
|
+
check: definition => ['steps', 'errorSteps'].flatMap(field => {
|
|
1237
|
+
const steps = flowStepList(definition[field]);
|
|
1238
|
+
if (steps.length === 0)
|
|
1239
|
+
return [];
|
|
1240
|
+
const stepIds = new Set(steps.map(step => String(step.stepId ?? '')));
|
|
1241
|
+
const dangling = steps.flatMap(step => flowBranches(step)
|
|
1242
|
+
.map(branch => String(branch.nextStepId ?? ''))
|
|
1243
|
+
.filter(nextStepId => nextStepId.length > 0 && !stepIds.has(nextStepId))
|
|
1244
|
+
.map(nextStepId => `${flowStepLabel(step)} → '${nextStepId}'`));
|
|
1245
|
+
if (dangling.length > 0) {
|
|
1246
|
+
return [{
|
|
1247
|
+
ruleId: 'FLOW-005',
|
|
1248
|
+
severity: 'block',
|
|
1249
|
+
message: `${field} içinde hedefi olmayan dal var: ${dangling.join(', ')}. branches[].nextStepId hedef adımın stepId STRING'idir; eşleşmeyeni backend sessizce null'a çevirir ve akış orada biter.`,
|
|
1250
|
+
guide: 'ngapps://guides/page'
|
|
1251
|
+
}];
|
|
1252
|
+
}
|
|
1253
|
+
const startStep = steps.find(step => step.type === 'start');
|
|
1254
|
+
if (!startStep) {
|
|
1255
|
+
return [{
|
|
1256
|
+
ruleId: 'FLOW-005',
|
|
1257
|
+
severity: 'block',
|
|
1258
|
+
message: `${field} içinde type='start' adımı yok — motor akışı start adımından başlatır (flow-engine.ts:115), start yoksa hiçbir şey çalışmaz.`,
|
|
1259
|
+
guide: 'ngapps://guides/page'
|
|
1260
|
+
}];
|
|
1261
|
+
}
|
|
1262
|
+
const reached = new Set();
|
|
1263
|
+
const queue = [String(startStep.stepId ?? '')];
|
|
1264
|
+
const stepsById = new Map(steps.map(step => [String(step.stepId ?? ''), step]));
|
|
1265
|
+
while (queue.length > 0) {
|
|
1266
|
+
const currentId = queue.pop();
|
|
1267
|
+
if (!currentId || reached.has(currentId))
|
|
1268
|
+
continue;
|
|
1269
|
+
reached.add(currentId);
|
|
1270
|
+
for (const branch of flowBranches(stepsById.get(currentId) ?? {})) {
|
|
1271
|
+
if (branch.nextStepId)
|
|
1272
|
+
queue.push(String(branch.nextStepId));
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const unreachable = steps.filter(step => !reached.has(String(step.stepId ?? '')));
|
|
1276
|
+
return unreachable.length > 0
|
|
1277
|
+
? [{
|
|
1278
|
+
ruleId: 'FLOW-005',
|
|
1279
|
+
severity: 'block',
|
|
1280
|
+
message: `${field} içinde start'tan erişilemeyen adım var: ${unreachable.map(flowStepLabel).join(', ')}. Önceki adımın branches[].nextStepId alanına bu adımların stepId'sini yazın.`,
|
|
1281
|
+
guide: 'ngapps://guides/page'
|
|
1282
|
+
}]
|
|
1283
|
+
: [];
|
|
1284
|
+
})
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
id: 'FLOW-006',
|
|
1288
|
+
module: 'flow',
|
|
1289
|
+
appliesTo: ['create', 'update'],
|
|
1290
|
+
sourceRef: 'setter.ts:68 process() yalnız change|fill dallanır; setter.types.ts SetterArgumentIterateeType. Backend StepManagementIterate.Type serbest string olduğu için geçersiz değer 200 döner',
|
|
1291
|
+
check: definition => flowAllSteps(definition)
|
|
1292
|
+
.filter(step => step.type === 'setter')
|
|
1293
|
+
.flatMap(step => flowIteratees(step)
|
|
1294
|
+
.map(iteratee => String(iteratee.type ?? ''))
|
|
1295
|
+
.filter(type => !flowSetterIterateeTypes.has(type))
|
|
1296
|
+
.map(type => ({
|
|
1297
|
+
ruleId: 'FLOW-006',
|
|
1298
|
+
severity: 'block',
|
|
1299
|
+
message: `'${flowStepLabel(step)}' setter adımında iteratees[].type='${type}' geçersiz — yalnız 'change' (tek prop'a değer yaz) ve 'fill' (diziyi mapping ile eşleyerek yaz) vardır. Backend serbest string sakladığı için istek 200 döner ama setter.ts hiçbir dala girmez: adım çalışır, HİÇBİR ŞEY yapmaz.`,
|
|
1300
|
+
guide: 'ngapps://guides/page'
|
|
1301
|
+
})))
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
id: 'WF-018',
|
|
1305
|
+
module: 'workflow',
|
|
1306
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1307
|
+
sourceRef: "LoopStep.cs:41 çocuk kuralı rule.Children.FirstOrDefault() ile okur ama API sözleşmesinde alan TEKİLdir (StepRuleDto.StepRule, WorkflowDto.cs:498). 'children' gönderilirse Children boş kalır, çocuk adım null rule ile koşar ve loop NullReferenceException verir — hata arrayExpression'ı işaret ettiği için yanlış yere baktırır. Ölçüm: anket-admin/anket-kaydet, 2026-08-19",
|
|
1308
|
+
check: (definition, context) => {
|
|
1309
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
1310
|
+
return workflowSteps(body)
|
|
1311
|
+
.filter(step => step.type === 'loop')
|
|
1312
|
+
.flatMap((step) => {
|
|
1313
|
+
const rule = (step.rule ?? {});
|
|
1314
|
+
const findings = [];
|
|
1315
|
+
if (!rule.stepRule) {
|
|
1316
|
+
const hasChildren = Array.isArray(rule.children) && rule.children.length > 0;
|
|
1317
|
+
findings.push({
|
|
1318
|
+
ruleId: 'WF-018',
|
|
1319
|
+
severity: 'block',
|
|
1320
|
+
message: `'${stepLabel(step)}' loop step'inde rule.stepRule yok${hasChildren ? " — ama rule.children dolu: çocuk kural alanının adı 'children' DEĞİL 'stepRule'dur (tekil)" : ''}. Çocuksuz loop çalışma anında NullReferenceException verir ve hata mesajı arrayExpression'ı suçlar. Çocuğun kendi rule gövdesini rule.stepRule'a koyun; geçerli öğeye çocuk ifadelerinde 'loopItem' ile erişilir, loop'un çıktısı [{ item, result }] dizisidir.`,
|
|
1321
|
+
guide: 'ngapps://guides/workflow'
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
if (String(rule.arrayExpression ?? '').trim().length === 0) {
|
|
1325
|
+
findings.push({
|
|
1326
|
+
ruleId: 'WF-018',
|
|
1327
|
+
severity: 'block',
|
|
1328
|
+
message: `'${stepLabel(step)}' loop step'inde rule.arrayExpression boş — döngü neyi yineleyeceğini bilemez. Diziyi üreten JS ifadesini yazın (ör. "input.questions || []").`,
|
|
1329
|
+
guide: 'ngapps://guides/workflow'
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
return findings;
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
id: 'FLOW-017',
|
|
1338
|
+
module: 'flow',
|
|
1339
|
+
appliesTo: ['create', 'update'],
|
|
1340
|
+
sourceRef: "setter.ts:116 change() ve fill() her dalda machine.setNode(element, { [property]: … }) çağırır; element/property boşsa çağrı sessizce hiçbir düğüme yazmaz. Ölçüm: anket-spike/spike-public 'Anketi yukle' + 'Cevabi gonder' akışları, 2026-08-19. Platform bu alanları DOĞRULAMAZ (bilinçli ürün kararı: doğrulama platform çekirdeğine değil tool katmanına ait); tek savunma bu kuraldır",
|
|
1341
|
+
check: definition => flowAllSteps(definition)
|
|
1342
|
+
.filter(step => step.type === 'setter')
|
|
1343
|
+
.flatMap(step => flowIteratees(step).flatMap((iteratee) => {
|
|
1344
|
+
const missing = ['element', 'property'].filter(field => String(iteratee[field] ?? '').trim().length === 0);
|
|
1345
|
+
if (missing.length === 0)
|
|
1346
|
+
return [];
|
|
1347
|
+
return [{
|
|
1348
|
+
ruleId: 'FLOW-017',
|
|
1349
|
+
severity: 'block',
|
|
1350
|
+
message: `'${flowStepLabel(step)}' setter adımının iteratees girdisinde ${missing.join(' ve ')} yok. setter.ts her dalda machine.setNode(element, { [property]: value }) çağırır — ikisinden biri boşken adım ÇALIŞIR, hata vermez ve HİÇBİR ŞEY yazmaz. element = hedef komponentin elementId'si (page_schema_get ile doğrulayın), property = yazılacak prop adı (html komponentine veri basmak için 'context', tabloya satır basmak için 'rows'). Platform bu gövdeyi 200 ile kabul eder — tek savunma budur.`,
|
|
1351
|
+
guide: 'ngapps://guides/page'
|
|
1352
|
+
}];
|
|
1353
|
+
}))
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
id: 'FLOW-007',
|
|
1357
|
+
module: 'flow',
|
|
1358
|
+
appliesTo: ['create', 'update'],
|
|
1359
|
+
sourceRef: "setter.ts:161 Storage dalı iteratee.mapping.source'u guard'sız okur; mapping null ise TypeError try/catch'e düşer ve adım hata verir",
|
|
1360
|
+
check: definition => flowAllSteps(definition)
|
|
1361
|
+
.filter(step => step.type === 'setter')
|
|
1362
|
+
.flatMap(step => flowIteratees(step)
|
|
1363
|
+
.filter(iteratee => flowValueOf(iteratee).type === 'storage' && !iteratee.mapping)
|
|
1364
|
+
.map(() => ({
|
|
1365
|
+
ruleId: 'FLOW-007',
|
|
1366
|
+
severity: 'block',
|
|
1367
|
+
message: `'${flowStepLabel(step)}' setter adımında value.type='storage' ama mapping yok. setter.ts mapping.source'u guard'sız okur → TypeError. En az mapping: { "source": "", "mappings": {} } verin.`,
|
|
1368
|
+
guide: 'ngapps://guides/page'
|
|
1369
|
+
})))
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
id: 'FLOW-008',
|
|
1373
|
+
module: 'flow',
|
|
1374
|
+
appliesTo: ['create', 'update'],
|
|
1375
|
+
sourceRef: 'microservice.ts:89 machine.setStorage(task.name, …) — storage anahtarı adımın ADI. setter.ts:161 mapping.source doluysa anahtar odur. flow-execution.functions.ts: code sandbox\'ında sonuçlar Steps["<ad>"] ile okunur, stepId diye bir değişken yoktur',
|
|
1376
|
+
check: definition => {
|
|
1377
|
+
const steps = flowAllSteps(definition);
|
|
1378
|
+
const names = new Set(steps.map(step => String(step.name ?? '')).filter(name => name.length > 0));
|
|
1379
|
+
const stepIds = new Set(steps.map(step => String(step.stepId ?? '')).filter(stepId => stepId.length > 0));
|
|
1380
|
+
return steps
|
|
1381
|
+
.filter(step => step.type === 'setter')
|
|
1382
|
+
.flatMap(step => flowIteratees(step).flatMap((iteratee) => {
|
|
1383
|
+
const value = flowValueOf(iteratee);
|
|
1384
|
+
const isBareIdentifier = typeof value.ref === 'string' && /^[A-Za-z_$][\w$]*$/.test(value.ref);
|
|
1385
|
+
const isStorage = value.type === 'storage';
|
|
1386
|
+
if (!isStorage && !(value.type === 'code' && isBareIdentifier))
|
|
1387
|
+
return [];
|
|
1388
|
+
const { reference, origin } = isStorage
|
|
1389
|
+
? flowStorageReference(iteratee)
|
|
1390
|
+
: { reference: String(value.ref), origin: 'value.ref' };
|
|
1391
|
+
const root = flowStorageRoot(reference);
|
|
1392
|
+
if (root.length === 0 || names.has(root))
|
|
1393
|
+
return [];
|
|
1394
|
+
if (stepIds.has(root)) {
|
|
1395
|
+
const stepName = steps.find(candidate => String(candidate.stepId ?? '') === root)?.name ?? '<adım adı>';
|
|
1396
|
+
const fix = origin === 'mapping.source'
|
|
1397
|
+
? `mapping.source: "${stepName}.data"`
|
|
1398
|
+
: `{ "type": "storage", "ref": "${stepName}.data" }`;
|
|
1399
|
+
return [{
|
|
1400
|
+
ruleId: 'FLOW-008',
|
|
1401
|
+
severity: 'block',
|
|
1402
|
+
message: `'${flowStepLabel(step)}' setter adımı ${origin}='${reference}' ile bir stepId'ye bakıyor; storage anahtarı adımın ADIdır. Doğrusu: ${fix}. value.type='code' ise ifade JS olarak koşar ve '${reference}' tanımsız değişken hatası verir — kod içinden okuma biçimi Steps["<adım adı>"].data'dır.`,
|
|
1403
|
+
guide: 'ngapps://guides/page'
|
|
1404
|
+
}];
|
|
1405
|
+
}
|
|
1406
|
+
return isStorage
|
|
1407
|
+
? [{
|
|
1408
|
+
ruleId: 'FLOW-008',
|
|
1409
|
+
severity: 'warn',
|
|
1410
|
+
message: `'${flowStepLabel(step)}' setter adımı ${origin}='${reference}' ile storage '${root}' okuyor ama bu akışta bu adı taşıyan adım yok — anahtarı yazan adım başka bir akıştaysa yok sayın, değilse ref adım adına eşitlenmeli.`,
|
|
1411
|
+
guide: 'ngapps://guides/page'
|
|
1412
|
+
}]
|
|
1413
|
+
: [];
|
|
1414
|
+
}));
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
id: 'FLOW-009',
|
|
1419
|
+
module: 'flow',
|
|
1420
|
+
appliesTo: ['create', 'update'],
|
|
1421
|
+
sourceRef: 'microservice.ts:81-90 — isRaw false ise storage { data, headers, error } sarmalı, değilse ham response.data',
|
|
1422
|
+
check: definition => {
|
|
1423
|
+
const steps = flowAllSteps(definition);
|
|
1424
|
+
const microserviceStepsByName = new Map(steps
|
|
1425
|
+
.filter(step => step.type === 'microservice' && step.name)
|
|
1426
|
+
.map(step => [String(step.name), step]));
|
|
1427
|
+
return steps
|
|
1428
|
+
.filter(step => step.type === 'setter')
|
|
1429
|
+
.flatMap(step => flowIteratees(step).flatMap(iteratee => {
|
|
1430
|
+
const value = flowValueOf(iteratee);
|
|
1431
|
+
if (value.type !== 'storage')
|
|
1432
|
+
return [];
|
|
1433
|
+
const { reference, origin } = flowStorageReference(iteratee);
|
|
1434
|
+
if (reference.length === 0)
|
|
1435
|
+
return [];
|
|
1436
|
+
const segments = reference.split('.');
|
|
1437
|
+
const root = flowStorageRoot(reference);
|
|
1438
|
+
const source = microserviceStepsByName.get(root);
|
|
1439
|
+
if (!source)
|
|
1440
|
+
return [];
|
|
1441
|
+
const isRaw = (source.args ?? {}).isRaw === true;
|
|
1442
|
+
const readsWrapper = segments.includes('data');
|
|
1443
|
+
if (!isRaw && !readsWrapper) {
|
|
1444
|
+
return [{
|
|
1445
|
+
ruleId: 'FLOW-009',
|
|
1446
|
+
severity: 'block',
|
|
1447
|
+
message: `'${flowStepLabel(step)}' setter adımı ${origin}='${reference}' okuyor ama '${root}' adımı isRaw=false ile çalışıyor → storage { data, headers, error } sarmalıdır. Ref '${root}.data' olmalı; aksi hâlde prop'a dizinin yerine sarmal nesne yazılır.`,
|
|
1448
|
+
guide: 'ngapps://guides/page'
|
|
1449
|
+
}];
|
|
1450
|
+
}
|
|
1451
|
+
return isRaw && readsWrapper
|
|
1452
|
+
? [{
|
|
1453
|
+
ruleId: 'FLOW-009',
|
|
1454
|
+
severity: 'block',
|
|
1455
|
+
message: `'${flowStepLabel(step)}' setter adımı ${origin}='${reference}' okuyor ama '${root}' adımı isRaw=true — storage ham cevaptır, '.data' diye bir alan yoktur. Ref '${root}' olmalı.`,
|
|
1456
|
+
guide: 'ngapps://guides/page'
|
|
1457
|
+
}]
|
|
1458
|
+
: [];
|
|
1459
|
+
}));
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
id: 'FLOW-010',
|
|
1464
|
+
module: 'flow',
|
|
1465
|
+
appliesTo: ['create', 'update'],
|
|
1466
|
+
sourceRef: 'flow-machine.ts:228 getStorage → dot-prop getProperty (nokta yol ayırıcısıdır); event-actions.mappers.ts:142 Studio her okumada microservice adımının isRaw\'ını false\'a çeker',
|
|
1467
|
+
check: definition => flowAllSteps(definition)
|
|
1468
|
+
.filter(step => step.type === 'microservice')
|
|
1469
|
+
.flatMap(step => {
|
|
1470
|
+
const findings = [];
|
|
1471
|
+
if (String(step.name ?? '').includes('.')) {
|
|
1472
|
+
findings.push({
|
|
1473
|
+
ruleId: 'FLOW-010',
|
|
1474
|
+
severity: 'warn',
|
|
1475
|
+
message: `'${flowStepLabel(step)}' adımının adında nokta var. Storage okuması dot-prop ile yapılır; noktalı ad yol segmentine bölünür ve setter ref'i asla çözülmez.`,
|
|
1476
|
+
guide: 'ngapps://guides/page'
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
if ((step.args ?? {}).isRaw === true) {
|
|
1480
|
+
findings.push({
|
|
1481
|
+
ruleId: 'FLOW-010',
|
|
1482
|
+
severity: 'warn',
|
|
1483
|
+
message: `'${flowStepLabel(step)}' adımı isRaw=true. Studio bu akışı her AÇTIĞINDA isRaw'ı false'a çevirir (event-actions.mappers.ts) ve storage ref'lerine '.data' ekler; kullanıcı akışı bir kez kaydederse ref'leriniz değişir. Kalıcı olması için isRaw'ı verme ve ref'i '<adım adı>.data' yaz.`,
|
|
1484
|
+
guide: 'ngapps://guides/page'
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
return findings;
|
|
1488
|
+
})
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
id: 'FLOW-011',
|
|
1492
|
+
module: 'flow',
|
|
1493
|
+
appliesTo: ['create', 'update'],
|
|
1494
|
+
sourceRef: 'microservice.ts:79-90 başarı yolunda storage { data, headers, error }; microservice.ts:132-135 HATA yolunda { error, data } — headers YOK. runCode TypeError\'ı yutar (flow-execution.functions.ts:403-408) ve flow-engine.ts:53 bir sonraki next() çağrısında akışı onError\'a düşürür',
|
|
1495
|
+
check: definition => {
|
|
1496
|
+
const steps = flowAllSteps(definition);
|
|
1497
|
+
const microserviceStepsByName = new Map(steps
|
|
1498
|
+
.filter(step => step.type === 'microservice' && step.name)
|
|
1499
|
+
.map(step => [String(step.name), step]));
|
|
1500
|
+
return steps.flatMap(step => flowCodeExpressions(step).flatMap(({ expression, origin }) => {
|
|
1501
|
+
flowHeadersAccess.lastIndex = 0;
|
|
1502
|
+
const findings = [];
|
|
1503
|
+
let match;
|
|
1504
|
+
while ((match = flowHeadersAccess.exec(expression)) !== null) {
|
|
1505
|
+
const name = match[1] ?? match[2] ?? '';
|
|
1506
|
+
const source = microserviceStepsByName.get(name);
|
|
1507
|
+
if (source && (source.args ?? {}).isRaw === true) {
|
|
1508
|
+
findings.push({
|
|
1509
|
+
ruleId: 'FLOW-011',
|
|
1510
|
+
severity: 'block',
|
|
1511
|
+
message: `'${flowStepLabel(step)}' adımının ${origin} ifadesi Steps.${name}.headers okuyor ama '${name}' adımı isRaw=true ile çalışıyor — o zaman storage ham response.data'dır, 'headers' diye bir alan HİÇ yazılmaz (microservice.ts:88). İfade her koşuda TypeError verir. isRaw'ı kaldırın (normalize false'a çeker) ya da header okumaktan vazgeçin.`,
|
|
1512
|
+
guide: 'ngapps://guides/page'
|
|
1513
|
+
});
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
findings.push({
|
|
1517
|
+
ruleId: 'FLOW-011',
|
|
1518
|
+
severity: 'warn',
|
|
1519
|
+
message: `'${flowStepLabel(step)}' adımının ${origin} ifadesinde Steps.${name}.headers guard'sız okunuyor. İstek HATA aldığında storage { error, data } olarak yazılır ve 'headers' alanı yoktur (microservice.ts:132) → TypeError; runCode undefined döner (prop'a NaN yazılır), ifade hatası toast'ı düşer ve flow-engine.ts:53 akışı onError'a çevirip durdurabilir. Guard'lı yazın: Number(Steps.${name}?.headers?.["total-count"] ?? 0).`,
|
|
1520
|
+
guide: 'ngapps://guides/page'
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
return findings;
|
|
1524
|
+
}));
|
|
1525
|
+
}
|
|
1526
|
+
},
|
|
1527
|
+
{
|
|
1528
|
+
id: 'FLOW-012',
|
|
1529
|
+
module: 'flow',
|
|
1530
|
+
appliesTo: ['create', 'update'],
|
|
1531
|
+
sourceRef: 'setter.ts:83-104 map() iterator: object[key] = … — mapping anahtarı DÜZ yazılır, iç içe nesne üretilmez (okuma tarafı dot-prop ile çözer)',
|
|
1532
|
+
check: definition => flowAllSteps(definition)
|
|
1533
|
+
.filter(step => step.type === 'setter')
|
|
1534
|
+
.flatMap(step => flowIteratees(step).flatMap(iteratee => {
|
|
1535
|
+
const dotted = Object.keys(flowMappingsOf(iteratee)).filter(key => key.includes('.'));
|
|
1536
|
+
return dotted.length > 0
|
|
1537
|
+
? [{
|
|
1538
|
+
ruleId: 'FLOW-012',
|
|
1539
|
+
severity: 'warn',
|
|
1540
|
+
message: `'${flowStepLabel(step)}' setter adımının mapping anahtarında nokta var: '${dotted.join("', '")}'. Mapper anahtarı DÜZ yazar (object['${dotted[0]}']), iç içe alan üretmez; kolon bunu nokta yolu olarak çözmeye çalışırsa değeri göremez. Kolonun kendi key'ini düz anahtar olarak verin (örn. 'parent_name'), kaynak yolunu ise ref'e yazın ({ "type": "item", "ref": "parent_title.name" }).`,
|
|
1541
|
+
guide: 'ngapps://guides/page'
|
|
1542
|
+
}]
|
|
1543
|
+
: [];
|
|
1544
|
+
}))
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
id: 'FLOW-014',
|
|
1548
|
+
module: 'flow',
|
|
1549
|
+
appliesTo: ['create', 'update'],
|
|
1550
|
+
sourceRef: "flow-trigger.ts: task.argument.flowId varsa engine.run(flowId), yoksa triggerElementId + triggerElementEvent ile hedef komponentin olay akışı çalıştırılır; ikisi de yoksa adım hiçbir şey yapmadan engine.next('always') der. Backend StepManagementArgument.ToDomain aynı ikiliği bekler (destek masası koşumu B-02)",
|
|
1551
|
+
check: definition => flowAllSteps(definition)
|
|
1552
|
+
.filter(step => step.type === 'flow-trigger')
|
|
1553
|
+
.flatMap((step) => {
|
|
1554
|
+
const args = (step.args ?? {});
|
|
1555
|
+
const hasFlowId = typeof args.flowId === 'string' && args.flowId.length > 0;
|
|
1556
|
+
const hasElement = typeof args.triggerElementId === 'string' && args.triggerElementId.length > 0;
|
|
1557
|
+
if (hasFlowId && hasElement) {
|
|
1558
|
+
return [{
|
|
1559
|
+
ruleId: 'FLOW-014',
|
|
1560
|
+
severity: 'warn',
|
|
1561
|
+
message: `'${flowStepLabel(step)}' flow-trigger adımında hem args.flowId hem args.triggerElementId var; runtime önce flowId'ye bakar (flow-trigger.ts) ve element dalına HİÇ girmez. Birini silin.`,
|
|
1562
|
+
guide: 'ngapps://guides/page'
|
|
1563
|
+
}];
|
|
1564
|
+
}
|
|
1565
|
+
if (hasFlowId)
|
|
1566
|
+
return [];
|
|
1567
|
+
if (hasElement) {
|
|
1568
|
+
return typeof args.triggerElementEvent === 'string' && args.triggerElementEvent.length > 0
|
|
1569
|
+
? []
|
|
1570
|
+
: [{
|
|
1571
|
+
ruleId: 'FLOW-014',
|
|
1572
|
+
severity: 'block',
|
|
1573
|
+
message: `'${flowStepLabel(step)}' flow-trigger adımında args.triggerElementId var ama args.triggerElementEvent yok — runtime hedef komponentin olay listesinde eşleşme arar, olay adı olmadan hiçbir akış çalışmaz. Olay adını component_get'ten alın.`,
|
|
1574
|
+
guide: 'ngapps://guides/page'
|
|
1575
|
+
}];
|
|
1576
|
+
}
|
|
1577
|
+
return [{
|
|
1578
|
+
ruleId: 'FLOW-014',
|
|
1579
|
+
severity: 'block',
|
|
1580
|
+
message: `'${flowStepLabel(step)}' flow-trigger adımının args'ı boş. İki geçerli şekil var: (1) başka bir akışı çalıştır → args: { "flowId": "<hedef flow id>" }; (2) bir komponentin olay akışını çalıştır → args: { "triggerElementId": "<elementId>", "triggerElementEvent": "<component_get events listesinden ad>" }. İkisi de yoksa backend 404 'Kayıt bulunamadı' döner (StepManagementArgument.ToDomain trigger dalı), dönmese bile adım hiçbir şey yapmaz.`,
|
|
1581
|
+
guide: 'ngapps://guides/page'
|
|
1582
|
+
}];
|
|
1583
|
+
})
|
|
1584
|
+
},
|
|
1585
|
+
{
|
|
1586
|
+
id: 'FLOW-015',
|
|
1587
|
+
module: 'flow',
|
|
1588
|
+
appliesTo: ['create', 'update'],
|
|
1589
|
+
sourceRef: "microservice.ts: adım args.microserviceId ile ucu çözer; boşsa istek /m/<app>/null adresine gider ve 404 döner. Kayıt 200 kabul edilir, hata yalnız O DAL koşunca ortaya çıkar (klinik panel/randevular koşumu 2026-08-18)",
|
|
1590
|
+
check: definition => flowAllSteps(definition)
|
|
1591
|
+
.filter(step => step.type === 'microservice')
|
|
1592
|
+
.flatMap((step) => {
|
|
1593
|
+
const args = (step.args ?? {});
|
|
1594
|
+
const microserviceId = args.microserviceId;
|
|
1595
|
+
return typeof microserviceId === 'string' && microserviceId.length > 0
|
|
1596
|
+
? []
|
|
1597
|
+
: [{
|
|
1598
|
+
ruleId: 'FLOW-015',
|
|
1599
|
+
severity: 'block',
|
|
1600
|
+
message: `'${flowStepLabel(step)}' microservice adımının args.microserviceId'si boş. Kayıt 200 döner ama çalışma anında istek '/m/<application>/null' adresine gidip 404 alır ve sonraki adım hata gövdesini veri sanır. Ucu microservice_list'ten seçip args: { "microserviceId": "<id>", "isRaw": false, "parameters": [] } verin; ucu henüz yaratmadıysanız önce onu yaratın, adımı boş bırakıp "sonra bağlarım" demeyin.`,
|
|
1601
|
+
guide: 'ngapps://guides/page'
|
|
1602
|
+
}];
|
|
1603
|
+
})
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
id: 'FLOW-016',
|
|
1607
|
+
module: 'flow',
|
|
1608
|
+
appliesTo: ['create', 'update'],
|
|
1609
|
+
sourceRef: "Page.user middleware'in set-user çıktısıdır ve sayfa middlewareIds ile bağlı değilse UNDEFINED'dır; ayrıca set-user DTO'su çoğu kurulumda roles dizisini taşımaz (yalnız role / roles_csv). klinik+kutuphane+obs koşumu 2026-08-18",
|
|
1610
|
+
check: definition => flowAllSteps(definition).flatMap((step) => {
|
|
1611
|
+
const source = JSON.stringify(step.args ?? {});
|
|
1612
|
+
if (!source.includes('Page.user'))
|
|
1613
|
+
return [];
|
|
1614
|
+
const findings = [];
|
|
1615
|
+
const unguarded = readsBeforeGuard(source, 'Page.user');
|
|
1616
|
+
if (unguarded) {
|
|
1617
|
+
findings.push({
|
|
1618
|
+
ruleId: 'FLOW-016',
|
|
1619
|
+
severity: 'warn',
|
|
1620
|
+
message: `'${flowStepLabel(step)}' adımı 'Page.user' alanını guard'sız okuyor. Page.user middleware'in set-user çıktısıdır ve SAYFA o middleware'e (page_update middlewareIds) bağlı değilse undefined'dır — 'Cannot read properties of undefined' ile ifade patlar, condition adımında bu sessizce else dalına düşmek demektir. Köke bir kez guard'lı dokunun — 'var u = Page.user || {};', 'Page.user && Page.user.x', 'Page.user?.x', 'typeof Page.user' biçimlerinin hepsi kabul edilir — ve sayfanın middlewareIds'ini verin.`,
|
|
1621
|
+
guide: 'ngapps://guides/expressions'
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
if (/Page\.user\s*\.\s*roles(?![A-Za-z0-9_])/.test(source)) {
|
|
1625
|
+
findings.push({
|
|
1626
|
+
ruleId: 'FLOW-016',
|
|
1627
|
+
severity: 'warn',
|
|
1628
|
+
message: `'${flowStepLabel(step)}' adımı 'Page.user.roles' DİZİSİNİ okuyor. set-user DTO'su diziyi çoğu kurulumda düşürür (dto_get ile bakın: genelde yalnız 'role', 'role_name', 'roles_csv' vardır) — dizi undefined gelir, '|| []' yüzünden hata da vermez ve rol dalları sessizce hiç eşleşmez. CSV'yi kullanın: var rc = u.roles_csv || (u.role ? ',' + u.role + ',' : ''); rc.indexOf(',admin,') >= 0.`,
|
|
1629
|
+
guide: 'ngapps://guides/expressions'
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
return findings;
|
|
1633
|
+
})
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
id: 'MS-013',
|
|
1637
|
+
module: 'microservice',
|
|
1638
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1639
|
+
sourceRef: "PostgreSqlRelationHandler.HandleJoin: ProcessJoinTree(entity, alias, joins) kök alias'ı doğrudan SqlKata'ya ON kolonunun SOL tarafı olarak verir ($\"{currentAlias}.{relationField.Slug}\"). Alias boşsa SQL'e '.<alan>' gider, SqlKata onu \"\".\"<alan>\" diye tırnaklar, Postgres 42601 zero-length delimited identifier döner. Runtime'da varsayılan atanmaz — Microservice.cs:671 yalnız şema üretiminde Entity.Slug'a düşer (crm koşumu B-2)",
|
|
1640
|
+
check: definition => {
|
|
1641
|
+
const parameters = (definition.sourceParameters ?? {});
|
|
1642
|
+
if (definition.source !== 'table')
|
|
1643
|
+
return [];
|
|
1644
|
+
if (asArray(parameters.joins).length === 0)
|
|
1645
|
+
return [];
|
|
1646
|
+
if (String(parameters.alias ?? '').trim() !== '')
|
|
1647
|
+
return [];
|
|
1648
|
+
return [{
|
|
1649
|
+
ruleId: 'MS-013',
|
|
1650
|
+
severity: 'block',
|
|
1651
|
+
message: "sourceParameters.alias (KÖK tablo alias'ı) boş ama join var: join'in ON koşulu '<kökAlias>.<ilişkiAlanı>' olarak kurulduğu için alias'sız gövde Postgres'te opak '42601: zero-length delimited identifier' hatası verir — ne kayıt ne çalıştırma sebebi söyler. alias'ı kök entity'nin slug'ıyla doldurun (kök selectFields'ta kullandığınız alias ile aynı olmalı).",
|
|
1652
|
+
guide: 'ngapps://guides/microservice'
|
|
1653
|
+
}];
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
id: 'WF-019',
|
|
1658
|
+
module: 'workflow',
|
|
1659
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1660
|
+
sourceRef: "MS-013'ün table step karşılığı: TableStep.ByGetAsync → HandleJoin(entityDto, schema.SourceEntity.Alias, …). Gateway alias'ı gövdeden türetmeye çalışır (normalizeTableParameters); türetilemediğinde bu kural konuşur",
|
|
1661
|
+
check: (definition, context) => {
|
|
1662
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
1663
|
+
return workflowSteps(body)
|
|
1664
|
+
.filter(step => step.type === 'table')
|
|
1665
|
+
.flatMap((step) => {
|
|
1666
|
+
const rule = (step.rule ?? {});
|
|
1667
|
+
if (asArray(rule.joins).length === 0)
|
|
1668
|
+
return [];
|
|
1669
|
+
if (String(rule.alias ?? '').trim() !== '')
|
|
1670
|
+
return [];
|
|
1671
|
+
return [{
|
|
1672
|
+
ruleId: 'WF-019',
|
|
1673
|
+
severity: 'block',
|
|
1674
|
+
message: `'${stepLabel(step)}' table step'inde join var ama rule.alias (KÖK tablo alias'ı) boş: join'in ON koşulu '<kökAlias>.<ilişkiAlanı>' olarak kurulur, boş alias SQL'e '.<alan>' yazar ve Postgres 42601 (zero-length delimited identifier) döner — adım hiç çalışmaz, hata da sebebi söylemez. rule.alias'ı kök entity'nin slug'ıyla (kök selectFields'taki alias) doldurun.`,
|
|
1675
|
+
guide: 'ngapps://guides/workflow'
|
|
1676
|
+
}];
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
id: 'WF-020',
|
|
1682
|
+
module: 'workflow',
|
|
1683
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1684
|
+
sourceRef: "row_list ilişki alanını HER ZAMAN nesne döndürür (GetRowsQuery.ProcessRelation her ilişki alanı için join üretir), table step/microservice ise YALNIZ joins'te bildirdiğin ilişkiyi nesneye çevirir (PostgreSqlRelationHandler.ModifyResponse → ProcessJoins). Join'siz okunan ilişki alanı DÜZ FK string'idir; '<alan>.<altAlan>' okuması undefined verir ve sessizce boş role/boş liste üretir (crm koşumu B-1)",
|
|
1685
|
+
check: (definition, context) => {
|
|
1686
|
+
const body = workflowBody(definition, context.workflowKind);
|
|
1687
|
+
return workflowSteps(body).flatMap((step) => {
|
|
1688
|
+
const source = JSON.stringify(step.rule ?? {});
|
|
1689
|
+
const reads = [...source.matchAll(/\.([A-Za-z_][A-Za-z0-9_]*_id)\s*\.\s*[A-Za-z_]/g)].map(match => match[1]);
|
|
1690
|
+
if (reads.length === 0)
|
|
1691
|
+
return [];
|
|
1692
|
+
if (/_id\s*(&&|\?|\|\|)/.test(source))
|
|
1693
|
+
return [];
|
|
1694
|
+
return [{
|
|
1695
|
+
ruleId: 'WF-020',
|
|
1696
|
+
severity: 'warn',
|
|
1697
|
+
message: `'${stepLabel(step)}' adımı bir ilişki alanının İÇİNİ okuyor (${[...new Set(reads)].join(', ')}). Table adımı ilişki alanını yalnız o ilişki için 'joins' bildirdiyseniz { id, <displayAlanı> } nesnesi döndürür; join'siz okumada DÜZ FK string'i gelir ve '.<altAlan>' UNDEFINED olur — hata vermez, sessizce boş sonuç üretir (rol dizisi boşalır, yetki kapısı yanlış karar verir). İki çözüm: (1) adıma o ilişki için join + selectFields ekleyin, (2) hedef tabloyu ayrı bir table adımıyla okuyup id üzerinden eşleştirin. Her iki şekle dayanan yazım: 'var v = x.${reads[0]} && x.${reads[0]}.id ? x.${reads[0]}.id : x.${reads[0]};'`,
|
|
1698
|
+
guide: 'ngapps://guides/workflow'
|
|
1699
|
+
}];
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
id: 'MW-003',
|
|
1705
|
+
module: 'workflow',
|
|
1706
|
+
appliesTo: ['create', 'update', 'preview'],
|
|
1707
|
+
sourceRef: "set-user değişkeni istek kapsamındaki WorkflowContext'e yazılır (SetUserStep → AddVariable('user')) ve sonraki middleware'in motoru onu görür (WorkflowRunner her koşuda JavascriptEngineInstance'ı _variables'tan kurar; sıra ExecuteMicroserviceBehavior:96 / ExecutePageBehavior:121 → OrderBy(m => m.Name)). Ama kuran middleware set-user'a ULAŞMADAN biterse (çerez yok, koşul dalı) değişken hiç doğmaz ve okuyan middleware ham 'user is not defined' ile 500 verir — 401 değil (crm koşumu B-3)",
|
|
1708
|
+
check: (definition, context) => {
|
|
1709
|
+
if (context.workflowKind !== 'middleware')
|
|
1710
|
+
return [];
|
|
1711
|
+
const steps = workflowSteps(definition);
|
|
1712
|
+
if (steps.some(step => step.type === 'set-user'))
|
|
1713
|
+
return [];
|
|
1714
|
+
const readers = steps.filter(step => /\buser\s*[.\]]/.test(JSON.stringify(step.rule ?? {})));
|
|
1715
|
+
return readers.length === 0
|
|
1716
|
+
? []
|
|
1717
|
+
: [{
|
|
1718
|
+
ruleId: 'MW-003',
|
|
1719
|
+
severity: 'warn',
|
|
1720
|
+
message: `Bu middleware 'user' okuyor (${readers.map(stepLabel).join(', ')}) ama kendisi bir set-user adımı taşımıyor: değişken BAŞKA bir middleware'in set-user'ından gelmek zorunda. Sıra ada göredir (alfabetik) ve değişken istek boyunca yaşar, ama kuran middleware set-user'a ulaşmadan biterse (token/çerez yok, koşul else dalı) 'user' hiç tanımlanmaz ve bu adım ham 'user is not defined' ile 500 döner — kullanıcı 401 yerine 500 görür. Doğrusu: rol/yetki kapısını kimlik middleware'inin İÇİNE, set-user'dan sonra koyun (platform_guide('middleware') Tarif 3b). Zinciri korumak istiyorsanız ifadeyi guard'layın: 'typeof user === \'undefined\' ? false : Authorized(user.roles)'.`,
|
|
1721
|
+
guide: 'ngapps://guides/middleware'
|
|
1722
|
+
}];
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
];
|