@enformik/cli 0.2.0 → 0.5.0
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/LICENSE +21 -0
- package/README.md +3 -3
- package/dist/cli.js +29 -8
- package/dist/codegen.js +137 -0
- package/dist/commands/ai.js +168 -0
- package/dist/commands/app.js +142 -0
- package/dist/commands/codegen.js +42 -0
- package/dist/commands/schema.js +72 -5
- package/dist/commands/site.js +47 -9
- package/dist/commands/whoami.js +3 -2
- package/dist/config.js +2 -0
- package/dist/http.js +10 -1
- package/dist/output.js +1 -1
- package/dist/precheck.js +104 -3
- package/dist/schema.js +62 -8
- package/dist/site.js +30 -7
- package/dist/zip.js +8 -2
- package/package.json +2 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { generateClient } from '../codegen.js';
|
|
4
|
+
import { CliError } from '../errors.js';
|
|
5
|
+
import { parseManifest } from '../schema.js';
|
|
6
|
+
/**
|
|
7
|
+
* enformik codegen [--manifest module.json] [--out src/enformik.generated.ts] [--check]
|
|
8
|
+
* Yerel manifestten TypeScript tipleri + tipli veri istemcisi üretir; ağ/kimlik gerekmez. --check: dosya güncel değilse çıkış 2.
|
|
9
|
+
*/
|
|
10
|
+
export async function codegen(ctx) {
|
|
11
|
+
const manifestPath = resolve(ctx.cwd, ctx.settings.manifestPath);
|
|
12
|
+
let text;
|
|
13
|
+
try {
|
|
14
|
+
text = readFileSync(manifestPath, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new CliError(`Manifest okunamadı: ${manifestPath}`);
|
|
18
|
+
}
|
|
19
|
+
const manifest = parseManifest(text, ctx.settings.manifestPath);
|
|
20
|
+
const code = generateClient(manifest);
|
|
21
|
+
const outPath = resolve(ctx.cwd, ctx.settings.codegenOut);
|
|
22
|
+
const current = existsSync(outPath) ? readFileSync(outPath, 'utf8') : null;
|
|
23
|
+
// Windows'ta git autocrlf dosyayı CRLF'ye çevirir; satır sonu/BOM farkı "güncel değil" sayılmasın.
|
|
24
|
+
const norm = (s) => s.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
|
|
25
|
+
const same = current !== null && norm(current) === norm(code);
|
|
26
|
+
const slug = manifest.module.slug;
|
|
27
|
+
if (ctx.flags.check === true) {
|
|
28
|
+
const upToDate = same;
|
|
29
|
+
ctx.out.info(upToDate ? `✓ ${ctx.settings.codegenOut} güncel (${slug}).` : `✗ ${ctx.settings.codegenOut} ${current === null ? 'yok' : 'manifestle uyumsuz'}; \`enformik codegen\` ile yeniden üretin.`);
|
|
30
|
+
ctx.out.result({ module: slug, out: ctx.settings.codegenOut, upToDate });
|
|
31
|
+
return upToDate ? 0 : 2;
|
|
32
|
+
}
|
|
33
|
+
const changed = !same;
|
|
34
|
+
if (changed) {
|
|
35
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
36
|
+
writeFileSync(outPath, code);
|
|
37
|
+
}
|
|
38
|
+
const n = (code.match(/^export interface \w+Input /gm) ?? []).length;
|
|
39
|
+
ctx.out.info(changed ? `✓ ${ctx.settings.codegenOut} yazıldı (${slug}, ${n} entity).` : `= ${ctx.settings.codegenOut} zaten güncel (${slug}, ${n} entity).`);
|
|
40
|
+
ctx.out.result({ module: slug, out: ctx.settings.codegenOut, entities: n, written: changed });
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
package/dist/commands/schema.js
CHANGED
|
@@ -3,9 +3,10 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { discoverApp, fetchAccessToken } from '../auth.js';
|
|
4
4
|
import { CliError } from '../errors.js';
|
|
5
5
|
import { assertSecureUrl } from '../http.js';
|
|
6
|
-
import { adminOnlySections, parseManifest, pendingChanges, renderDiff, SchemaClient, typeChanges } from '../schema.js';
|
|
6
|
+
import { adminOnlySections, parseManifest, pendingChanges, renderDiff, SchemaClient, stripBom, typeChanges, unapprovedChanges } from '../schema.js';
|
|
7
|
+
import { renderTable } from '../output.js';
|
|
7
8
|
/** Ortak hazırlık: manifest → uygulama keşfi → belirteç → modül kimliği. */
|
|
8
|
-
async function prepare(ctx, mode = 'existing') {
|
|
9
|
+
export async function prepare(ctx, mode = 'existing') {
|
|
9
10
|
const { settings, out } = ctx;
|
|
10
11
|
if (!settings.appUrl)
|
|
11
12
|
throw new CliError('Uygulama adresi yok. `enformik link --app-url https://…` çalıştırın ya da ENFORMIK_APP_URL tanımlayın.');
|
|
@@ -20,12 +21,14 @@ async function prepare(ctx, mode = 'existing') {
|
|
|
20
21
|
const manifestPath = resolve(ctx.cwd, settings.manifestPath);
|
|
21
22
|
let text;
|
|
22
23
|
try {
|
|
23
|
-
text = readFileSync(manifestPath, 'utf8');
|
|
24
|
+
text = stripBom(readFileSync(manifestPath, 'utf8'));
|
|
24
25
|
}
|
|
25
26
|
catch {
|
|
26
27
|
throw new CliError(`Manifest okunamadı: ${manifestPath}`);
|
|
27
28
|
}
|
|
28
29
|
const manifest = parseManifest(text, settings.manifestPath);
|
|
30
|
+
// Sunucuya dosyanın kendi metni gider (JSON.stringify yeniden serileştirmesi büyük sayıları/biçimi değiştirebilirdi).
|
|
31
|
+
const manifestJson = text;
|
|
29
32
|
const slug = settings.moduleSlug ?? manifest.module.slug;
|
|
30
33
|
if (settings.moduleSlug && settings.moduleSlug !== manifest.module.slug) {
|
|
31
34
|
throw new CliError(`Yapılandırmadaki modül ("${settings.moduleSlug}") ile manifestteki slug ("${manifest.module.slug}") farklı.`);
|
|
@@ -39,16 +42,21 @@ async function prepare(ctx, mode = 'existing') {
|
|
|
39
42
|
const workspaceId = settings.workspaceId || app.tenantId;
|
|
40
43
|
if (!workspaceId)
|
|
41
44
|
throw new CliError('Çalışma alanı belirlenemedi. --workspace <kimlik> verin ya da `enformik link --workspace` ile kaydedin.');
|
|
45
|
+
// Token adresi uygulamanın /api/config yanıtından gelir; istemci sırrı oraya gider, bu yüzden hedefi görünür kıl.
|
|
46
|
+
out.info(`Kimlik sağlayıcı: ${new URL(app.oidcAuthority).host} (istemci kimliği buraya gönderilir)`);
|
|
42
47
|
const tok = await fetchAccessToken(app, settings.clientId, settings.clientSecret, ctx.version, ctx.fetchImpl);
|
|
43
48
|
const client = new SchemaClient({ appUrl: settings.appUrl, token: tok.token, workspaceId, version: ctx.version, fetchImpl: ctx.fetchImpl });
|
|
49
|
+
if (mode === 'validate') {
|
|
50
|
+
return { client, manifest, manifestJson, moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
|
|
51
|
+
}
|
|
44
52
|
if (mode === 'install') {
|
|
45
53
|
const existing = await client.findModule(slug);
|
|
46
54
|
if (existing)
|
|
47
55
|
throw new CliError(`"${slug}" modülü bu uygulamada zaten kurulu (sürüm ${existing.version}). Yükseltmek için \`enformik schema push\` kullanın.`);
|
|
48
|
-
return { client, manifest, manifestJson
|
|
56
|
+
return { client, manifest, manifestJson, moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
|
|
49
57
|
}
|
|
50
58
|
const mod = await client.resolveModule(slug);
|
|
51
|
-
return { client, manifest, manifestJson
|
|
59
|
+
return { client, manifest, manifestJson, moduleId: mod.id, installedVersion: mod.version, appHost: appUrl.hostname, seedData };
|
|
52
60
|
}
|
|
53
61
|
function summary(p, r) {
|
|
54
62
|
return {
|
|
@@ -112,6 +120,13 @@ export async function schemaPush(ctx) {
|
|
|
112
120
|
return 1;
|
|
113
121
|
}
|
|
114
122
|
ctx.out.info(ctx.out.paint('green', `✓ Uygulandı: ${pendingChanges(res.diffs).length || pending.length} değişiklik (modül ${p.manifest.module.slug} ${p.manifest.module.version}).`));
|
|
123
|
+
// Onay ile uygulama arasında uygulama başkası tarafından değişmiş olabilir: onaylanmayan bir değişiklik uygulandıysa açıkça bildir.
|
|
124
|
+
const extra = unapprovedChanges(dry.diffs, res.diffs);
|
|
125
|
+
if (extra.length > 0) {
|
|
126
|
+
ctx.out.error(`Uyarı: onayladığınız farkta olmayan ${extra.length} değişiklik de uygulandı (diff ile upgrade arasında uygulama değişmiş olabilir): ${extra.map((d) => `${d.resourceType} ${d.slug} (${d.action})`).join(', ')}. Sonucu \`enformik schema diff\` ile doğrulayın.`);
|
|
127
|
+
ctx.out.result({ ...summary(p, res), applied: true, diverged: true, unapproved: extra });
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
115
130
|
ctx.out.result({ ...summary(p, res), applied: true });
|
|
116
131
|
return 0;
|
|
117
132
|
}
|
|
@@ -151,3 +166,55 @@ export async function schemaInstall(ctx) {
|
|
|
151
166
|
ctx.out.result({ ...base, moduleId: res.moduleId, diffs: res.diffs, errors: [], installed: true });
|
|
152
167
|
return 0;
|
|
153
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* enformik schema validate [--profile import|ai] [--offline] — manifesti DB'ye/modüle dokunmadan doğrular.
|
|
171
|
+
* Çıkış: bulgu varsa 1, yoksa 0. Yönetici gerektiren bölümler hata değildir; `adminSections` olarak raporlanır
|
|
172
|
+
* (geliştirici bunları bir yöneticiye ayrı adım olarak devreder). --offline: yalnızca yerel denetim (kimlik/ağ gerekmez).
|
|
173
|
+
*/
|
|
174
|
+
export async function schemaValidate(ctx) {
|
|
175
|
+
const profileFlag = ctx.flags.profile;
|
|
176
|
+
if (profileFlag !== undefined && profileFlag !== 'import' && profileFlag !== 'ai') {
|
|
177
|
+
throw new CliError(`--profile "import" ya da "ai" olmalı (verilen: ${String(profileFlag)}).`);
|
|
178
|
+
}
|
|
179
|
+
const profile = profileFlag === 'ai' ? 'ai' : 'import';
|
|
180
|
+
const seedData = ctx.flags['seed-data'] === true;
|
|
181
|
+
let result;
|
|
182
|
+
let source;
|
|
183
|
+
let slug;
|
|
184
|
+
if (ctx.flags.offline === true) {
|
|
185
|
+
// ai profilinin kuralları (isimlendirme/yapı) yalnızca sunucuda denetlenir; yerel denetimle "geçerli" demek yanlış güven verir.
|
|
186
|
+
if (profileFlag === 'ai')
|
|
187
|
+
throw new CliError('--offline, --profile ai ile birlikte kullanılamaz: ai profilinin kuralları yalnızca sunucuda denetlenir. --offline olmadan çalıştırın.');
|
|
188
|
+
const manifestPath = resolve(ctx.cwd, ctx.settings.manifestPath);
|
|
189
|
+
let text;
|
|
190
|
+
try {
|
|
191
|
+
text = readFileSync(manifestPath, 'utf8');
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
throw new CliError(`Manifest okunamadı: ${manifestPath}`);
|
|
195
|
+
}
|
|
196
|
+
const manifest = parseManifest(text, ctx.settings.manifestPath); // JSON + module.slug/version
|
|
197
|
+
slug = manifest.module.slug;
|
|
198
|
+
result = { valid: true, profile, issues: [], adminSections: adminOnlySections(manifest, { seedData }).map((section) => ({ section })) };
|
|
199
|
+
source = 'local';
|
|
200
|
+
ctx.out.warn('Yalnızca yerel denetim yapıldı (JSON, module.slug/version, yönetici bölümleri); isimlendirme ve yapı kuralları sunucuda denetlenir.');
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const p = await prepare(ctx, 'validate');
|
|
204
|
+
slug = p.manifest.module.slug;
|
|
205
|
+
result = await p.client.validate(p.manifestJson, { profile, seedData });
|
|
206
|
+
source = 'server';
|
|
207
|
+
}
|
|
208
|
+
ctx.out.info(`Modül: ${slug} profil: ${profile} (${source === 'server' ? 'sunucu doğrulaması' : 'yerel'})`);
|
|
209
|
+
if (result.issues.length > 0) {
|
|
210
|
+
ctx.out.info(renderTable(ctx.out, ['KOD', 'AÇIKLAMA'], result.issues.map((i) => [i.code, i.message])));
|
|
211
|
+
}
|
|
212
|
+
if (result.adminSections.length > 0) {
|
|
213
|
+
ctx.out.info(ctx.out.paint('yellow', `Yönetici adımı gerekir (düşük rollü istemci uygulayamaz): ${result.adminSections.map((s) => s.section).join(', ')}`));
|
|
214
|
+
}
|
|
215
|
+
ctx.out.info(result.valid
|
|
216
|
+
? ctx.out.paint('green', source === 'local' ? '✓ Yerel denetim geçti (sunucu kuralları denetlenmedi).' : '✓ Manifest geçerli.')
|
|
217
|
+
: ctx.out.paint('red', `✗ ${result.issues.length} bulgu var.`));
|
|
218
|
+
ctx.out.result({ module: slug, source, ...result });
|
|
219
|
+
return result.valid ? 0 : 1;
|
|
220
|
+
}
|
package/dist/commands/site.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync, statSync } from 'node:fs';
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { CliError } from '../errors.js';
|
|
5
|
-
import { assertSecureUrl } from '../http.js';
|
|
6
5
|
import { formatBytes } from '../output.js';
|
|
7
|
-
import { precheckSite, DEFAULT_LIMITS } from '../precheck.js';
|
|
6
|
+
import { precheckSite, parseScanIgnore, DEFAULT_LIMITS } from '../precheck.js';
|
|
8
7
|
import { collectMeta, DeployClient, idempotencyKey, isTerminal, MAX_ARCHIVE_BYTES, sha256File, waitForDeployment } from '../site.js';
|
|
9
8
|
import { writeZipFile } from '../zip.js';
|
|
10
9
|
function client(ctx) {
|
|
@@ -13,12 +12,26 @@ function client(ctx) {
|
|
|
13
12
|
throw new CliError('Konsol adresi yok. `enformik link --console-url https://…` çalıştırın ya da ENFORMIK_CONSOLE_URL tanımlayın.');
|
|
14
13
|
if (!settings.deployKey)
|
|
15
14
|
throw new CliError('ENFORMIK_DEPLOY_KEY tanımlı değil (konsolda Ek ortamlar → Geliştirici sitesi → Dağıtım anahtarları).');
|
|
16
|
-
assertSecureUrl(settings.consoleUrl, 'Konsol adresi');
|
|
17
15
|
return new DeployClient({ consoleUrl: settings.consoleUrl, deployKey: settings.deployKey, version: ctx.version, fetchImpl: ctx.fetchImpl });
|
|
18
16
|
}
|
|
17
|
+
/** --expect-host verilmişse hedef sitenin ana makine adı eşleşmeli; eşleşmezse hiçbir şey yüklenmeden durur. */
|
|
18
|
+
function assertExpectedHost(ctx, info) {
|
|
19
|
+
const expect = ctx.flags['expect-host'];
|
|
20
|
+
if (typeof expect !== 'string')
|
|
21
|
+
return;
|
|
22
|
+
let host = '';
|
|
23
|
+
try {
|
|
24
|
+
host = new URL(info.url).hostname;
|
|
25
|
+
}
|
|
26
|
+
catch { /* adres bozuksa eşleşmez */ }
|
|
27
|
+
if (expect.toLowerCase() !== host.toLowerCase()) {
|
|
28
|
+
throw new CliError(`Beklenen site "${expect}" ama hedef "${host || info.url}". Yanlış siteye dağıtım yapılmaması için durduruldu.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
19
31
|
/** enformik site status */
|
|
20
32
|
export async function siteStatus(ctx) {
|
|
21
33
|
const info = await client(ctx).getSite();
|
|
34
|
+
assertExpectedHost(ctx, info);
|
|
22
35
|
ctx.out.info(`Web sitesi ortamı : ${info.environment.name}`);
|
|
23
36
|
ctx.out.info(`Adres : ${info.url}`);
|
|
24
37
|
if (info.app)
|
|
@@ -40,9 +53,19 @@ export async function siteDeploy(ctx) {
|
|
|
40
53
|
catch {
|
|
41
54
|
throw new CliError(`Derleme klasörü bulunamadı: ${dir} (önce siteyi derleyin ya da --dir verin).`);
|
|
42
55
|
}
|
|
56
|
+
const timeoutSec = parseTimeout(ctx.flags.timeout);
|
|
43
57
|
// Sunucunun kotası biliniyorsa ön denetimde kullan (aşımı yüklemeden görmek için).
|
|
44
58
|
const info = await api.getSite();
|
|
45
|
-
|
|
59
|
+
assertExpectedHost(ctx, info);
|
|
60
|
+
// Sır taraması yanlış alarmı için proje kökündeki .enformikignore (yol listesi); yoksa boş.
|
|
61
|
+
let scanIgnore = [];
|
|
62
|
+
try {
|
|
63
|
+
scanIgnore = parseScanIgnore(readFileSync(join(ctx.cwd, '.enformikignore'), 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
catch { /* dosya yok */ }
|
|
66
|
+
const pre = precheckSite(dir, { ...DEFAULT_LIMITS, maxFiles: info.limits.maxFiles, maxFileBytes: info.limits.maxFileBytes, maxUnpackedBytes: info.limits.maxUnpackedBytes }, { scanIgnore });
|
|
67
|
+
for (const w of pre.warnings)
|
|
68
|
+
out.warn(w);
|
|
46
69
|
if (pre.issues.length > 0) {
|
|
47
70
|
out.error('Site arşivi kurallara uymuyor; yükleme yapılmadı:');
|
|
48
71
|
for (const i of pre.issues)
|
|
@@ -58,7 +81,7 @@ export async function siteDeploy(ctx) {
|
|
|
58
81
|
throw new CliError(`Sıkıştırılmış arşiv ${formatBytes(z.bytes)}; en çok ${formatBytes(MAX_ARCHIVE_BYTES)} olabilir.`);
|
|
59
82
|
out.info(`${pre.files.length} dosya, ${formatBytes(pre.unpackedBytes)} → ${formatBytes(z.bytes)} (zip)`);
|
|
60
83
|
const sha = sha256File(zipPath);
|
|
61
|
-
const init = await api.init({ sizeBytes: z.bytes, sha256: sha, meta: collectMeta(ctx.env, ctx.cwd) }, idempotencyKey(ctx.env));
|
|
84
|
+
const init = await api.init({ sizeBytes: z.bytes, sha256: sha, meta: collectMeta(ctx.env, ctx.cwd) }, idempotencyKey(ctx.env, undefined, sha));
|
|
62
85
|
let dep = init.deployment;
|
|
63
86
|
if (init.replayed)
|
|
64
87
|
out.info(`Önceki deneme bulundu (#${dep.seq}, durum: ${dep.status}); kaldığı yerden devam ediliyor.`);
|
|
@@ -72,14 +95,14 @@ export async function siteDeploy(ctx) {
|
|
|
72
95
|
await api.finalize(dep.deploymentId);
|
|
73
96
|
dep = { ...dep, status: 'validating' };
|
|
74
97
|
}
|
|
75
|
-
|
|
98
|
+
// Terminal durumdaki (failed/expired/live) yeniden oynatılan kayıt --no-wait ile de gerçek sonucu vermeli.
|
|
99
|
+
if (ctx.flags['no-wait'] === true && !isTerminal(dep.status)) {
|
|
76
100
|
out.info(`Dağıtım başlatıldı (#${dep.seq}). Durum için: konsol → Geliştirici sitesi → Sürümler.`);
|
|
77
101
|
out.result({ deployed: false, deploymentId: dep.deploymentId, seq: dep.seq, status: dep.status });
|
|
78
102
|
return 0;
|
|
79
103
|
}
|
|
80
|
-
const timeoutSec = Number(typeof ctx.flags.timeout === 'string' ? ctx.flags.timeout : 600);
|
|
81
104
|
const final = isTerminal(dep.status) ? dep : await waitForDeployment(api, dep.deploymentId, {
|
|
82
|
-
timeoutMs:
|
|
105
|
+
timeoutMs: timeoutSec * 1000,
|
|
83
106
|
onStatus: (s) => out.info(` durum: ${s}`),
|
|
84
107
|
sleep: ctx.sleep,
|
|
85
108
|
});
|
|
@@ -88,6 +111,12 @@ export async function siteDeploy(ctx) {
|
|
|
88
111
|
out.result({ deployed: true, deploymentId: final.deploymentId, seq: final.seq, url: final.url ?? info.url });
|
|
89
112
|
return 0;
|
|
90
113
|
}
|
|
114
|
+
if (final.status === 'superseded') {
|
|
115
|
+
// Daha yeni bir sürüm canlıya çıktı: istenen son durum sağlandı; CI'ı kırmayız ama açıkça uyarırız.
|
|
116
|
+
out.warn(`Uyarı: bu dağıtım (#${final.seq}) daha yeni bir sürümle değiştirildi; canlı olan daha yeni sürümdür.`);
|
|
117
|
+
out.result({ deployed: false, superseded: true, status: 'superseded', deploymentId: final.deploymentId, seq: final.seq, url: info.url });
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
91
120
|
out.error(`✗ Dağıtım başarısız (#${final.seq}, ${final.status}): ${final.error ?? 'bilinmeyen neden'}`);
|
|
92
121
|
for (const i of final.issues ?? [])
|
|
93
122
|
out.error(` - ${i.path ? i.path + ': ' : ''}${i.message}`);
|
|
@@ -98,3 +127,12 @@ export async function siteDeploy(ctx) {
|
|
|
98
127
|
rmSync(tmp, { recursive: true, force: true });
|
|
99
128
|
}
|
|
100
129
|
}
|
|
130
|
+
/** --timeout (saniye): verilmişse pozitif sayı olmalı; geçersiz değer CI'da hemen görünsün diye hata verir. Vars. 600. */
|
|
131
|
+
function parseTimeout(raw) {
|
|
132
|
+
if (raw === undefined)
|
|
133
|
+
return 600;
|
|
134
|
+
const n = typeof raw === 'string' ? Number(raw) : NaN;
|
|
135
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
136
|
+
throw new CliError(`--timeout pozitif bir saniye sayısı olmalı (verilen: ${String(raw)}).`);
|
|
137
|
+
return n;
|
|
138
|
+
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -13,10 +13,11 @@ export async function whoami(ctx) {
|
|
|
13
13
|
out.info('Şema yükseltme (API istemcisi)');
|
|
14
14
|
out.info(` Uygulama : ${settings.appUrl}`);
|
|
15
15
|
out.info(` İstemci : ${settings.clientId}`);
|
|
16
|
+
out.info(` Kimlik sağlayıcı : ${new URL(app.oidcAuthority).host}`);
|
|
16
17
|
out.info(` Roller : ${roles.join(', ') || '(belirteçte rol yok)'}`);
|
|
17
18
|
out.info(` Çalışma alanı : ${settings.workspaceId || app.tenantId || '(belirtilmedi)'}`);
|
|
18
|
-
out.info(` Belirteç : geçerli (${Math.round(tok.expiresInSec / 60)} dk)`);
|
|
19
|
-
result.schema = { appUrl: settings.appUrl, clientId: settings.clientId, roles, workspaceId: settings.workspaceId || app.tenantId || null };
|
|
19
|
+
out.info(` Belirteç : geçerli${tok.expiresInSec > 0 ? ` (${Math.round(tok.expiresInSec / 60)} dk)` : ' (süre bilgisi yok)'}`);
|
|
20
|
+
result.schema = { appUrl: settings.appUrl, authorityHost: new URL(app.oidcAuthority).host, clientId: settings.clientId, roles, workspaceId: settings.workspaceId || app.tenantId || null };
|
|
20
21
|
}
|
|
21
22
|
if (settings.consoleUrl && settings.deployKey) {
|
|
22
23
|
any = true;
|
package/dist/config.js
CHANGED
|
@@ -64,5 +64,7 @@ export function resolveSettings(config, env, flags) {
|
|
|
64
64
|
moduleSlug: pick(flags.module, config.module?.slug),
|
|
65
65
|
manifestPath: pick(flags.manifest, config.module?.manifest) ?? 'module.json',
|
|
66
66
|
siteDir: pick(flags.dir, config.site?.dir) ?? 'dist',
|
|
67
|
+
docsUrl: (pick(flags['docs-url'], env.ENFORMIK_DOCS_URL) ?? 'https://help.enformik.com'),
|
|
68
|
+
codegenOut: pick(flags.out, config.codegen?.out) ?? 'src/enformik.generated.ts',
|
|
67
69
|
};
|
|
68
70
|
}
|
package/dist/http.js
CHANGED
|
@@ -18,6 +18,8 @@ export function joinUrl(base, path) {
|
|
|
18
18
|
return base.replace(/\/+$/, '') + (path.startsWith('/') ? path : '/' + path);
|
|
19
19
|
}
|
|
20
20
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21
|
+
/** fetch(redirect:'error') yönlendirmede "fetch failed" + cause "unexpected redirect" ile reddeder. */
|
|
22
|
+
const isRedirectError = (e) => /redirect/i.test(String(e?.cause?.message ?? e?.message ?? ''));
|
|
21
23
|
export async function request(url, opts = {}) {
|
|
22
24
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
23
25
|
const attempts = (opts.retries ?? 0) + 1;
|
|
@@ -47,6 +49,8 @@ export async function request(url, opts = {}) {
|
|
|
47
49
|
}
|
|
48
50
|
catch (err) {
|
|
49
51
|
lastErr = err;
|
|
52
|
+
if (isRedirectError(err))
|
|
53
|
+
break; // yönlendirme tekrar denemeyle değişmez
|
|
50
54
|
if (i < attempts - 1) {
|
|
51
55
|
await sleep(400 * 2 ** i);
|
|
52
56
|
continue;
|
|
@@ -54,6 +58,9 @@ export async function request(url, opts = {}) {
|
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
const e = lastErr;
|
|
61
|
+
if (isRedirectError(e)) {
|
|
62
|
+
throw new CliError(`Sunucu başka bir adrese yönlendirdi (${new URL(url).host}); güvenlik gereği kimlik başlığıyla yönlendirme takip edilmez. Adresi yönlendirmenin hedefiyle (ör. https:// ya da doğru alan adı) güncelleyin.`);
|
|
63
|
+
}
|
|
57
64
|
const reason = e?.name === 'TimeoutError' ? 'zaman aşımı' : (e?.cause?.code ?? e?.message ?? 'bilinmeyen hata');
|
|
58
65
|
throw new CliError(`Sunucuya ulaşılamadı (${new URL(url).host}): ${reason}`);
|
|
59
66
|
}
|
|
@@ -63,6 +70,8 @@ export function ensureOk(res) {
|
|
|
63
70
|
return res;
|
|
64
71
|
const body = res.json;
|
|
65
72
|
const code = typeof body?.error === 'string' ? body.error : undefined;
|
|
66
|
-
|
|
73
|
+
// Ara katmanların (CDN/WAF) HTML hata sayfaları mesaja girmez; yalnızca "HTTP <durum>" kalır.
|
|
74
|
+
const plain = /^\s*</.test(res.text) ? '' : res.text.trim().slice(0, 300);
|
|
75
|
+
const message = body?.message ?? (typeof body?.error === 'string' ? body.error : plain) ?? '';
|
|
67
76
|
throw new ApiError(res.status, message || `HTTP ${res.status}`, code, res.json ?? res.text);
|
|
68
77
|
}
|
package/dist/output.js
CHANGED
|
@@ -14,7 +14,7 @@ export function createOutput(opts = {}) {
|
|
|
14
14
|
const stderr = opts.stderr ?? process.stderr;
|
|
15
15
|
const env = opts.env ?? process.env;
|
|
16
16
|
const json = !!opts.json;
|
|
17
|
-
const color = !env.NO_COLOR && env.TERM !== 'dumb' && !!stderr.isTTY &&
|
|
17
|
+
const color = !env.NO_COLOR && env.TERM !== 'dumb' && !!stderr.isTTY && (json || !!stdout.isTTY); // stdout dosyaya/boruya yönlendirilmişse ANSI kaçışı yazılmaz
|
|
18
18
|
const human = (s) => (json ? stderr : stdout).write(s + '\n');
|
|
19
19
|
return {
|
|
20
20
|
json,
|
package/dist/precheck.js
CHANGED
|
@@ -1,9 +1,89 @@
|
|
|
1
|
-
import { lstatSync, readdirSync } from 'node:fs';
|
|
1
|
+
import { lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
/** İzin listesi: yalnızca sitenin çalışması için gereken türler (SPEC CAP-2). Worker (site_artifact.go) ile aynı kümedir;
|
|
4
|
+
* sapmayı temporal-worker testdata/site_file_types.json sözleşme dosyası yakalar. */
|
|
5
|
+
export const ALLOWED_EXTS = new Set([
|
|
6
|
+
'.html', '.htm', '.css', '.js', '.mjs', '.map',
|
|
7
|
+
'.json', '.webmanifest', '.xml', '.txt', '.csv', '.md',
|
|
8
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.bmp',
|
|
9
|
+
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
10
|
+
'.mp3', '.ogg', '.wav', '.m4a',
|
|
11
|
+
'.mp4', '.webm',
|
|
12
|
+
'.pdf', '.wasm',
|
|
13
|
+
]);
|
|
14
|
+
export const EXECUTABLE_EXTS = new Set([
|
|
15
|
+
'.exe', '.dll', '.msi', '.bat', '.cmd', '.com', '.scr', '.ps1',
|
|
16
|
+
'.sh', '.apk', '.ipa', '.dmg', '.pkg', '.deb', '.rpm', '.jar', '.app',
|
|
17
|
+
]);
|
|
18
|
+
/** İzin listesinde olmayan dosya için sorun döner; izinliyse null. Uzantısız dosya yalnızca .well-known/ altında izinli. */
|
|
19
|
+
export function fileTypeIssue(relPath) {
|
|
20
|
+
const base = relPath.slice(relPath.lastIndexOf('/') + 1);
|
|
21
|
+
const dot = base.lastIndexOf('.');
|
|
22
|
+
const ext = dot > 0 ? base.slice(dot).toLowerCase() : '';
|
|
23
|
+
if (EXECUTABLE_EXTS.has(ext)) {
|
|
24
|
+
return { code: 'executable_file', message: `Çalıştırılabilir/paket dosyası (${ext}) yüklenemez. Sitenin çalışması için gerekmeyen dosyaları klasörden çıkarın.` };
|
|
25
|
+
}
|
|
26
|
+
if (ext === '') {
|
|
27
|
+
return relPath.startsWith('.well-known/') ? null : { code: 'disallowed_file_type', message: 'Uzantısız dosya yüklenemez (yalnızca .well-known/ altında izinlidir).' };
|
|
28
|
+
}
|
|
29
|
+
if (!ALLOWED_EXTS.has(ext)) {
|
|
30
|
+
return { code: 'disallowed_file_type', message: `${ext} dosyaları yüklenemez; yalnızca sitenin çalışması için gereken türler (sayfa, betik, stil, görsel, yazı tipi, ses, video, pdf, wasm, metin/veri) izinlidir.` };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
3
34
|
export const DEFAULT_LIMITS = { maxFiles: 10000, maxFileBytes: 100 * 1024 * 1024 };
|
|
4
35
|
export const RESERVED_DIR = '_enformik';
|
|
5
36
|
const MAX_ISSUES = 20;
|
|
6
|
-
|
|
37
|
+
/** Worker (site_artifact.go maxSitePathBytes) ile aynı sınır. */
|
|
38
|
+
const MAX_PATH_BYTES = 255;
|
|
39
|
+
// Tarayıcı paketine sızmaması gereken sırlar. Değer ASLA çıktıya yazılmaz; yalnızca dosya yolu ve tür bildirilir.
|
|
40
|
+
// Bilerek dar tutulur (yanlış alarm CI'ı kırar): dağıtım anahtarı, istemci sırrı atamaları, sabit Basic kimlik başlığı.
|
|
41
|
+
const SECRET_PATTERNS = [
|
|
42
|
+
{ kind: 'dağıtım anahtarı (enf_dk_…)', re: /enf_dk_[A-Za-z0-9_-]{16,}/ },
|
|
43
|
+
{ kind: 'istemci sırrı (client_secret)', re: /client_?secret["']?\s*[:=]\s*["'`][^"'`\s]{8,}["'`]/i },
|
|
44
|
+
{ kind: 'ENFORMIK_CLIENT_SECRET değeri', re: /ENFORMIK_CLIENT_SECRET["']?\s*[:=]\s*["'`][^"'`\s]{6,}["'`]/ },
|
|
45
|
+
{ kind: 'sabit Basic kimlik başlığı', re: /Authorization["']?\s*:\s*["'`]Basic\s+[A-Za-z0-9+/=]{16,}/i },
|
|
46
|
+
];
|
|
47
|
+
const SCAN_EXT = new Set(['.js', '.mjs', '.cjs', '.html', '.htm', '.json', '.map', '.css', '.txt', '.xml', '.svg', '.webmanifest', '.csv', '.md']);
|
|
48
|
+
const SCAN_MAX_BYTES = 5 * 1024 * 1024;
|
|
49
|
+
/**
|
|
50
|
+
* `.enformikignore` içeriğini yol listesine çevirir: satır başına bir yol (dağıtım klasörüne göre), `#` yorum.
|
|
51
|
+
* `dir/` ile biten satır o dizinin altındaki her dosyayı kapsar; joker yoktur (bilerek dar). Yalnızca sır taramasını
|
|
52
|
+
* atlatır; dosya türü, boyut ve diğer denetimler yine uygulanır.
|
|
53
|
+
*/
|
|
54
|
+
export function parseScanIgnore(text) {
|
|
55
|
+
return text.split(/\r?\n/).map((l) => l.trim().replace(/^\.\//, '')).filter((l) => l && !l.startsWith('#'));
|
|
56
|
+
}
|
|
57
|
+
const isIgnored = (name, ignore) => ignore.some((p) => (p.endsWith('/') ? name.startsWith(p) : name === p));
|
|
58
|
+
/** Metin dosyalarında sır desenlerini arar (dosya başına en çok bir bulgu). */
|
|
59
|
+
export function scanSecrets(files, ignore = []) {
|
|
60
|
+
const out = { issues: [], ignored: [], unscanned: [] };
|
|
61
|
+
for (const f of files) {
|
|
62
|
+
const dot = f.name.lastIndexOf('.');
|
|
63
|
+
if (dot < 0 || !SCAN_EXT.has(f.name.slice(dot).toLowerCase()))
|
|
64
|
+
continue;
|
|
65
|
+
if (isIgnored(f.name, ignore)) {
|
|
66
|
+
out.ignored.push(f.name);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (f.size > SCAN_MAX_BYTES) {
|
|
70
|
+
out.unscanned.push(f.name);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
let text;
|
|
74
|
+
try {
|
|
75
|
+
text = readFileSync(f.path, 'utf8');
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const hit = SECRET_PATTERNS.find((p) => p.re.test(text));
|
|
81
|
+
if (hit)
|
|
82
|
+
out.issues.push({ code: 'secret_in_bundle', path: f.name, message: `Pakette sır var gibi görünüyor: ${hit.kind}. Tarayıcıya giden dosyalara sır konmaz; sırrı paketten çıkarıp (ve sızdıysa iptal edip) yeniden derleyin. Yanlış alarmsa dosya yolunu proje kökündeki .enformikignore dosyasına yazın.` });
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
export function precheckSite(dir, limits = DEFAULT_LIMITS, opts = {}) {
|
|
7
87
|
const files = [];
|
|
8
88
|
const issues = [];
|
|
9
89
|
const add = (code, path, message) => {
|
|
@@ -26,6 +106,14 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS) {
|
|
|
26
106
|
add('hidden_file', relPath, `Gizli dosya/dizin yüklenemez ("${ent.name}"). .env, .git gibi dosyaları arşive koymayın.`);
|
|
27
107
|
continue;
|
|
28
108
|
}
|
|
109
|
+
if (Buffer.byteLength(relPath) > MAX_PATH_BYTES) {
|
|
110
|
+
add('unsafe_path', relPath, `Yol ${MAX_PATH_BYTES} karakterden uzun olamaz.`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (relPath.includes('\\') || /[\u0000-\u001f\u007f]/.test(relPath)) {
|
|
114
|
+
add('unsafe_path', relPath, 'Yolda geçersiz karakter ya da kontrol karakteri var.');
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
29
117
|
const st = lstatSync(absPath);
|
|
30
118
|
if (st.isSymbolicLink() || (!st.isFile() && !st.isDirectory())) {
|
|
31
119
|
add('unsupported_entry', relPath, 'Sembolik bağ veya özel dosya desteklenmez.');
|
|
@@ -35,6 +123,11 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS) {
|
|
|
35
123
|
walk(absPath, relPath);
|
|
36
124
|
continue;
|
|
37
125
|
}
|
|
126
|
+
const ft = fileTypeIssue(relPath);
|
|
127
|
+
if (ft) {
|
|
128
|
+
add(ft.code, relPath, ft.message);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
38
131
|
const key = relPath.toLowerCase();
|
|
39
132
|
if (seen.has(key)) {
|
|
40
133
|
add('duplicate_path', relPath, 'Aynı yol büyük/küçük harf farkıyla birden fazla var.');
|
|
@@ -55,6 +148,14 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS) {
|
|
|
55
148
|
}
|
|
56
149
|
};
|
|
57
150
|
walk(dir, '');
|
|
151
|
+
const scan = scanSecrets(files, opts.scanIgnore);
|
|
152
|
+
for (const s of scan.issues)
|
|
153
|
+
add(s.code, s.path, s.message);
|
|
154
|
+
const warnings = [];
|
|
155
|
+
if (scan.ignored.length > 0)
|
|
156
|
+
warnings.push(`.enformikignore nedeniyle sır taraması atlanan dosya: ${scan.ignored.join(', ')}`);
|
|
157
|
+
if (scan.unscanned.length > 0)
|
|
158
|
+
warnings.push(`${SCAN_MAX_BYTES / 1048576} MB üstü olduğu için sır taraması yapılamayan dosya: ${scan.unscanned.join(', ')}`);
|
|
58
159
|
if (files.length > limits.maxFiles) {
|
|
59
160
|
add('too_many_files', undefined, `Arşivde en fazla ${limits.maxFiles} dosya olabilir (${files.length} bulundu).`);
|
|
60
161
|
}
|
|
@@ -70,5 +171,5 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS) {
|
|
|
70
171
|
add('missing_index', 'index.html', 'Dizinin kökünde index.html bulunmalı.');
|
|
71
172
|
}
|
|
72
173
|
}
|
|
73
|
-
return { files, unpackedBytes: total, issues };
|
|
174
|
+
return { files, unpackedBytes: total, issues, warnings };
|
|
74
175
|
}
|
package/dist/schema.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ApiError, CliError } from './errors.js';
|
|
2
2
|
import { ensureOk, joinUrl, request } from './http.js';
|
|
3
3
|
import { renderTable } from './output.js';
|
|
4
|
+
/** UTF-8 BOM (Windows editörleri) JSON.parse'ı bozar; sunucuya da gitmemeli. */
|
|
5
|
+
export const stripBom = (text) => text.replace(/^\uFEFF/, '');
|
|
4
6
|
export function parseManifest(text, path) {
|
|
5
7
|
let m;
|
|
6
8
|
try {
|
|
7
|
-
m = JSON.parse(text);
|
|
9
|
+
m = JSON.parse(stripBom(text));
|
|
8
10
|
}
|
|
9
11
|
catch (e) {
|
|
10
12
|
throw new CliError(`${path} geçerli JSON değil: ${e.message}`);
|
|
@@ -39,12 +41,25 @@ export function adminOnlySections(m, opts) {
|
|
|
39
41
|
out.push('rbac');
|
|
40
42
|
if (has(m.uiExtensions))
|
|
41
43
|
out.push('uiExtensions');
|
|
44
|
+
if (has(m.access))
|
|
45
|
+
out.push('access'); // erişim kuralı tanımlamak yönetici işidir
|
|
46
|
+
if (Array.isArray(m.scripts) && m.scripts.some((s) => s?.runAs === 'definer'))
|
|
47
|
+
out.push('scripts.runAs=definer');
|
|
42
48
|
return out;
|
|
43
49
|
}
|
|
44
|
-
const
|
|
45
|
-
/**
|
|
50
|
+
const NOT_PENDING = new Set(['none', 'kept_local']);
|
|
51
|
+
/**
|
|
52
|
+
* Uygulanacak (kalıcı) değişiklikler: yalnızca none/kept_local sayılmaz; veri içeren alanın silinmesi sunucuda atlanır.
|
|
53
|
+
* Bilinmeyen eylem (sunucu yeni bir tür eklerse, ör. rename) "değişiklik yok" sayılmaz — `diff --check` sessizce 0 vermesin.
|
|
54
|
+
*/
|
|
46
55
|
export function pendingChanges(diffs) {
|
|
47
|
-
return diffs.filter((d) =>
|
|
56
|
+
return diffs.filter((d) => !NOT_PENDING.has(d.action) && !(d.action === 'drop' && d.hasData));
|
|
57
|
+
}
|
|
58
|
+
const diffKey = (d) => `${d.resourceType}:${d.slug}:${d.action}`;
|
|
59
|
+
/** Uygulanan değişikliklerden, onaylanan (kuru çalıştırma) listede olmayanlar: diff ile upgrade arasında uygulama değişmiş. */
|
|
60
|
+
export function unapprovedChanges(approved, applied) {
|
|
61
|
+
const ok = new Set(pendingChanges(approved).map(diffKey));
|
|
62
|
+
return pendingChanges(applied).filter((d) => !ok.has(diffKey(d)));
|
|
48
63
|
}
|
|
49
64
|
export function typeChanges(diffs) {
|
|
50
65
|
return diffs.filter((d) => d.resourceType === 'field' && d.action === 'alter');
|
|
@@ -107,8 +122,11 @@ export class SchemaClient {
|
|
|
107
122
|
catch (e) {
|
|
108
123
|
throw describeSchemaError(e, { slug });
|
|
109
124
|
}
|
|
110
|
-
const j = res.json;
|
|
111
|
-
|
|
125
|
+
const j = (res.json ?? {});
|
|
126
|
+
// Beklenmeyen yanıt biçimi "/api/modules/undefined/diff" çağrısına dönüşmesin.
|
|
127
|
+
if (typeof j.id !== 'string' || !j.id)
|
|
128
|
+
throw new CliError(`Sunucu yanıtı beklenen biçimde değil: "${slug}" modülünün kimliği yok.`);
|
|
129
|
+
return { id: j.id, name: j.name ?? '', version: j.version ?? '' };
|
|
112
130
|
}
|
|
113
131
|
/** slug → kurulu modül ya da null (404 module_not_found); diğer hatalar fırlatılır. */
|
|
114
132
|
async findModule(slug) {
|
|
@@ -137,8 +155,44 @@ export class SchemaClient {
|
|
|
137
155
|
}
|
|
138
156
|
return normalizeDiff(res.json);
|
|
139
157
|
}
|
|
158
|
+
/** Entity erişim kurallarının özeti (kim hangi eylemi yapabilir; filtre içeriği yok). Eski sunucuda null. */
|
|
159
|
+
async accessSummary(entities) {
|
|
160
|
+
const q = entities.length > 0 ? `?entities=${encodeURIComponent(entities.join(','))}` : '';
|
|
161
|
+
const res = await request(joinUrl(this.o.appUrl, `/api/access-rules/summary${q}`), { headers: this.headers(), retries: 2, fetchImpl: this.o.fetchImpl });
|
|
162
|
+
if (res.status === 404 || res.status === 405)
|
|
163
|
+
return null;
|
|
164
|
+
try {
|
|
165
|
+
ensureOk(res);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
throw describeSchemaError(e, {});
|
|
169
|
+
}
|
|
170
|
+
const j = (res.json ?? {});
|
|
171
|
+
return { mode: j.mode ?? '', entities: Array.isArray(j.entities) ? j.entities : [] };
|
|
172
|
+
}
|
|
173
|
+
/** Sunucudaki doğrulayıcılarla manifesti denetler (DB'ye dokunmaz; salt okunur, yeniden denenebilir). */
|
|
174
|
+
async validate(manifestJson, opts) {
|
|
175
|
+
const q = [`profile=${opts.profile}`, opts.seedData ? 'seed_data=true' : ''].filter(Boolean).join('&');
|
|
176
|
+
const res = await request(joinUrl(this.o.appUrl, `/api/modules/validate?${q}`), {
|
|
177
|
+
method: 'POST', headers: this.headers({ 'Content-Type': 'application/json' }), body: manifestJson, retries: 1, timeoutMs: 120_000, fetchImpl: this.o.fetchImpl,
|
|
178
|
+
});
|
|
179
|
+
if (res.status === 404 || res.status === 405) {
|
|
180
|
+
throw new CliError('Bu uygulama sürümü sunucu tarafı doğrulamayı (/api/modules/validate) desteklemiyor. Uygulama güncellenene kadar `schema validate --offline` ile yalnızca yerel denetim yapın.', 1, 'validate_unsupported');
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
ensureOk(res);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
throw describeSchemaError(e, {});
|
|
187
|
+
}
|
|
188
|
+
const j = (res.json ?? {});
|
|
189
|
+
return {
|
|
190
|
+
valid: j.valid === true, profile: j.profile ?? opts.profile,
|
|
191
|
+
issues: Array.isArray(j.issues) ? j.issues : [], adminSections: Array.isArray(j.adminSections) ? j.adminSections : [],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
140
194
|
async diff(moduleId, manifestJson, opts) {
|
|
141
|
-
const res = await request(joinUrl(this.o.appUrl, `/api/modules/${moduleId}/diff${opts.seedData ? '?seed_data=true' : ''}`), {
|
|
195
|
+
const res = await request(joinUrl(this.o.appUrl, `/api/modules/${encodeURIComponent(moduleId)}/diff${opts.seedData ? '?seed_data=true' : ''}`), {
|
|
142
196
|
method: 'POST', headers: this.headers({ 'Content-Type': 'application/json' }), body: manifestJson, retries: 1, timeoutMs: 120_000, fetchImpl: this.o.fetchImpl,
|
|
143
197
|
});
|
|
144
198
|
try {
|
|
@@ -151,7 +205,7 @@ export class SchemaClient {
|
|
|
151
205
|
}
|
|
152
206
|
async upgrade(moduleId, manifestJson, opts) {
|
|
153
207
|
// retries: 0 — uygulama tekrar edilmemeli (ağ hatasında durumu `diff` ile kontrol edin).
|
|
154
|
-
const res = await request(joinUrl(this.o.appUrl, `/api/modules/${moduleId}/upgrade${opts.seedData ? '?seed_data=true' : ''}`), {
|
|
208
|
+
const res = await request(joinUrl(this.o.appUrl, `/api/modules/${encodeURIComponent(moduleId)}/upgrade${opts.seedData ? '?seed_data=true' : ''}`), {
|
|
155
209
|
method: 'POST', headers: this.headers({ 'Content-Type': 'application/json' }), body: manifestJson, timeoutMs: 300_000, fetchImpl: this.o.fetchImpl,
|
|
156
210
|
});
|
|
157
211
|
try {
|