@enformik/cli 0.4.0 → 0.6.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/README.md +72 -31
- package/dist/auth.js +13 -9
- package/dist/cli.js +56 -38
- package/dist/codegen.js +6 -6
- package/dist/commands/ai.js +55 -30
- package/dist/commands/app.js +90 -73
- package/dist/commands/codegen.js +9 -6
- package/dist/commands/link.js +12 -12
- package/dist/commands/schema.js +60 -40
- package/dist/commands/site.js +51 -29
- package/dist/commands/whoami.js +20 -15
- package/dist/config.js +4 -4
- package/dist/context.js +32 -17
- package/dist/errors.js +5 -1
- package/dist/http.js +41 -5
- package/dist/output.js +1 -1
- package/dist/precheck.js +90 -21
- package/dist/schema.js +50 -34
- package/dist/site.js +75 -20
- package/dist/zip.js +11 -5
- package/package.json +2 -2
package/dist/commands/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliError } from '../errors.js';
|
|
2
2
|
import { assertSecureUrl, joinUrl, request } from '../http.js';
|
|
3
3
|
import { DeployClient } from '../site.js';
|
|
4
|
-
import { prepare } from './schema.js';
|
|
4
|
+
import { assertExpectedAppHost, prepare } from './schema.js';
|
|
5
5
|
const ICON = { ok: '✓', warn: '!', fail: '✗', skip: '-' };
|
|
6
6
|
/** Web kullanıcısına (external) açık sayılan kural: rol "external" ya da özel rol; en az bir okuma eylemi. */
|
|
7
7
|
function grantsWebRead(r) {
|
|
@@ -14,9 +14,9 @@ function grantsWebRead(r) {
|
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* enformik app check [--entity a,b] — web
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* enformik app check [--entity a,b] — READ-ONLY check of the web app's integration health with Enformik.
|
|
18
|
+
* The site side (ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY) and the app side (ENFORMIK_APP_URL + client) are independent;
|
|
19
|
+
* a side without credentials is skipped. Exit: blocking problem 1, warnings only 2, all OK 0.
|
|
20
20
|
*/
|
|
21
21
|
export async function appCheck(ctx) {
|
|
22
22
|
const { settings, out } = ctx;
|
|
@@ -24,99 +24,116 @@ export async function appCheck(ctx) {
|
|
|
24
24
|
const hasSite = !!(settings.consoleUrl && settings.deployKey);
|
|
25
25
|
const hasApp = !!(settings.appUrl && settings.clientId && settings.clientSecret);
|
|
26
26
|
if (!hasSite && !hasApp) {
|
|
27
|
-
throw new CliError('
|
|
27
|
+
throw new CliError('No credentials to check. Set ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY for the site, and ENFORMIK_APP_URL + ENFORMIK_CLIENT_ID/SECRET for the app.');
|
|
28
28
|
}
|
|
29
|
+
// --expect-host is about the APP host (as in the schema commands); the site half ignores it. Checked first, so a mismatch stops the whole
|
|
30
|
+
// command (a hard error, not a check entry) before ANY request is sent, including the site half's GET /site.
|
|
31
|
+
if (settings.appUrl && typeof ctx.flags['expect-host'] === 'string')
|
|
32
|
+
assertExpectedAppHost(ctx, assertSecureUrl(settings.appUrl, 'App URL'));
|
|
29
33
|
// ── Site: yayınlanmış çalışma zamanı yapılandırması ──────────────────────────────────────────
|
|
30
34
|
if (hasSite) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let cfg;
|
|
38
|
-
try {
|
|
39
|
-
const res = await request(joinUrl(info.url, '/_enformik/config.json'), { retries: 1, fetchImpl: ctx.fetchImpl });
|
|
40
|
-
if (res.ok && res.json && typeof res.json === 'object')
|
|
41
|
-
cfg = res.json;
|
|
42
|
-
}
|
|
43
|
-
catch { /* ulaşılamadı → aşağıda uyarı */ }
|
|
44
|
-
if (!cfg) {
|
|
45
|
-
checks.push({ id: 'site.config', status: 'warn', message: `${info.url}/_enformik/config.json okunamadı.`, hint: 'Site yayında mı? Yeniden dağıtmayı deneyin.' });
|
|
35
|
+
if (typeof ctx.flags['expect-host'] === 'string')
|
|
36
|
+
out.warn('Note: --expect-host checks the app host only; the site side is not checked against it.');
|
|
37
|
+
try {
|
|
38
|
+
const info = await new DeployClient({ consoleUrl: settings.consoleUrl, deployKey: settings.deployKey, version: ctx.version, fetchImpl: ctx.fetchImpl }).getSite();
|
|
39
|
+
if (!info.live) {
|
|
40
|
+
checks.push({ id: 'site.live', status: 'warn', message: 'The site has no live deployment yet; the sign-in configuration could not be checked.', hint: 'Publish it with `enformik site deploy`.' });
|
|
46
41
|
}
|
|
47
42
|
else {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
assertSecureUrl(info.url, 'Site URL');
|
|
44
|
+
let cfg;
|
|
45
|
+
try {
|
|
46
|
+
const res = await request(joinUrl(info.url, '/_enformik/config.json'), { retries: 1, fetchImpl: ctx.fetchImpl });
|
|
47
|
+
if (res.ok && res.json && typeof res.json === 'object')
|
|
48
|
+
cfg = res.json;
|
|
49
|
+
}
|
|
50
|
+
catch { /* ulaşılamadı → aşağıda uyarı */ }
|
|
51
|
+
if (!cfg) {
|
|
52
|
+
checks.push({ id: 'site.config', status: 'warn', message: `Could not read ${info.url}/_enformik/config.json.`, hint: 'Is the site live? Try redeploying.' });
|
|
51
53
|
}
|
|
52
54
|
else {
|
|
53
|
-
|
|
55
|
+
const auth = cfg.auth;
|
|
56
|
+
if (!auth || !auth.clientId) {
|
|
57
|
+
checks.push({ id: 'site.auth', status: 'fail', message: 'Site sign-in is off: the published configuration has no sign-in (auth) information.', hint: 'Enable sign-in under Console → Developer site → Site sign-in, then redeploy the site.' });
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
checks.push({ id: 'site.auth', status: 'ok', message: 'Site sign-in is on; the published configuration has sign-in information.' });
|
|
61
|
+
}
|
|
62
|
+
const ok = typeof cfg.appUrl === 'string' && typeof cfg.apiUrl === 'string' && !!cfg.workspaceId;
|
|
63
|
+
checks.push(ok ? { id: 'site.binding', status: 'ok', message: 'The site is linked to an app and a workspace.' }
|
|
64
|
+
: { id: 'site.binding', status: 'fail', message: 'The configuration is missing the app/workspace link.', hint: 'Check the developer site\'s app link in the console.' });
|
|
54
65
|
}
|
|
55
|
-
const ok = typeof cfg.appUrl === 'string' && typeof cfg.apiUrl === 'string' && !!cfg.workspaceId;
|
|
56
|
-
checks.push(ok ? { id: 'site.binding', status: 'ok', message: 'Site bir uygulamaya ve çalışma alanına bağlı.' }
|
|
57
|
-
: { id: 'site.binding', status: 'fail', message: 'Yapılandırmada uygulama/çalışma alanı bağlantısı eksik.', hint: 'Konsolda geliştirici sitesinin uygulama bağlantısını kontrol edin.' });
|
|
58
66
|
}
|
|
59
67
|
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
// Bir yarının hatası diğer yarıyı ve checks[] çıktısını kaybettirmesin.
|
|
70
|
+
checks.push({ id: 'site.error', status: 'fail', message: `The site check could not be completed: ${e.message}` });
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
else {
|
|
62
|
-
checks.push({ id: 'site', status: 'skip', message: 'Site
|
|
74
|
+
checks.push({ id: 'site', status: 'skip', message: 'Site check skipped (ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY not set).' });
|
|
63
75
|
}
|
|
64
76
|
// ── Uygulama: modül, erişim modu ve web kullanıcısı erişimi ──────────────────────────────────
|
|
65
77
|
if (hasApp) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
else if (mod.version !== p.manifest.module.version) {
|
|
73
|
-
checks.push({ id: 'module.version', status: 'warn', message: `Uygulamadaki modül sürümü ${mod.version}, manifest ${p.manifest.module.version}.`, hint: '`enformik schema diff` / `schema push` ile eşitleyin.' });
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
checks.push({ id: 'module.version', status: 'ok', message: `Modül ${slug} ${mod.version} kurulu ve manifestle aynı sürümde.` });
|
|
77
|
-
}
|
|
78
|
-
const wanted = typeof ctx.flags.entity === 'string' && ctx.flags.entity !== ''
|
|
79
|
-
? ctx.flags.entity.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
80
|
-
: (p.manifest.entities ?? []).map((e) => (e.slug ?? '').toLowerCase()).filter(Boolean);
|
|
81
|
-
const summary = await p.client.accessSummary(wanted);
|
|
82
|
-
if (!summary) {
|
|
83
|
-
checks.push({ id: 'access', status: 'warn', message: 'Bu uygulama sürümü erişim özeti ucunu desteklemiyor; web kullanıcısı erişimi denetlenemedi.', hint: 'Uygulama güncellenince yeniden deneyin.' });
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
if (summary.mode !== 'on') {
|
|
87
|
-
checks.push({ id: 'access.mode', status: 'fail', message: `Kayıt erişim kuralları "${summary.mode || 'bilinmiyor'}" modunda; web kullanıcıları bu modda çalışmaz.`, hint: 'Etkinleştirilmesi için Enformik yönetim konsolundan destek kaydı açın.' });
|
|
78
|
+
try {
|
|
79
|
+
const p = await prepare(ctx, 'validate');
|
|
80
|
+
const slug = p.manifest.module.slug;
|
|
81
|
+
const mod = await p.client.findModule(slug);
|
|
82
|
+
if (!mod) {
|
|
83
|
+
checks.push({ id: 'module.installed', status: 'fail', message: `Module "${slug}" is not installed in the app.`, hint: 'Install it with `enformik schema install` (Schema builder).' });
|
|
88
84
|
}
|
|
89
|
-
else {
|
|
90
|
-
checks.push({ id: '
|
|
85
|
+
else if (mod.version !== p.manifest.module.version) {
|
|
86
|
+
checks.push({ id: 'module.version', status: 'warn', message: `The module version in the app is ${mod.version}; the manifest is ${p.manifest.module.version}.`, hint: 'Sync them with `enformik schema diff` / `schema push`.' });
|
|
91
87
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const custom = [];
|
|
95
|
-
for (const name of wanted) {
|
|
96
|
-
const kinds = (byName.get(name) ?? []).map(grantsWebRead);
|
|
97
|
-
if (kinds.includes('external'))
|
|
98
|
-
continue;
|
|
99
|
-
if (kinds.includes('custom'))
|
|
100
|
-
custom.push(name);
|
|
101
|
-
else
|
|
102
|
-
none.push(name);
|
|
103
|
-
}
|
|
104
|
-
if (wanted.length === 0) {
|
|
105
|
-
checks.push({ id: 'access.web', status: 'skip', message: 'Denetlenecek entity yok (manifestte entity ya da --entity yok).' });
|
|
88
|
+
else {
|
|
89
|
+
checks.push({ id: 'module.version', status: 'ok', message: `Module ${slug} ${mod.version} is installed and matches the manifest version.` });
|
|
106
90
|
}
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
const wanted = typeof ctx.flags.entity === 'string' && ctx.flags.entity !== ''
|
|
92
|
+
? ctx.flags.entity.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)
|
|
93
|
+
: (p.manifest.entities ?? []).map((e) => (e.slug ?? '').toLowerCase()).filter(Boolean);
|
|
94
|
+
const summary = await p.client.accessSummary(wanted);
|
|
95
|
+
if (!summary) {
|
|
96
|
+
checks.push({ id: 'access', status: 'warn', message: 'This app version does not support the access summary endpoint; web user access could not be checked.', hint: 'Try again once the app is updated.' });
|
|
109
97
|
}
|
|
110
98
|
else {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
99
|
+
if (summary.mode !== 'on') {
|
|
100
|
+
checks.push({ id: 'access.mode', status: 'fail', message: `Record access rules are in "${summary.mode || 'unknown'}" mode; web users do not work in this mode.`, hint: 'Open a support ticket from the Enformik management console to have it enabled.' });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
checks.push({ id: 'access.mode', status: 'ok', message: 'Record access rules are enabled.' });
|
|
104
|
+
}
|
|
105
|
+
const byName = new Map(summary.entities.map((e) => [e.entity.toLowerCase(), e.rules]));
|
|
106
|
+
const none = [];
|
|
107
|
+
const custom = [];
|
|
108
|
+
for (const name of wanted) {
|
|
109
|
+
const kinds = (byName.get(name) ?? []).map(grantsWebRead);
|
|
110
|
+
if (kinds.includes('external'))
|
|
111
|
+
continue;
|
|
112
|
+
if (kinds.includes('custom'))
|
|
113
|
+
custom.push(name);
|
|
114
|
+
else
|
|
115
|
+
none.push(name);
|
|
116
|
+
}
|
|
117
|
+
if (wanted.length === 0) {
|
|
118
|
+
checks.push({ id: 'access.web', status: 'skip', message: 'No entities to check (no entities in the manifest and no --entity).' });
|
|
119
|
+
}
|
|
120
|
+
else if (none.length > 0) {
|
|
121
|
+
checks.push({ id: 'access.web', status: 'warn', message: `Web users cannot see data in these tables (no read rule): ${none.join(', ')}.`, hint: 'An administrator must define a read rule for "Web User" (or a custom role) on the Record Access screen of those tables; a low-role client cannot apply access rules.' });
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
checks.push({ id: 'access.web', status: 'ok', message: `Read rules exist for web users (${wanted.length - custom.length} tables).` });
|
|
125
|
+
}
|
|
126
|
+
if (custom.length > 0) {
|
|
127
|
+
checks.push({ id: 'access.custom', status: 'warn', message: `These tables have only a custom role rule: ${custom.join(', ')}.`, hint: 'Make sure the relevant custom role is assigned to web users (the default role for new users, or an administrator assignment).' });
|
|
128
|
+
}
|
|
115
129
|
}
|
|
116
130
|
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
checks.push({ id: 'app.error', status: 'fail', message: `The app check could not be completed: ${e.message}` });
|
|
133
|
+
}
|
|
117
134
|
}
|
|
118
135
|
else {
|
|
119
|
-
checks.push({ id: 'app', status: 'skip', message: '
|
|
136
|
+
checks.push({ id: 'app', status: 'skip', message: 'App check skipped (ENFORMIK_APP_URL + ENFORMIK_CLIENT_ID/SECRET not set).' });
|
|
120
137
|
}
|
|
121
138
|
for (const c of checks) {
|
|
122
139
|
const paint = c.status === 'ok' ? 'green' : c.status === 'fail' ? 'red' : c.status === 'warn' ? 'yellow' : 'dim';
|
package/dist/commands/codegen.js
CHANGED
|
@@ -5,7 +5,7 @@ import { CliError } from '../errors.js';
|
|
|
5
5
|
import { parseManifest } from '../schema.js';
|
|
6
6
|
/**
|
|
7
7
|
* enformik codegen [--manifest module.json] [--out src/enformik.generated.ts] [--check]
|
|
8
|
-
*
|
|
8
|
+
* Generates TypeScript types + a typed data client from the local manifest; no network or credentials needed. --check: exit 2 if the file is out of date.
|
|
9
9
|
*/
|
|
10
10
|
export async function codegen(ctx) {
|
|
11
11
|
const manifestPath = resolve(ctx.cwd, ctx.settings.manifestPath);
|
|
@@ -14,26 +14,29 @@ export async function codegen(ctx) {
|
|
|
14
14
|
text = readFileSync(manifestPath, 'utf8');
|
|
15
15
|
}
|
|
16
16
|
catch {
|
|
17
|
-
throw new CliError(`
|
|
17
|
+
throw new CliError(`Could not read the manifest: ${manifestPath}`);
|
|
18
18
|
}
|
|
19
19
|
const manifest = parseManifest(text, ctx.settings.manifestPath);
|
|
20
20
|
const code = generateClient(manifest);
|
|
21
21
|
const outPath = resolve(ctx.cwd, ctx.settings.codegenOut);
|
|
22
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);
|
|
23
26
|
const slug = manifest.module.slug;
|
|
24
27
|
if (ctx.flags.check === true) {
|
|
25
|
-
const upToDate =
|
|
26
|
-
ctx.out.info(upToDate ? `✓ ${ctx.settings.codegenOut}
|
|
28
|
+
const upToDate = same;
|
|
29
|
+
ctx.out.info(upToDate ? `✓ ${ctx.settings.codegenOut} up to date (${slug}).` : `✗ ${ctx.settings.codegenOut} ${current === null ? 'is missing' : 'does not match the manifest'}; regenerate it with \`enformik codegen\`.`);
|
|
27
30
|
ctx.out.result({ module: slug, out: ctx.settings.codegenOut, upToDate });
|
|
28
31
|
return upToDate ? 0 : 2;
|
|
29
32
|
}
|
|
30
|
-
const changed =
|
|
33
|
+
const changed = !same;
|
|
31
34
|
if (changed) {
|
|
32
35
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
33
36
|
writeFileSync(outPath, code);
|
|
34
37
|
}
|
|
35
38
|
const n = (code.match(/^export interface \w+Input /gm) ?? []).length;
|
|
36
|
-
ctx.out.info(changed ? `✓ ${ctx.settings.codegenOut}
|
|
39
|
+
ctx.out.info(changed ? `✓ ${ctx.settings.codegenOut} written (${slug}, ${n} entities).` : `= ${ctx.settings.codegenOut} already up to date (${slug}, ${n} entities).`);
|
|
37
40
|
ctx.out.result({ module: slug, out: ctx.settings.codegenOut, entities: n, written: changed });
|
|
38
41
|
return 0;
|
|
39
42
|
}
|
package/dist/commands/link.js
CHANGED
|
@@ -15,10 +15,10 @@ export async function link(ctx) {
|
|
|
15
15
|
return cur;
|
|
16
16
|
return (await ctx.ask(label, cur)) || undefined;
|
|
17
17
|
};
|
|
18
|
-
next.consoleUrl = await prompt('
|
|
19
|
-
next.appUrl = await prompt('
|
|
20
|
-
next.workspaceId = await prompt('
|
|
21
|
-
const slug = await prompt('
|
|
18
|
+
next.consoleUrl = await prompt('Console URL (for site deployment; may be left empty)', config.consoleUrl, str('console-url'));
|
|
19
|
+
next.appUrl = await prompt('App URL (for schema upgrades; may be left empty)', config.appUrl, str('app-url'));
|
|
20
|
+
next.workspaceId = await prompt('Workspace ID (optional)', config.workspaceId, str('workspace'));
|
|
21
|
+
const slug = await prompt('Module slug (for schema upgrades)', config.module?.slug, str('module'));
|
|
22
22
|
const manifest = str('manifest') ?? config.module?.manifest;
|
|
23
23
|
const dir = str('dir') ?? config.site?.dir;
|
|
24
24
|
if (slug || manifest)
|
|
@@ -26,26 +26,26 @@ export async function link(ctx) {
|
|
|
26
26
|
if (dir)
|
|
27
27
|
next.site = { ...(config.site ?? {}), dir };
|
|
28
28
|
if (!next.consoleUrl && !next.appUrl)
|
|
29
|
-
throw new CliError('
|
|
29
|
+
throw new CliError('At least one URL is required: --console-url (site deployment) and/or --app-url (schema upgrades).');
|
|
30
30
|
if (next.consoleUrl)
|
|
31
|
-
assertSecureUrl(next.consoleUrl, '
|
|
31
|
+
assertSecureUrl(next.consoleUrl, 'Console URL');
|
|
32
32
|
if (next.appUrl) {
|
|
33
|
-
assertSecureUrl(next.appUrl, '
|
|
33
|
+
assertSecureUrl(next.appUrl, 'App URL');
|
|
34
34
|
if (flags['no-verify'] !== true) {
|
|
35
35
|
await discoverApp(next.appUrl, ctx.version, ctx.fetchImpl); // erişilebilir mi / API istemcisi kabul ediyor mu
|
|
36
|
-
out.info('✓
|
|
36
|
+
out.info('✓ The app is reachable.');
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
for (const k of Object.keys(next))
|
|
40
40
|
if (next[k] === undefined)
|
|
41
41
|
delete next[k];
|
|
42
42
|
const path = saveConfig(root, next);
|
|
43
|
-
out.info(`
|
|
44
|
-
out.info('
|
|
43
|
+
out.info(`Config written: ${path}`);
|
|
44
|
+
out.info('Secrets are not written to the file; provide them as environment variables:');
|
|
45
45
|
if (next.consoleUrl)
|
|
46
|
-
out.info(' ENFORMIK_DEPLOY_KEY (site
|
|
46
|
+
out.info(' ENFORMIK_DEPLOY_KEY (site deployment: deploy key)');
|
|
47
47
|
if (next.appUrl)
|
|
48
|
-
out.info(' ENFORMIK_CLIENT_ID / ENFORMIK_CLIENT_SECRET (
|
|
48
|
+
out.info(' ENFORMIK_CLIENT_ID / ENFORMIK_CLIENT_SECRET (schema upgrades: API client)');
|
|
49
49
|
out.result({ linked: true, path, config: next });
|
|
50
50
|
return 0;
|
|
51
51
|
}
|
package/dist/commands/schema.js
CHANGED
|
@@ -3,56 +3,64 @@ 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
7
|
import { renderTable } from '../output.js';
|
|
8
|
+
/** --expect-host: the app's host name must match (case-insensitive); checked before any request so nothing reaches the wrong app. */
|
|
9
|
+
export function assertExpectedAppHost(ctx, appUrl) {
|
|
10
|
+
const expect = ctx.flags['expect-host'];
|
|
11
|
+
if (typeof expect === 'string' && expect.toLowerCase() !== appUrl.hostname.toLowerCase()) {
|
|
12
|
+
throw new CliError(`Expected app "${expect}" but the target is "${appUrl.hostname}". Stopped so that nothing is applied to the wrong app.`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
8
15
|
/** Ortak hazırlık: manifest → uygulama keşfi → belirteç → modül kimliği. */
|
|
9
16
|
export async function prepare(ctx, mode = 'existing') {
|
|
10
17
|
const { settings, out } = ctx;
|
|
11
18
|
if (!settings.appUrl)
|
|
12
|
-
throw new CliError('
|
|
19
|
+
throw new CliError('No app URL. Run `enformik link --app-url https://…` or set ENFORMIK_APP_URL.');
|
|
13
20
|
if (!settings.clientId || !settings.clientSecret) {
|
|
14
|
-
throw new CliError('ENFORMIK_CLIENT_ID
|
|
15
|
-
}
|
|
16
|
-
const appUrl = assertSecureUrl(settings.appUrl, 'Uygulama adresi');
|
|
17
|
-
const expect = ctx.flags['expect-host'];
|
|
18
|
-
if (typeof expect === 'string' && expect.toLowerCase() !== appUrl.hostname.toLowerCase()) {
|
|
19
|
-
throw new CliError(`Beklenen uygulama "${expect}" ama hedef "${appUrl.hostname}". Yanlış uygulamaya işlem yapılmaması için durduruldu.`);
|
|
21
|
+
throw new CliError('ENFORMIK_CLIENT_ID and ENFORMIK_CLIENT_SECRET must be set (in the console, create a client with the "Schema upgrader" role — or "Schema builder" for the first installation — on the "API keys" page).');
|
|
20
22
|
}
|
|
23
|
+
const appUrl = assertSecureUrl(settings.appUrl, 'App URL');
|
|
24
|
+
assertExpectedAppHost(ctx, appUrl);
|
|
21
25
|
const manifestPath = resolve(ctx.cwd, settings.manifestPath);
|
|
22
26
|
let text;
|
|
23
27
|
try {
|
|
24
|
-
text = readFileSync(manifestPath, 'utf8');
|
|
28
|
+
text = stripBom(readFileSync(manifestPath, 'utf8'));
|
|
25
29
|
}
|
|
26
30
|
catch {
|
|
27
|
-
throw new CliError(`
|
|
31
|
+
throw new CliError(`Could not read the manifest: ${manifestPath}`);
|
|
28
32
|
}
|
|
29
33
|
const manifest = parseManifest(text, settings.manifestPath);
|
|
34
|
+
// Sunucuya dosyanın kendi metni gider (JSON.stringify yeniden serileştirmesi büyük sayıları/biçimi değiştirebilirdi).
|
|
35
|
+
const manifestJson = text;
|
|
30
36
|
const slug = settings.moduleSlug ?? manifest.module.slug;
|
|
31
37
|
if (settings.moduleSlug && settings.moduleSlug !== manifest.module.slug) {
|
|
32
|
-
throw new CliError(`
|
|
38
|
+
throw new CliError(`The module in the config ("${settings.moduleSlug}") differs from the slug in the manifest ("${manifest.module.slug}").`);
|
|
33
39
|
}
|
|
34
40
|
const seedData = ctx.flags['seed-data'] === true;
|
|
35
41
|
const admin = adminOnlySections(manifest, { seedData });
|
|
36
42
|
if (admin.length > 0) {
|
|
37
|
-
out.warn(`
|
|
43
|
+
out.warn(`Note: the manifest contains sections that require an administrator (${admin.join(', ')}). A client with the "Schema upgrader/builder" role cannot apply them.`);
|
|
38
44
|
}
|
|
39
45
|
const app = await discoverApp(settings.appUrl, ctx.version, ctx.fetchImpl);
|
|
40
46
|
const workspaceId = settings.workspaceId || app.tenantId;
|
|
41
47
|
if (!workspaceId)
|
|
42
|
-
throw new CliError('
|
|
48
|
+
throw new CliError('Could not determine the workspace. Pass --workspace <id> or save it with `enformik link --workspace`.');
|
|
49
|
+
// 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.
|
|
50
|
+
out.info(`Identity provider: ${new URL(app.oidcAuthority).host} (the client credentials are sent here)`);
|
|
43
51
|
const tok = await fetchAccessToken(app, settings.clientId, settings.clientSecret, ctx.version, ctx.fetchImpl);
|
|
44
52
|
const client = new SchemaClient({ appUrl: settings.appUrl, token: tok.token, workspaceId, version: ctx.version, fetchImpl: ctx.fetchImpl });
|
|
45
53
|
if (mode === 'validate') {
|
|
46
|
-
return { client, manifest, manifestJson
|
|
54
|
+
return { client, manifest, manifestJson, moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
|
|
47
55
|
}
|
|
48
56
|
if (mode === 'install') {
|
|
49
57
|
const existing = await client.findModule(slug);
|
|
50
58
|
if (existing)
|
|
51
|
-
throw new CliError(`"${slug}"
|
|
52
|
-
return { client, manifest, manifestJson
|
|
59
|
+
throw new CliError(`Module "${slug}" is already installed in this app (version ${existing.version}). Use \`enformik schema push\` to upgrade it.`);
|
|
60
|
+
return { client, manifest, manifestJson, moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
|
|
53
61
|
}
|
|
54
62
|
const mod = await client.resolveModule(slug);
|
|
55
|
-
return { client, manifest, manifestJson
|
|
63
|
+
return { client, manifest, manifestJson, moduleId: mod.id, installedVersion: mod.version, appHost: appUrl.hostname, seedData };
|
|
56
64
|
}
|
|
57
65
|
function summary(p, r) {
|
|
58
66
|
return {
|
|
@@ -69,7 +77,7 @@ function summary(p, r) {
|
|
|
69
77
|
export async function schemaDiff(ctx) {
|
|
70
78
|
const p = await prepare(ctx);
|
|
71
79
|
const r = await p.client.diff(p.moduleId, p.manifestJson, { seedData: p.seedData });
|
|
72
|
-
ctx.out.info(`
|
|
80
|
+
ctx.out.info(`Module: ${p.manifest.module.slug} installed ${p.installedVersion} → manifest ${p.manifest.module.version}`);
|
|
73
81
|
ctx.out.info(renderDiff(ctx.out, r, { appHost: p.appHost }));
|
|
74
82
|
const s = summary(p, r);
|
|
75
83
|
ctx.out.result(s);
|
|
@@ -83,7 +91,7 @@ export async function schemaDiff(ctx) {
|
|
|
83
91
|
export async function schemaPush(ctx) {
|
|
84
92
|
const p = await prepare(ctx);
|
|
85
93
|
const dry = await p.client.diff(p.moduleId, p.manifestJson, { seedData: p.seedData });
|
|
86
|
-
ctx.out.info(`
|
|
94
|
+
ctx.out.info(`Module: ${p.manifest.module.slug} installed ${p.installedVersion} → manifest ${p.manifest.module.version}`);
|
|
87
95
|
ctx.out.info(renderDiff(ctx.out, dry, { appHost: p.appHost }));
|
|
88
96
|
if (dry.errors.length > 0) {
|
|
89
97
|
ctx.out.result({ ...summary(p, dry), applied: false });
|
|
@@ -92,18 +100,18 @@ export async function schemaPush(ctx) {
|
|
|
92
100
|
const pending = pendingChanges(dry.diffs);
|
|
93
101
|
// Sürüm farkı olsa bile yapısal değişiklik yoksa uygulanacak bir şey yok.
|
|
94
102
|
if (pending.length === 0) {
|
|
95
|
-
ctx.out.info('
|
|
103
|
+
ctx.out.info('No changes to apply.');
|
|
96
104
|
ctx.out.result({ ...summary(p, dry), applied: false });
|
|
97
105
|
return 0;
|
|
98
106
|
}
|
|
99
107
|
const tc = typeChanges(dry.diffs);
|
|
100
108
|
if (tc.length > 0)
|
|
101
|
-
ctx.out.warn(`
|
|
109
|
+
ctx.out.warn(`Field type is changing (${tc.map((d) => d.slug).join(', ')}): this can only be applied with the administrator role.`);
|
|
102
110
|
if (ctx.flags.yes !== true) {
|
|
103
111
|
if (!ctx.isTTY)
|
|
104
|
-
throw new CliError('
|
|
105
|
-
if (!(await ctx.confirm('
|
|
106
|
-
ctx.out.info('
|
|
112
|
+
throw new CliError('No interactive terminal: pass --yes to apply the changes (nothing is confirmed silently).');
|
|
113
|
+
if (!(await ctx.confirm('Do you want to apply the changes?'))) {
|
|
114
|
+
ctx.out.info('Cancelled; no changes were applied.');
|
|
107
115
|
ctx.out.result({ ...summary(p, dry), applied: false });
|
|
108
116
|
return 1;
|
|
109
117
|
}
|
|
@@ -111,20 +119,27 @@ export async function schemaPush(ctx) {
|
|
|
111
119
|
const res = await p.client.upgrade(p.moduleId, p.manifestJson, { seedData: p.seedData });
|
|
112
120
|
if (res.errors.length > 0) {
|
|
113
121
|
for (const e of res.errors)
|
|
114
|
-
ctx.out.error(`
|
|
122
|
+
ctx.out.error(`Error: ${e}`);
|
|
115
123
|
ctx.out.result({ ...summary(p, res), applied: false });
|
|
116
124
|
return 1;
|
|
117
125
|
}
|
|
118
|
-
ctx.out.info(ctx.out.paint('green', `✓
|
|
126
|
+
ctx.out.info(ctx.out.paint('green', `✓ Applied: ${pendingChanges(res.diffs).length || pending.length} changes (module ${p.manifest.module.slug} ${p.manifest.module.version}).`));
|
|
127
|
+
// 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.
|
|
128
|
+
const extra = unapprovedChanges(dry.diffs, res.diffs);
|
|
129
|
+
if (extra.length > 0) {
|
|
130
|
+
ctx.out.error(`Warning: ${extra.length} change(s) that were not in the diff you approved were also applied (the app may have changed between diff and upgrade): ${extra.map((d) => `${d.resourceType} ${d.slug} (${d.action})`).join(', ')}. Verify the result with \`enformik schema diff\`.`);
|
|
131
|
+
ctx.out.result({ ...summary(p, res), applied: true, diverged: true, unapproved: extra });
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
119
134
|
ctx.out.result({ ...summary(p, res), applied: true });
|
|
120
135
|
return 0;
|
|
121
136
|
}
|
|
122
|
-
/** enformik schema install [--yes] [--dry-run] —
|
|
137
|
+
/** enformik schema install [--yes] [--dry-run] — first installation of the module (role: Schema builder or Administrator). */
|
|
123
138
|
export async function schemaInstall(ctx) {
|
|
124
139
|
const p = await prepare(ctx, 'install');
|
|
125
140
|
const slug = p.manifest.module.slug;
|
|
126
141
|
const dry = await p.client.install(p.manifestJson, { seedData: p.seedData, dryRun: true });
|
|
127
|
-
ctx.out.info(`
|
|
142
|
+
ctx.out.info(`Module: ${slug} new installation → manifest ${p.manifest.module.version}`);
|
|
128
143
|
ctx.out.info(renderDiff(ctx.out, dry, { appHost: p.appHost }));
|
|
129
144
|
const base = { module: slug, manifestVersion: p.manifest.module.version, changes: pendingChanges(dry.diffs).length, diffs: dry.diffs, errors: dry.errors };
|
|
130
145
|
if (dry.errors.length > 0) {
|
|
@@ -137,9 +152,9 @@ export async function schemaInstall(ctx) {
|
|
|
137
152
|
}
|
|
138
153
|
if (ctx.flags.yes !== true) {
|
|
139
154
|
if (!ctx.isTTY)
|
|
140
|
-
throw new CliError('
|
|
141
|
-
if (!(await ctx.confirm('
|
|
142
|
-
ctx.out.info('
|
|
155
|
+
throw new CliError('No interactive terminal: pass --yes to install the module (nothing is confirmed silently).');
|
|
156
|
+
if (!(await ctx.confirm('Do you want to install the module?'))) {
|
|
157
|
+
ctx.out.info('Cancelled; nothing was installed.');
|
|
143
158
|
ctx.out.result({ ...base, installed: false });
|
|
144
159
|
return 1;
|
|
145
160
|
}
|
|
@@ -147,11 +162,11 @@ export async function schemaInstall(ctx) {
|
|
|
147
162
|
const res = await p.client.install(p.manifestJson, { seedData: p.seedData, dryRun: false });
|
|
148
163
|
if (res.errors.length > 0) {
|
|
149
164
|
for (const e of res.errors)
|
|
150
|
-
ctx.out.error(`
|
|
165
|
+
ctx.out.error(`Error: ${e}`);
|
|
151
166
|
ctx.out.result({ ...base, diffs: res.diffs, errors: res.errors, installed: false });
|
|
152
167
|
return 1;
|
|
153
168
|
}
|
|
154
|
-
ctx.out.info(ctx.out.paint('green', `✓
|
|
169
|
+
ctx.out.info(ctx.out.paint('green', `✓ Installed: module ${slug} ${p.manifest.module.version} (${pendingChanges(res.diffs).length} resources). Use \`enformik schema push\` for later changes.`));
|
|
155
170
|
ctx.out.result({ ...base, moduleId: res.moduleId, diffs: res.diffs, errors: [], installed: true });
|
|
156
171
|
return 0;
|
|
157
172
|
}
|
|
@@ -163,7 +178,7 @@ export async function schemaInstall(ctx) {
|
|
|
163
178
|
export async function schemaValidate(ctx) {
|
|
164
179
|
const profileFlag = ctx.flags.profile;
|
|
165
180
|
if (profileFlag !== undefined && profileFlag !== 'import' && profileFlag !== 'ai') {
|
|
166
|
-
throw new CliError(`--profile "import"
|
|
181
|
+
throw new CliError(`--profile must be "import" or "ai" (got: ${String(profileFlag)}).`);
|
|
167
182
|
}
|
|
168
183
|
const profile = profileFlag === 'ai' ? 'ai' : 'import';
|
|
169
184
|
const seedData = ctx.flags['seed-data'] === true;
|
|
@@ -171,19 +186,22 @@ export async function schemaValidate(ctx) {
|
|
|
171
186
|
let source;
|
|
172
187
|
let slug;
|
|
173
188
|
if (ctx.flags.offline === true) {
|
|
189
|
+
// ai profilinin kuralları (isimlendirme/yapı) yalnızca sunucuda denetlenir; yerel denetimle "geçerli" demek yanlış güven verir.
|
|
190
|
+
if (profileFlag === 'ai')
|
|
191
|
+
throw new CliError('--offline cannot be combined with --profile ai: the ai profile rules are checked only on the server. Run without --offline.');
|
|
174
192
|
const manifestPath = resolve(ctx.cwd, ctx.settings.manifestPath);
|
|
175
193
|
let text;
|
|
176
194
|
try {
|
|
177
195
|
text = readFileSync(manifestPath, 'utf8');
|
|
178
196
|
}
|
|
179
197
|
catch {
|
|
180
|
-
throw new CliError(`
|
|
198
|
+
throw new CliError(`Could not read the manifest: ${manifestPath}`);
|
|
181
199
|
}
|
|
182
200
|
const manifest = parseManifest(text, ctx.settings.manifestPath); // JSON + module.slug/version
|
|
183
201
|
slug = manifest.module.slug;
|
|
184
202
|
result = { valid: true, profile, issues: [], adminSections: adminOnlySections(manifest, { seedData }).map((section) => ({ section })) };
|
|
185
203
|
source = 'local';
|
|
186
|
-
ctx.out.warn('
|
|
204
|
+
ctx.out.warn('Only local checks were run (JSON, module.slug/version, administrator sections); naming and structure rules are checked on the server.');
|
|
187
205
|
}
|
|
188
206
|
else {
|
|
189
207
|
const p = await prepare(ctx, 'validate');
|
|
@@ -191,14 +209,16 @@ export async function schemaValidate(ctx) {
|
|
|
191
209
|
result = await p.client.validate(p.manifestJson, { profile, seedData });
|
|
192
210
|
source = 'server';
|
|
193
211
|
}
|
|
194
|
-
ctx.out.info(`
|
|
212
|
+
ctx.out.info(`Module: ${slug} profile: ${profile} (${source === 'server' ? 'server validation' : 'local'})`);
|
|
195
213
|
if (result.issues.length > 0) {
|
|
196
|
-
ctx.out.info(renderTable(ctx.out, ['
|
|
214
|
+
ctx.out.info(renderTable(ctx.out, ['CODE', 'DESCRIPTION'], result.issues.map((i) => [i.code, i.message])));
|
|
197
215
|
}
|
|
198
216
|
if (result.adminSections.length > 0) {
|
|
199
|
-
ctx.out.info(ctx.out.paint('yellow', `
|
|
217
|
+
ctx.out.info(ctx.out.paint('yellow', `Administrator step required (a low-role client cannot apply these): ${result.adminSections.map((s) => s.section).join(', ')}`));
|
|
200
218
|
}
|
|
201
|
-
ctx.out.info(result.valid
|
|
219
|
+
ctx.out.info(result.valid
|
|
220
|
+
? ctx.out.paint('green', source === 'local' ? '✓ Local checks passed (server rules were not checked).' : '✓ Manifest is valid.')
|
|
221
|
+
: ctx.out.paint('red', `✗ ${result.issues.length} issue(s) found.`));
|
|
202
222
|
ctx.out.result({ module: slug, source, ...result });
|
|
203
223
|
return result.valid ? 0 : 1;
|
|
204
224
|
}
|