@rex0220/formula-authoring-tools 0.1.0 → 0.2.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 +4 -2
- package/dist/cli.mjs +33 -11
- package/dist/core.mjs +1361 -35
- package/dist/eval-worker.mjs +21 -8
- package/dist/types/commands/save.d.ts +82 -0
- package/dist/types/commands/take.d.ts +18 -0
- package/dist/types/commit-file.d.ts +38 -0
- package/dist/types/connections.d.ts +106 -0
- package/dist/types/core.d.ts +19 -11
- package/dist/types/engine.d.ts +4 -0
- package/dist/types/permission-error.d.ts +3 -0
- package/dist/types/permission.d.ts +10 -0
- package/dist/types/worker/eval-job.d.ts +18 -0
- package/dist/types/worker/run.d.ts +4 -0
- package/dist/types/workspace.d.ts +126 -0
- package/package.json +2 -1
package/dist/core.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @rex0220/formula-authoring-tools/core 0.
|
|
1
|
+
/*! @rex0220/formula-authoring-tools/core 0.2.0 (c) rex0220. MIT License. 計算式エンジンと計算式プラグインのコードは利用者の計算式プラグインプラグインの zip から読む(このファイルには含まれない)。commit 1e50847. */
|
|
2
2
|
|
|
3
3
|
// src/safe-path.ts
|
|
4
4
|
import { lstatSync, realpathSync } from "node:fs";
|
|
@@ -56,8 +56,8 @@ function resolveRead(target, cwd) {
|
|
|
56
56
|
const real = realResolve(target, cwd);
|
|
57
57
|
const cwdReal = realResolve(".", cwd);
|
|
58
58
|
if (!isInside(real, cwdReal)) throw new PathError(`読むファイルは作業フォルダーの中に置く: ${target}`);
|
|
59
|
-
const
|
|
60
|
-
if (/^\.env(\.|$)/i.test(path.basename(real)) || /^(node_modules|\.git)(\/|$)/i.test(
|
|
59
|
+
const rel2 = path.relative(cwdReal, real).replace(/\\/g, "/");
|
|
60
|
+
if (/^\.env(\.|$)/i.test(path.basename(real)) || /^(node_modules|\.git)(\/|$)/i.test(rel2)) throw new PathError(`このファイルは tools では読まない: ${target}`);
|
|
61
61
|
return real;
|
|
62
62
|
}
|
|
63
63
|
function resolveWrite(target, roots, cwd) {
|
|
@@ -74,6 +74,14 @@ function resolveWrite(target, roots, cwd) {
|
|
|
74
74
|
if (WINDOWS_RESERVED.test(base) || /[. ]$/.test(base)) throw new PathError(`使えないファイル名: ${base}`);
|
|
75
75
|
return real;
|
|
76
76
|
}
|
|
77
|
+
function resolveWriteDir(target, roots, cwd) {
|
|
78
|
+
if (!target || typeof target !== "string") throw new PathError("フォルダーのパスが要る");
|
|
79
|
+
const real = realResolve(target, cwd);
|
|
80
|
+
const cwdReal = realResolve(".", cwd);
|
|
81
|
+
if (!isInside(real, cwdReal)) throw new PathError(`出力フォルダーは作業フォルダーの中: ${target}`);
|
|
82
|
+
if (!roots.some((r) => isInside(real, path.join(cwdReal, r)))) throw new PathError(`出力フォルダーは ${roots.map((r) => `${r}/`).join(" か ")}(またはその下): ${target}`);
|
|
83
|
+
return real;
|
|
84
|
+
}
|
|
77
85
|
function safeFileName(name, fallback) {
|
|
78
86
|
let s = name.replace(/[\\/:*?"<>|\u0000-\u001f\u007f]/g, "_").replace(/[. ]+$/g, "").trim();
|
|
79
87
|
if (!s || WINDOWS_RESERVED.test(s)) s = fallback;
|
|
@@ -182,7 +190,7 @@ function devMeta() {
|
|
|
182
190
|
return { toolsVersion: pkg.version, pluginId: FORMULA_PLUGIN_ID, minPluginVersion: MIN_PLUGIN_VERSION, supportedApiVersions: [...SUPPORTED_API_VERSIONS], commit: gitCommit(AUTHORING_ROOT), builtAt: "", mode: "dev" };
|
|
183
191
|
}
|
|
184
192
|
function toolsMeta() {
|
|
185
|
-
if (true) return JSON.parse('{"toolsVersion":"0.
|
|
193
|
+
if (true) return JSON.parse('{"toolsVersion":"0.2.0","pluginId":"dhgihghfgfmibkdldkeibboabfnmdpke","minPluginVersion":101,"supportedApiVersions":[1],"commit":"1e50847","builtAt":"2026-10-18T02:11:59.644Z","mode":"build"}');
|
|
186
194
|
return devMeta();
|
|
187
195
|
}
|
|
188
196
|
|
|
@@ -491,6 +499,9 @@ function summarizeUsers(src, origin, user) {
|
|
|
491
499
|
}
|
|
492
500
|
|
|
493
501
|
// src/engine.ts
|
|
502
|
+
function momentFileOf(opt) {
|
|
503
|
+
return opt.vendorDir ? path4.join(opt.vendorDir, path4.basename(MOMENT_FILE)) : MOMENT_FILE;
|
|
504
|
+
}
|
|
494
505
|
var DEFAULT_CONTEXT_BASE_URL = "https://example.cybozu.com";
|
|
495
506
|
var SENTINEL_USER = { id: "0", code: "__sentinel__", name: "", email: "", timezone: "Asia/Tokyo", language: "ja", isGuest: false };
|
|
496
507
|
var loaded = null;
|
|
@@ -523,7 +534,7 @@ function resolvePluginSources(opt = {}, mode = opt.mode ?? toolsMeta().mode) {
|
|
|
523
534
|
throw new PluginZipError(`計算式プラグインの zip の場所が分からない。.env に FORMULA_PLUGIN_ZIP=<計算式プラグインの zip のパス> を書く${mode === "dev" ? "(開発中に隣の formula の prod/ を読むなら環境変数 FORMULA_ALLOW_DEV_PLUGIN=1)" : ""}`, "not-configured");
|
|
524
535
|
}
|
|
525
536
|
if (!dev || !existsSync(path4.join(dev, ENGINE_ENTRY))) throw new PluginZipError(`計算式プラグインの zip の場所が分からない(FORMULA_ALLOW_DEV_PLUGIN=1 だが formula の prod/ が無い: ${dev})`, "not-configured");
|
|
526
|
-
const read = (
|
|
537
|
+
const read = (rel2) => readFileSync3(path4.join(dev, rel2), "utf8");
|
|
527
538
|
const apiFile = path4.join(dev, API_ENTRY);
|
|
528
539
|
const decimalFile = path4.join(dev, DECIMAL_ENTRY);
|
|
529
540
|
const engine = read(ENGINE_ENTRY);
|
|
@@ -590,15 +601,16 @@ async function loadOnce(opt) {
|
|
|
590
601
|
}
|
|
591
602
|
if (!src.api) throw new PluginZipError(`計算式プラグインの zip に ${API_ENTRY} が無い(Ver.${MIN_PLUGIN_VERSION} 以降の zip が要る): ${src.from}`, "no-api", known);
|
|
592
603
|
if (!src.decimal) warnings.push(`zip に ${DECIMAL_ENTRY} が無い(べき乗の小数の指数がエラーになる)`);
|
|
593
|
-
|
|
604
|
+
const momentFile = momentFileOf(opt);
|
|
605
|
+
if (!existsSync(momentFile)) throw new Error(`moment が無い: ${momentFile}(tools の npm install 時に取る。手で取るなら npm run vendor)`);
|
|
594
606
|
try {
|
|
595
|
-
return await startEngine({ ...src, api: src.api }, known, warnings, stamp);
|
|
607
|
+
return await startEngine({ ...src, api: src.api }, known, warnings, stamp, momentFile);
|
|
596
608
|
} catch (e) {
|
|
597
609
|
poisoned = { code: e instanceof PluginZipError ? e.code : "unexpected", info: e instanceof PluginZipError ? e.info : known };
|
|
598
610
|
throw e;
|
|
599
611
|
}
|
|
600
612
|
}
|
|
601
|
-
async function startEngine(src, known, warnings, stamp) {
|
|
613
|
+
async function startEngine(src, known, warnings, stamp, momentFile) {
|
|
602
614
|
const g = globalThis;
|
|
603
615
|
const { Window } = await import("happy-dom");
|
|
604
616
|
const ctx = {
|
|
@@ -674,7 +686,7 @@ async function startEngine(src, known, warnings, stamp) {
|
|
|
674
686
|
throw new PluginZipError(`計算式プラグインの zip のコード(${entry})を実行できない: ${e instanceof Error ? e.message : String(e)}`, "engine-unreadable", known);
|
|
675
687
|
}
|
|
676
688
|
};
|
|
677
|
-
runScript(readFileSync3(
|
|
689
|
+
runScript(readFileSync3(momentFile, "utf8"), momentFile);
|
|
678
690
|
runScript(src.momentTimezone, `${src.from}!${MOMENT_TZ_ENTRY}`, MOMENT_TZ_ENTRY);
|
|
679
691
|
runScript(src.bignumber, `${src.from}!${BIGNUMBER_ENTRY}`, BIGNUMBER_ENTRY);
|
|
680
692
|
if (src.decimal) runScript(src.decimal, `${src.from}!${DECIMAL_ENTRY}`, DECIMAL_ENTRY);
|
|
@@ -853,6 +865,17 @@ function loadAuth(opt) {
|
|
|
853
865
|
if (!token && !(username && password)) throw new AuthError("KINTONE_API_TOKEN か、KINTONE_USERNAME と KINTONE_PASSWORD の両方が要る(OS の環境変数か .env)");
|
|
854
866
|
return { baseUrl, token, username: token ? void 0 : username, password: token ? void 0 : password };
|
|
855
867
|
}
|
|
868
|
+
function loadUserApiAuth(opt) {
|
|
869
|
+
const pick = picker(opt);
|
|
870
|
+
const baseUrl = baseUrlFromEnv(opt);
|
|
871
|
+
if (!baseUrl) throw new AuthError(`KINTONE_BASE_URL が無い(OS の環境変数か .env: ${envFileOf(opt)})`);
|
|
872
|
+
const bearer = pick(NAMES.userApiToken);
|
|
873
|
+
if (bearer) return { baseUrl, bearer };
|
|
874
|
+
const username = pick(NAMES.username);
|
|
875
|
+
const password = pick(NAMES.password);
|
|
876
|
+
if (username && password) return { baseUrl, username, password };
|
|
877
|
+
throw new AuthError("User API(users --pull)は FORMULA_USER_API_TOKEN(cybozu.com 共通管理の API トークン)か、KINTONE_USERNAME と KINTONE_PASSWORD(2 要素認証のないアカウント)で呼ぶ。kintone アプリの API トークン(KINTONE_API_TOKEN)では呼べない");
|
|
878
|
+
}
|
|
856
879
|
function pluginZipPath(opt) {
|
|
857
880
|
const picked = pickerWithSource(opt, true)(NAMES.pluginZip);
|
|
858
881
|
if (!picked) return void 0;
|
|
@@ -890,6 +913,11 @@ function jsonErrorWhere(e) {
|
|
|
890
913
|
if (!m) return "";
|
|
891
914
|
return m[2] ? `(${m[2]} 行 ${m[3]} 列)` : `(位置 ${m[1]})`;
|
|
892
915
|
}
|
|
916
|
+
function jsonErrorText(e) {
|
|
917
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
918
|
+
if (e instanceof SyntaxError || /is not valid JSON|in JSON at position|Unexpected (token|end|non-whitespace)|Bad control character|Unterminated string/.test(msg)) return `JSON の構文の誤り${jsonErrorWhere(e)}(中身は表示しない)`;
|
|
919
|
+
return msg;
|
|
920
|
+
}
|
|
893
921
|
|
|
894
922
|
// src/input.ts
|
|
895
923
|
var PLUGIN_NAME = "計算式プラグイン";
|
|
@@ -939,8 +967,8 @@ function readFieldsFile(file) {
|
|
|
939
967
|
return fields;
|
|
940
968
|
}
|
|
941
969
|
function relativeSettingsPath(file, cwd) {
|
|
942
|
-
const
|
|
943
|
-
return
|
|
970
|
+
const rel2 = path6.relative(cwd, path6.resolve(cwd, file));
|
|
971
|
+
return rel2.startsWith("..") ? file : rel2.replace(/\\/g, "/");
|
|
944
972
|
}
|
|
945
973
|
function quiet(fn) {
|
|
946
974
|
const saved = { log: console.log, warn: console.warn, error: console.error };
|
|
@@ -988,7 +1016,7 @@ var RestError = class extends Error {
|
|
|
988
1016
|
var NotAllowedError = class extends Error {
|
|
989
1017
|
};
|
|
990
1018
|
async function readCapped(res, limit, apiPath) {
|
|
991
|
-
const
|
|
1019
|
+
const tooLarge2 = () => new RestError(`kintone ${apiPath} の応答が大きすぎる(上限 ${limit} バイト)`, res.status, "LimitError", apiPath);
|
|
992
1020
|
const quietCancel = async (reader2) => {
|
|
993
1021
|
try {
|
|
994
1022
|
await reader2.cancel();
|
|
@@ -1001,7 +1029,7 @@ async function readCapped(res, limit, apiPath) {
|
|
|
1001
1029
|
const declared = Number(res.headers?.get("content-length") ?? Number.NaN);
|
|
1002
1030
|
if (Number.isFinite(declared) && declared > limit) {
|
|
1003
1031
|
await quietCancel(reader);
|
|
1004
|
-
throw
|
|
1032
|
+
throw tooLarge2();
|
|
1005
1033
|
}
|
|
1006
1034
|
const chunks = [];
|
|
1007
1035
|
let total = 0;
|
|
@@ -1012,7 +1040,7 @@ async function readCapped(res, limit, apiPath) {
|
|
|
1012
1040
|
total += value.length;
|
|
1013
1041
|
if (total > limit) {
|
|
1014
1042
|
await quietCancel(reader);
|
|
1015
|
-
throw
|
|
1043
|
+
throw tooLarge2();
|
|
1016
1044
|
}
|
|
1017
1045
|
chunks.push(value);
|
|
1018
1046
|
}
|
|
@@ -1651,6 +1679,7 @@ function diffSettings(before, after, names) {
|
|
|
1651
1679
|
|
|
1652
1680
|
// src/commands/users.ts
|
|
1653
1681
|
var PAGE = 100;
|
|
1682
|
+
var CODES_PER_CALL = 100;
|
|
1654
1683
|
var DEFAULT_MAX_CALLS = 300;
|
|
1655
1684
|
var UsersPullError = class extends Error {
|
|
1656
1685
|
};
|
|
@@ -1708,6 +1737,95 @@ function scanUserInfoCalls(text) {
|
|
|
1708
1737
|
}
|
|
1709
1738
|
return out;
|
|
1710
1739
|
}
|
|
1740
|
+
function referencedCodesOf(settings, strip) {
|
|
1741
|
+
const out = { users: [], orgs: [], groups: [], dynamic: [], fields: 0 };
|
|
1742
|
+
const rows = Array.isArray(settings.pluginInfos) ? settings.pluginInfos : [];
|
|
1743
|
+
const add = (kind, code) => {
|
|
1744
|
+
if (!out[kind].includes(code)) out[kind].push(code);
|
|
1745
|
+
};
|
|
1746
|
+
for (const r of rows) {
|
|
1747
|
+
if (!r || typeof r.formulaSet !== "string" || !r.formulaSet.trim()) continue;
|
|
1748
|
+
out.fields++;
|
|
1749
|
+
const fieldcode = typeof r.fieldcode === "string" ? r.fieldcode : "?";
|
|
1750
|
+
const calls = scanUserInfoCalls(strip(r.formulaSet));
|
|
1751
|
+
for (const d of calls.dynamic) out.dynamic.push(`${fieldcode}: ${d}`);
|
|
1752
|
+
for (const kind of ["users", "orgs", "groups"]) for (const c of calls[kind]) add(kind, c);
|
|
1753
|
+
}
|
|
1754
|
+
return out;
|
|
1755
|
+
}
|
|
1756
|
+
async function byCodes(client, api, key, codes, budget) {
|
|
1757
|
+
const out = [];
|
|
1758
|
+
for (let i = 0; i < codes.length; i += CODES_PER_CALL) {
|
|
1759
|
+
if (++budget.calls > budget.max) throw new UsersPullError(`User API の呼び出しが ${budget.max} 回を超えた(--max で上げる)`);
|
|
1760
|
+
const res = await client.getUser(api, { codes: codes.slice(i, i + CODES_PER_CALL), size: PAGE });
|
|
1761
|
+
out.push(...res[key] ?? []);
|
|
1762
|
+
}
|
|
1763
|
+
return out;
|
|
1764
|
+
}
|
|
1765
|
+
var uniq = (a) => [...new Set(a)];
|
|
1766
|
+
var missingOf = (want, have) => {
|
|
1767
|
+
const got = new Set([...have].map((x) => x.code));
|
|
1768
|
+
return want.filter((c) => !got.has(c));
|
|
1769
|
+
};
|
|
1770
|
+
async function pullReferencedUsers(client, ref, opt = {}) {
|
|
1771
|
+
const budget = { calls: 0, max: opt.maxCalls ?? DEFAULT_MAX_CALLS };
|
|
1772
|
+
const log = opt.log ?? (() => void 0);
|
|
1773
|
+
const one = async (api, key, code) => all(client, api, key, { code }, budget);
|
|
1774
|
+
const orgMembers = /* @__PURE__ */ new Map();
|
|
1775
|
+
for (const o of uniq(ref.orgs)) orgMembers.set(o, (await one("organization/users", "userTitles", o)).map((m) => m.user?.code).filter((c) => !!c));
|
|
1776
|
+
const groupMembers = /* @__PURE__ */ new Map();
|
|
1777
|
+
for (const g of uniq(ref.groups)) groupMembers.set(g, (await one("group/users", "users", g)).map((m) => m.code).filter((c) => !!c));
|
|
1778
|
+
const userCodes = uniq([...ref.users, ...[...orgMembers.values()].flat(), ...[...groupMembers.values()].flat()]);
|
|
1779
|
+
const rawUsers = userCodes.length ? await byCodes(client, "users", "users", userCodes, budget) : [];
|
|
1780
|
+
const missingUsers = missingOf(ref.users, rawUsers);
|
|
1781
|
+
if (missingUsers.length) throw new UsersPullError(`式が参照しているユーザーが無い: ${missingUsers.slice(0, 5).join(", ")}(コードを確かめる。全部取るなら --only-referenced を外す)`);
|
|
1782
|
+
const valid = rawUsers.filter((u2) => u2.valid !== false);
|
|
1783
|
+
log(`ユーザー ${valid.length}(参照 ${ref.users.length}、組織 ${ref.orgs.length} とグループ ${ref.groups.length} の所属を含む。ここまで ${budget.calls} 回)。所属をあと ${valid.length * 2} 回`);
|
|
1784
|
+
const userOrgs = /* @__PURE__ */ new Map();
|
|
1785
|
+
const userGroups = /* @__PURE__ */ new Map();
|
|
1786
|
+
for (const u2 of valid) {
|
|
1787
|
+
if ((budget.calls += 2) > budget.max) throw new UsersPullError(`User API の呼び出しが ${budget.max} 回を超えた(--max で上げる)`);
|
|
1788
|
+
const o = await client.getUser("user/organizations", { code: u2.code });
|
|
1789
|
+
userOrgs.set(u2.code, (o.organizationTitles ?? []).map((t) => t.organization).filter((x) => !!x && typeof x.code === "string"));
|
|
1790
|
+
const g = await client.getUser("user/groups", { code: u2.code });
|
|
1791
|
+
userGroups.set(u2.code, (g.groups ?? []).filter((x) => !!x && typeof x.code === "string"));
|
|
1792
|
+
}
|
|
1793
|
+
const orgDetail = /* @__PURE__ */ new Map();
|
|
1794
|
+
for (const list of userOrgs.values()) for (const o of list) orgDetail.set(o.code, o);
|
|
1795
|
+
const parentsOf = (orgs) => uniq([...orgs].map((o) => o.parentCode).filter((c) => !!c));
|
|
1796
|
+
let pending = uniq([...ref.orgs, ...parentsOf(orgDetail.values())]).filter((c) => !orgDetail.has(c));
|
|
1797
|
+
while (pending.length) {
|
|
1798
|
+
const got = await byCodes(client, "organizations", "organizations", pending, budget);
|
|
1799
|
+
for (const o of got) orgDetail.set(o.code, o);
|
|
1800
|
+
const missing = pending.filter((c) => !orgDetail.has(c));
|
|
1801
|
+
if (missing.length) throw new UsersPullError(`式が参照している組織が無い: ${missing.slice(0, 5).join(", ")}(コードを確かめる。全部取るなら --only-referenced を外す)`);
|
|
1802
|
+
pending = parentsOf(got).filter((c) => !orgDetail.has(c));
|
|
1803
|
+
}
|
|
1804
|
+
const groupDetail = /* @__PURE__ */ new Map();
|
|
1805
|
+
for (const list of userGroups.values()) for (const g of list) groupDetail.set(g.code, g);
|
|
1806
|
+
const gPending = uniq(ref.groups).filter((c) => !groupDetail.has(c));
|
|
1807
|
+
if (gPending.length) {
|
|
1808
|
+
for (const g of await byCodes(client, "groups", "groups", gPending, budget)) groupDetail.set(g.code, g);
|
|
1809
|
+
const missing = gPending.filter((c) => !groupDetail.has(c));
|
|
1810
|
+
if (missing.length) throw new UsersPullError(`式が参照しているグループが無い: ${missing.slice(0, 5).join(", ")}(コードを確かめる。全部取るなら --only-referenced を外す)`);
|
|
1811
|
+
}
|
|
1812
|
+
const byId = (a, b) => Number(a.id) - Number(b.id) || String(a.id).localeCompare(String(b.id));
|
|
1813
|
+
const orgRecords = [...orgDetail.values()].map((o) => ({
|
|
1814
|
+
id: String(o.id),
|
|
1815
|
+
code: o.code,
|
|
1816
|
+
name: o.name,
|
|
1817
|
+
parentCode: o.parentCode ?? null,
|
|
1818
|
+
users: valid.filter((u2) => (orgMembers.get(o.code) ?? []).includes(u2.code) || (userOrgs.get(u2.code) ?? []).some((x) => x.code === o.code)).map((u2) => u2.code)
|
|
1819
|
+
})).sort(byId);
|
|
1820
|
+
const groupRecords = [...groupDetail.values()].map((g) => ({
|
|
1821
|
+
id: String(g.id),
|
|
1822
|
+
code: g.code,
|
|
1823
|
+
name: g.name,
|
|
1824
|
+
users: valid.filter((u2) => (groupMembers.get(g.code) ?? []).includes(u2.code) || (userGroups.get(u2.code) ?? []).some((x) => x.code === g.code)).map((u2) => u2.code)
|
|
1825
|
+
})).sort(byId);
|
|
1826
|
+
const userRecords = valid.map((u2) => ({ ...u2, id: String(u2.id), customItemValues: u2.customItemValues ?? [] })).sort(byId);
|
|
1827
|
+
return { source: { users: userRecords, organizations: orgRecords, groups: groupRecords }, calls: budget.calls };
|
|
1828
|
+
}
|
|
1711
1829
|
function usersFileNameOf(baseUrl) {
|
|
1712
1830
|
try {
|
|
1713
1831
|
return `${new URL(baseUrl).hostname}.json`;
|
|
@@ -2171,7 +2289,7 @@ function calcSettings(input) {
|
|
|
2171
2289
|
const usersLabel = !table ? userInfoOn ? "なし" : "各種設定「ユーザー情報」OFF" : input.usersOrigin.kind === "file" ? input.usersOrigin.file : "既定のテストデータ(架空)";
|
|
2172
2290
|
const contextLine = `文脈: アプリ ${input.appId}、ログインユーザー ${loginUser ? loginUser.code : "指定なし(番兵)"}、mobile ${input.mobile ? "true" : "false"}、イベント ${evType}${event ? `(${event.action.value} / ${event.status.value} → ${event.nextStatus.value})` : ""}、ユーザー情報 ${usersLabel}、ルックアップ先 ${Object.keys(targets.lookupTargets).length}、関連レコード一覧の先 ${Object.keys(targets.relatedTargets).length}`;
|
|
2173
2291
|
const notice = `更新していません(模擬)。EV_INFO の文脈を与えた 3 段(項目 → テーブルの行 → 2 段目)の計算で、${evType === "allUpdate" ? "設定画面の「既存レコードを変更」の文脈" : evType === "batch" ? "一覧の「一括計算」の文脈" : "画面のイベントの処理の完全な再現ではありません"}。`;
|
|
2174
|
-
const
|
|
2292
|
+
const markOf2 = (fieldcode) => {
|
|
2175
2293
|
const scan = normalized.scans.get(fieldcode);
|
|
2176
2294
|
if (!scan) return [];
|
|
2177
2295
|
const used = Object.keys(scan.functions);
|
|
@@ -2194,7 +2312,7 @@ function calcSettings(input) {
|
|
|
2194
2312
|
const a = r.after[code]?.value;
|
|
2195
2313
|
const raw = record[code]?.value;
|
|
2196
2314
|
const b = isTable(raw) && isTable(rb) && raw.length !== rb.length ? raw : rb;
|
|
2197
|
-
lines.push({ fieldcode: code, before: b, after: a, changed: JSON.stringify(b) !== JSON.stringify(a), marks:
|
|
2315
|
+
lines.push({ fieldcode: code, before: b, after: a, changed: JSON.stringify(b) !== JSON.stringify(a), marks: markOf2(code) });
|
|
2198
2316
|
}
|
|
2199
2317
|
const byField = /* @__PURE__ */ new Map();
|
|
2200
2318
|
for (const e of r.errors) {
|
|
@@ -2249,15 +2367,868 @@ function calcSettings(input) {
|
|
|
2249
2367
|
return { contextLine, notice, records: results, summary, text: text.join("\n") };
|
|
2250
2368
|
}
|
|
2251
2369
|
|
|
2370
|
+
// src/commands/take.ts
|
|
2371
|
+
import { closeSync as closeSync2, existsSync as existsSync6, fstatSync as fstatSync2, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync9, readSync as readSync2, unlinkSync } from "node:fs";
|
|
2372
|
+
import path12 from "node:path";
|
|
2373
|
+
|
|
2374
|
+
// src/permission.ts
|
|
2375
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
2376
|
+
import path10 from "node:path";
|
|
2377
|
+
|
|
2378
|
+
// src/connections.ts
|
|
2379
|
+
import { createHash as createHash3, createHmac, randomBytes } from "node:crypto";
|
|
2380
|
+
import * as nodeFs from "node:fs";
|
|
2381
|
+
import { realpathSync as realpathSync3 } from "node:fs";
|
|
2382
|
+
import path8 from "node:path";
|
|
2383
|
+
var CONNECTION_MAX_BYTES = 256 * 1024;
|
|
2384
|
+
var REAL_FS = {
|
|
2385
|
+
openSync: (p, flags) => nodeFs.openSync(p, flags),
|
|
2386
|
+
fstatSync: (fd) => nodeFs.fstatSync(fd),
|
|
2387
|
+
readSync: (fd, buf, offset, length, position) => nodeFs.readSync(fd, buf, offset, length, position),
|
|
2388
|
+
closeSync: (fd) => nodeFs.closeSync(fd),
|
|
2389
|
+
statSync: (p) => nodeFs.statSync(p)
|
|
2390
|
+
};
|
|
2391
|
+
var fsOps = REAL_FS;
|
|
2392
|
+
var PROFILE_NAME_RE = /^[A-Za-z0-9_-]{1,40}$/;
|
|
2393
|
+
var ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2394
|
+
var FALLBACK_PROFILE = "dev";
|
|
2395
|
+
var ConnectionError = class extends Error {
|
|
2396
|
+
code;
|
|
2397
|
+
constructor(message, code) {
|
|
2398
|
+
super(message);
|
|
2399
|
+
this.code = code;
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
var SECRETS = /* @__PURE__ */ new WeakMap();
|
|
2403
|
+
var FINGERPRINT_KEY = randomBytes(32);
|
|
2404
|
+
var fingerprint = (parts) => createHmac("sha256", FINGERPRINT_KEY).update(JSON.stringify(parts)).digest("hex");
|
|
2405
|
+
var nonblank = (v) => typeof v === "string" && v.trim() ? v : void 0;
|
|
2406
|
+
var isPlainObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2407
|
+
function appKeyOf(key) {
|
|
2408
|
+
const m = /^(?:APP)?([1-9][0-9]*)$/i.exec(key);
|
|
2409
|
+
if (!m) return void 0;
|
|
2410
|
+
const n = Number(m[1]);
|
|
2411
|
+
return Number.isSafeInteger(n) ? n : void 0;
|
|
2412
|
+
}
|
|
2413
|
+
function readProfile(name, raw, env) {
|
|
2414
|
+
if (!PROFILE_NAME_RE.test(name)) return { reason: "profile の名前は英数字と - _ の 40 文字まで(フォルダーの名前になる)" };
|
|
2415
|
+
if (!isPlainObject(raw)) return { reason: "profile がオブジェクトでない" };
|
|
2416
|
+
if (typeof raw.baseUrl !== "string") return { reason: "baseUrl が無い、または文字列でない" };
|
|
2417
|
+
for (const k of ["username", "password", "passwordEnv"]) {
|
|
2418
|
+
if (raw[k] !== void 0 && typeof raw[k] !== "string") return { reason: `${k} が文字列でない` };
|
|
2419
|
+
}
|
|
2420
|
+
let baseUrl;
|
|
2421
|
+
try {
|
|
2422
|
+
baseUrl = normalizeKintoneBaseUrl(raw.baseUrl);
|
|
2423
|
+
} catch {
|
|
2424
|
+
return { reason: "baseUrl が kintone の URL でない(https://<サブドメイン>.cybozu.com / .kintone.com / .cybozu.cn だけ。パス・ポート・ユーザー情報なし)" };
|
|
2425
|
+
}
|
|
2426
|
+
let guestSpaceId = null;
|
|
2427
|
+
if (raw.guestSpaceId !== void 0 && raw.guestSpaceId !== null) {
|
|
2428
|
+
if (typeof raw.guestSpaceId !== "number" || !Number.isSafeInteger(raw.guestSpaceId) || raw.guestSpaceId <= 0) return { reason: "guestSpaceId は正の整数" };
|
|
2429
|
+
guestSpaceId = raw.guestSpaceId;
|
|
2430
|
+
}
|
|
2431
|
+
const mode = raw.auth ?? "auto";
|
|
2432
|
+
if (mode !== "token" && mode !== "userpass" && mode !== "auto") return { reason: "auth は token / userpass / auto のどれか" };
|
|
2433
|
+
if (raw.passwordEnv !== void 0 && (typeof raw.passwordEnv !== "string" || !ENV_NAME_RE.test(raw.passwordEnv))) return { reason: "passwordEnv は環境変数の名前(英字か _ で始まる英数字と _)" };
|
|
2434
|
+
if (raw.userApiToken !== void 0) {
|
|
2435
|
+
if (typeof raw.userApiToken !== "string" || !raw.userApiToken.trim()) return { reason: "userApiToken が文字列でない、または空" };
|
|
2436
|
+
if (raw.userApiToken.startsWith("env:") && !ENV_NAME_RE.test(raw.userApiToken.slice(4))) return { reason: "userApiToken の env: の後ろが環境変数の名前でない" };
|
|
2437
|
+
}
|
|
2438
|
+
const userApiToken = typeof raw.userApiToken === "string" ? raw.userApiToken : void 0;
|
|
2439
|
+
const username = nonblank(raw.username);
|
|
2440
|
+
const password = (typeof raw.passwordEnv === "string" ? nonblank(env[raw.passwordEnv]) : void 0) ?? nonblank(raw.password);
|
|
2441
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
2442
|
+
if (raw.tokenMap !== void 0) {
|
|
2443
|
+
if (!isPlainObject(raw.tokenMap)) return { reason: "tokenMap がオブジェクトでない" };
|
|
2444
|
+
for (const key of Object.keys(raw.tokenMap)) {
|
|
2445
|
+
const appId = appKeyOf(key);
|
|
2446
|
+
if (appId === void 0) return { reason: `tokenMap のキー「${key.slice(0, 40)}」はアプリの番号でない(APP101 か 101。先頭に 0 を付けない)` };
|
|
2447
|
+
if (tokens.has(appId)) return { reason: `tokenMap にアプリ ${appId} が 2 つある(APP${appId} と ${appId} など)` };
|
|
2448
|
+
const value = raw.tokenMap[key];
|
|
2449
|
+
if (typeof value !== "string" || !value.trim()) return { reason: `tokenMap の APP${appId} の値が文字列でない、または空` };
|
|
2450
|
+
if (value.startsWith("env:") && !ENV_NAME_RE.test(value.slice(4))) return { reason: `tokenMap の APP${appId} の env: の後ろが環境変数の名前でない` };
|
|
2451
|
+
tokens.set(appId, value);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
const auth = mode === "auto" ? username && password ? "userpass" : "token" : mode;
|
|
2455
|
+
const tokenApps = [...tokens.keys()].sort((a, b) => a - b);
|
|
2456
|
+
const tokenReady = {};
|
|
2457
|
+
for (const [appId, value] of tokens) tokenReady[appId] = value.startsWith("env:") ? !!nonblank(env[value.slice(4)]) : true;
|
|
2458
|
+
const host = new URL(baseUrl).hostname;
|
|
2459
|
+
const userApiTokenReady = userApiToken === void 0 ? false : userApiToken.startsWith("env:") ? !!nonblank(env[userApiToken.slice(4)]) : true;
|
|
2460
|
+
return {
|
|
2461
|
+
def: { profile: name, baseUrl, host, guestSpaceId, auth, tokenApps, tokenReady, userpassReady: !!(username && password), userApiTokenReady },
|
|
2462
|
+
secrets: {
|
|
2463
|
+
...username ? { username } : {},
|
|
2464
|
+
...password ? { password } : {},
|
|
2465
|
+
tokens,
|
|
2466
|
+
...userApiToken !== void 0 ? { userApiToken } : {}
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
function realOf(p) {
|
|
2471
|
+
return realpathSync3.native(p);
|
|
2472
|
+
}
|
|
2473
|
+
var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs;
|
|
2474
|
+
var changed = (name) => new ConnectionError(`接続のファイル(${name})が読み込みの途中で変わった`, "unreadable");
|
|
2475
|
+
function readStable(real, name) {
|
|
2476
|
+
let fd;
|
|
2477
|
+
try {
|
|
2478
|
+
fd = fsOps.openSync(real, "r");
|
|
2479
|
+
const before = fsOps.fstatSync(fd);
|
|
2480
|
+
if (!before.isFile()) throw new ConnectionError(`接続のファイル(${name})が普通のファイルでない(フォルダーなど)`, "unreadable");
|
|
2481
|
+
if (before.size > CONNECTION_MAX_BYTES) throw new ConnectionError(`接続のファイル(${name})が大きすぎる(上限 ${CONNECTION_MAX_BYTES} バイト)`, "unreadable");
|
|
2482
|
+
const buf = Buffer.alloc(CONNECTION_MAX_BYTES + 1);
|
|
2483
|
+
let off = 0;
|
|
2484
|
+
for (; ; ) {
|
|
2485
|
+
const n = fsOps.readSync(fd, buf, off, buf.length - off, off);
|
|
2486
|
+
if (n === 0) break;
|
|
2487
|
+
off += n;
|
|
2488
|
+
if (off > CONNECTION_MAX_BYTES) throw new ConnectionError(`接続のファイル(${name})が大きすぎる(上限 ${CONNECTION_MAX_BYTES} バイト)`, "unreadable");
|
|
2489
|
+
}
|
|
2490
|
+
const after = fsOps.fstatSync(fd);
|
|
2491
|
+
let onPath;
|
|
2492
|
+
try {
|
|
2493
|
+
onPath = fsOps.statSync(real);
|
|
2494
|
+
} catch {
|
|
2495
|
+
throw changed(name);
|
|
2496
|
+
}
|
|
2497
|
+
if (!sameFile(before, after) || !sameFile(before, onPath) || off !== before.size) throw changed(name);
|
|
2498
|
+
return { bytes: Buffer.from(buf.subarray(0, off)), stat: before };
|
|
2499
|
+
} catch (e) {
|
|
2500
|
+
if (e instanceof ConnectionError) throw e;
|
|
2501
|
+
throw new ConnectionError(`接続のファイル(${name})を読めない`, "unreadable");
|
|
2502
|
+
} finally {
|
|
2503
|
+
if (fd !== void 0) fsOps.closeSync(fd);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
function readRegularFile(real, name) {
|
|
2507
|
+
const first = readStable(real, name);
|
|
2508
|
+
if (first.stat.ino === 0) {
|
|
2509
|
+
const second = readStable(real, name);
|
|
2510
|
+
if (!first.bytes.equals(second.bytes)) throw changed(name);
|
|
2511
|
+
}
|
|
2512
|
+
return { bytes: first.bytes, mode: first.stat.mode };
|
|
2513
|
+
}
|
|
2514
|
+
function hasInlineSecrets(rawProfiles) {
|
|
2515
|
+
for (const key of Object.keys(rawProfiles)) {
|
|
2516
|
+
const p = rawProfiles[key];
|
|
2517
|
+
if (!isPlainObject(p)) continue;
|
|
2518
|
+
if (nonblank(p.password)) return true;
|
|
2519
|
+
if (typeof p.userApiToken === "string" && p.userApiToken.trim() !== "" && !p.userApiToken.startsWith("env:")) return true;
|
|
2520
|
+
if (isPlainObject(p.tokenMap) && Object.values(p.tokenMap).some((v) => typeof v === "string" && v.trim() !== "" && !v.startsWith("env:"))) return true;
|
|
2521
|
+
}
|
|
2522
|
+
return false;
|
|
2523
|
+
}
|
|
2524
|
+
function loadConnections(file, opt) {
|
|
2525
|
+
if (!file || !file.trim()) throw new ConnectionError("kintone の接続のファイルが設定されていない(ksql.config.json と同じ形のファイル)", "not-configured");
|
|
2526
|
+
const name = path8.basename(file);
|
|
2527
|
+
if (!path8.isAbsolute(file)) throw new ConnectionError(`接続のファイル(${name})は絶対パスで指定する`, "unreadable");
|
|
2528
|
+
let real;
|
|
2529
|
+
try {
|
|
2530
|
+
real = realOf(file);
|
|
2531
|
+
} catch {
|
|
2532
|
+
throw new ConnectionError(`接続のファイル(${name})が無い、または読めない`, "unreadable");
|
|
2533
|
+
}
|
|
2534
|
+
const given = path8.resolve(file);
|
|
2535
|
+
for (const root of opt.workspaceRoots) {
|
|
2536
|
+
let rootReal;
|
|
2537
|
+
try {
|
|
2538
|
+
rootReal = realOf(root);
|
|
2539
|
+
} catch {
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
if (isInside(real, rootReal) || isInside(given, rootReal) || isInside(given, path8.resolve(root))) {
|
|
2543
|
+
throw new ConnectionError(`接続のファイル(${name})が作業フォルダーの中にある。作業フォルダーの外に置く`, "inside-workspace");
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
const { bytes, mode } = readRegularFile(real, name);
|
|
2547
|
+
let again;
|
|
2548
|
+
try {
|
|
2549
|
+
again = realOf(file);
|
|
2550
|
+
} catch {
|
|
2551
|
+
again = void 0;
|
|
2552
|
+
}
|
|
2553
|
+
if (again !== real) throw new ConnectionError(`接続のファイル(${name})が読み込みの途中で変わった`, "unreadable");
|
|
2554
|
+
let data;
|
|
2555
|
+
try {
|
|
2556
|
+
data = JSON.parse(bytes.toString("utf8"));
|
|
2557
|
+
} catch {
|
|
2558
|
+
throw new ConnectionError(`接続のファイル(${name})を JSON として読めない`, "unreadable");
|
|
2559
|
+
}
|
|
2560
|
+
if (!isPlainObject(data)) throw new ConnectionError(`接続のファイル(${name})の最上位がオブジェクトでない`, "unreadable");
|
|
2561
|
+
const rawProfiles = data.profiles ?? {};
|
|
2562
|
+
if (!isPlainObject(rawProfiles)) throw new ConnectionError(`接続のファイル(${name})の profiles がオブジェクトでない`, "unreadable");
|
|
2563
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
2564
|
+
const byProfile = /* @__PURE__ */ new Map();
|
|
2565
|
+
const invalid = [];
|
|
2566
|
+
for (const key of Object.keys(rawProfiles)) {
|
|
2567
|
+
const r = readProfile(key, rawProfiles[key], opt.env);
|
|
2568
|
+
if ("reason" in r) invalid.push({ name: key.slice(0, 40), reason: r.reason });
|
|
2569
|
+
else {
|
|
2570
|
+
profiles.set(key, r.def);
|
|
2571
|
+
byProfile.set(key, r.secrets);
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
const lower = /* @__PURE__ */ new Map();
|
|
2575
|
+
for (const key of profiles.keys()) lower.set(key.toLowerCase(), [...lower.get(key.toLowerCase()) ?? [], key]);
|
|
2576
|
+
for (const keys of lower.values()) {
|
|
2577
|
+
if (keys.length < 2) continue;
|
|
2578
|
+
for (const key of keys) {
|
|
2579
|
+
profiles.delete(key);
|
|
2580
|
+
byProfile.delete(key);
|
|
2581
|
+
invalid.push({ name: key, reason: `大文字小文字だけが違う profile がある(${keys.join("、")})` });
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
if (data.defaultProfile !== void 0 && typeof data.defaultProfile !== "string") throw new ConnectionError(`接続のファイル(${name})の defaultProfile が文字列でない`, "unreadable");
|
|
2585
|
+
const defaultProfile = data.defaultProfile;
|
|
2586
|
+
const warnings = [];
|
|
2587
|
+
if (process.platform !== "win32" && hasInlineSecrets(rawProfiles) && (mode & 63) !== 0) warnings.push(`接続のファイル(${name})に秘密の値が直接書いてあり、ほかの利用者も読める。chmod 600 を勧める`);
|
|
2588
|
+
const set = { fileName: name, digest: createHash3("sha256").update(bytes).digest("hex"), ...defaultProfile !== void 0 ? { defaultProfile } : {}, profiles, invalid, warnings };
|
|
2589
|
+
SECRETS.set(set, { env: opt.env, byProfile });
|
|
2590
|
+
return set;
|
|
2591
|
+
}
|
|
2592
|
+
function pickProfile(set, name) {
|
|
2593
|
+
const bad = (n) => set.invalid.find((p) => p.name === n)?.reason;
|
|
2594
|
+
if (name !== void 0) {
|
|
2595
|
+
const def2 = set.profiles.get(name);
|
|
2596
|
+
if (def2) return def2;
|
|
2597
|
+
const reason2 = bad(name);
|
|
2598
|
+
if (reason2) throw new ConnectionError(`profile「${name.slice(0, 40)}」は使えない: ${reason2}`, "profile-invalid");
|
|
2599
|
+
throw new ConnectionError(`profile「${name.slice(0, 40)}」は接続のファイル(${set.fileName})に無い`, "unknown-profile");
|
|
2600
|
+
}
|
|
2601
|
+
if (set.defaultProfile !== void 0) {
|
|
2602
|
+
const def2 = set.profiles.get(set.defaultProfile);
|
|
2603
|
+
if (def2) return def2;
|
|
2604
|
+
const reason2 = bad(set.defaultProfile);
|
|
2605
|
+
throw new ConnectionError(`defaultProfile の「${set.defaultProfile.slice(0, 40)}」が${reason2 ? `使えない: ${reason2}` : "接続のファイルの profiles に無い"}`, "profile-invalid");
|
|
2606
|
+
}
|
|
2607
|
+
const def = set.profiles.get(FALLBACK_PROFILE);
|
|
2608
|
+
if (def) return def;
|
|
2609
|
+
const reason = bad(FALLBACK_PROFILE);
|
|
2610
|
+
if (reason) throw new ConnectionError(`profile「${FALLBACK_PROFILE}」は使えない: ${reason}`, "profile-invalid");
|
|
2611
|
+
throw new ConnectionError(`profile を指定するか、接続のファイル(${set.fileName})に defaultProfile を書く(無ければ "${FALLBACK_PROFILE}" を使うが、それも無い)`, "no-default-profile");
|
|
2612
|
+
}
|
|
2613
|
+
function snapshotOf(set, def) {
|
|
2614
|
+
return { ...identityOf(def), auth: def.auth, fileName: set.fileName };
|
|
2615
|
+
}
|
|
2616
|
+
function semanticDigest(set, def, appId) {
|
|
2617
|
+
const held = SECRETS.get(set);
|
|
2618
|
+
const secrets = held?.byProfile.get(def.profile);
|
|
2619
|
+
let credential = "none";
|
|
2620
|
+
if (def.auth === "userpass") credential = fingerprint(["userpass", secrets?.username ?? "", secrets?.password ?? ""]);
|
|
2621
|
+
else if (appId !== void 0) {
|
|
2622
|
+
const v = secrets?.tokens.get(appId);
|
|
2623
|
+
const value = v === void 0 ? "" : v.startsWith("env:") ? nonblank(held?.env[v.slice(4)]) ?? "" : v;
|
|
2624
|
+
credential = fingerprint(["token", v?.startsWith("env:") ? v : "inline", value]);
|
|
2625
|
+
}
|
|
2626
|
+
return createHash3("sha256").update(JSON.stringify([def.profile, def.baseUrl, def.guestSpaceId, def.auth, credential, appId ?? null])).digest("hex");
|
|
2627
|
+
}
|
|
2628
|
+
function assertSameConnection(before, reload, appId) {
|
|
2629
|
+
const want = semanticDigest(before.set, before.def, appId);
|
|
2630
|
+
let now;
|
|
2631
|
+
try {
|
|
2632
|
+
const set = reload();
|
|
2633
|
+
now = semanticDigest(set, pickProfile(set, before.def.profile), appId);
|
|
2634
|
+
} catch (e) {
|
|
2635
|
+
if (e instanceof ConnectionError && (e.code === "unknown-profile" || e.code === "profile-invalid")) throw new ConnectionError(`profile「${before.def.profile}」の接続が途中で変わった(使えなくなった)。もう一度やり直す`, "connection-changed");
|
|
2636
|
+
throw e;
|
|
2637
|
+
}
|
|
2638
|
+
if (now !== want) throw new ConnectionError(`profile「${before.def.profile}」の接続が途中で変わった。もう一度やり直す`, "connection-changed");
|
|
2639
|
+
}
|
|
2640
|
+
function authFor(set, profile, appId) {
|
|
2641
|
+
const held = SECRETS.get(set);
|
|
2642
|
+
const secrets = held?.byProfile.get(profile.profile);
|
|
2643
|
+
if (!held || !secrets) throw new ConnectionError(`profile「${profile.profile}」の接続が読み込まれていない`, "profile-invalid");
|
|
2644
|
+
if (profile.auth === "userpass") {
|
|
2645
|
+
if (!secrets.username || !secrets.password) throw new ConnectionError(`profile「${profile.profile}」のログイン名とパスワードがそろわない(username と password か passwordEnv)`, "no-userpass");
|
|
2646
|
+
return { baseUrl: profile.baseUrl, username: secrets.username, password: secrets.password };
|
|
2647
|
+
}
|
|
2648
|
+
const value = secrets.tokens.get(appId);
|
|
2649
|
+
if (value === void 0) throw new ConnectionError(`profile「${profile.profile}」の tokenMap に APP${appId} が無い(そのアプリの API トークンを足す)`, "no-token");
|
|
2650
|
+
if (value.startsWith("env:")) {
|
|
2651
|
+
const token = nonblank(held.env[value.slice(4)]);
|
|
2652
|
+
if (!token) throw new ConnectionError(`profile「${profile.profile}」の APP${appId} のトークンの環境変数が無い(設定した後、print-craft を再接続する。Desktop は起動し直す)`, "env-missing");
|
|
2653
|
+
return { baseUrl: profile.baseUrl, token };
|
|
2654
|
+
}
|
|
2655
|
+
return { baseUrl: profile.baseUrl, token: value };
|
|
2656
|
+
}
|
|
2657
|
+
function userApiAuthFor(set, profile) {
|
|
2658
|
+
const held = SECRETS.get(set);
|
|
2659
|
+
const secrets = held?.byProfile.get(profile.profile);
|
|
2660
|
+
if (!held || !secrets) throw new ConnectionError(`profile「${profile.profile}」の接続が読み込まれていない`, "profile-invalid");
|
|
2661
|
+
const raw = secrets.userApiToken;
|
|
2662
|
+
if (raw !== void 0) {
|
|
2663
|
+
const bearer = raw.startsWith("env:") ? nonblank(held.env[raw.slice(4)]) : raw;
|
|
2664
|
+
if (!bearer) throw new ConnectionError(`profile「${profile.profile}」の userApiToken の環境変数が無い(設定した後、起動し直す)`, "env-missing");
|
|
2665
|
+
return { baseUrl: profile.baseUrl, bearer };
|
|
2666
|
+
}
|
|
2667
|
+
if (secrets.username && secrets.password) return { baseUrl: profile.baseUrl, username: secrets.username, password: secrets.password };
|
|
2668
|
+
throw new ConnectionError(`profile「${profile.profile}」に User API(users --pull)の認証が無い。userApiToken(cybozu.com 共通管理の API トークン。env:<環境変数> か値)か、username と password(passwordEnv)を書く。アプリの API トークン(tokenMap)では呼べない`, "no-user-api-auth");
|
|
2669
|
+
}
|
|
2670
|
+
function identityOf(def) {
|
|
2671
|
+
return { profile: def.profile, baseUrl: def.baseUrl, host: def.host, guestSpaceId: def.guestSpaceId };
|
|
2672
|
+
}
|
|
2673
|
+
function sameIdentity(a, b) {
|
|
2674
|
+
return a.profile === b.profile && a.baseUrl === b.baseUrl && a.guestSpaceId === b.guestSpaceId;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/permission-error.ts
|
|
2678
|
+
var PermissionError = class extends Error {
|
|
2679
|
+
};
|
|
2680
|
+
|
|
2681
|
+
// src/workspace.ts
|
|
2682
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync, readdirSync, readFileSync as readFileSync7, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2683
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2684
|
+
import path9 from "node:path";
|
|
2685
|
+
var KINTONE_ROOT = "kintone";
|
|
2686
|
+
var INBOX = "inbox";
|
|
2687
|
+
var LEGACY_WORKSPACE_FILE = "environments.json";
|
|
2688
|
+
var APP_MARKER = ".formula-app.json";
|
|
2689
|
+
var SNAPSHOT_PREFIX = "rex0220-formula";
|
|
2690
|
+
var SNAPSHOT_RE = /^rex0220-formula-app(\d+)-(\d{8})-(\d{6})\.json$/;
|
|
2691
|
+
var EDIT_RE = /^rex0220-formula-app(\d+)-(\d{8})-(\d{6})-edit\.json$/;
|
|
2692
|
+
var MARKER_MAX_BYTES = 4 * 1024;
|
|
2693
|
+
var WorkspaceError = class extends Error {
|
|
2694
|
+
};
|
|
2695
|
+
function hasLegacyConfig(cwd) {
|
|
2696
|
+
try {
|
|
2697
|
+
lstatSync2(path9.join(cwd, LEGACY_WORKSPACE_FILE));
|
|
2698
|
+
return true;
|
|
2699
|
+
} catch {
|
|
2700
|
+
return false;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
function legacyBlockedError() {
|
|
2704
|
+
return new ConnectionError(
|
|
2705
|
+
`${LEGACY_WORKSPACE_FILE} は tools 2.0.0 から使わない。kintone の接続のファイル(ksql.config.json と同じ形)を書いて設定し(CLI は FORMULA_KINTONE_CONFIG)、${LEGACY_WORKSPACE_FILE} を消す(バージョンアップ手順)`,
|
|
2706
|
+
"legacy-config-present"
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
function modeOf(cwd, opt) {
|
|
2710
|
+
if (opt.configFile !== void 0) {
|
|
2711
|
+
if (!opt.configFile.trim()) throw new ConnectionError("kintone の接続のファイル(FORMULA_KINTONE_CONFIG)が空。ファイルのパスを書くか、設定を消す", "unreadable");
|
|
2712
|
+
const load = () => loadConnections(opt.configFile, { workspaceRoots: opt.workspaceRoots, env: opt.env });
|
|
2713
|
+
return { kind: "profiles", connections: { set: load(), reload: load } };
|
|
2714
|
+
}
|
|
2715
|
+
if (hasLegacyConfig(cwd)) return { kind: "legacy-blocked" };
|
|
2716
|
+
return opt.surface === "cli" ? { kind: "single" } : { kind: "not-configured" };
|
|
2717
|
+
}
|
|
2718
|
+
function requireProfiles(mode, what) {
|
|
2719
|
+
if (mode.kind === "profiles") return mode.connections;
|
|
2720
|
+
if (mode.kind === "legacy-blocked") throw legacyBlockedError();
|
|
2721
|
+
if (mode.kind === "not-configured") throw new ConnectionError("kintone の接続のファイルが設定されていない(ksql.config.json と同じ形のファイルを設定に選ぶ)", "not-configured");
|
|
2722
|
+
throw new ConnectionError(`${what} は kintone の接続のファイル(FORMULA_KINTONE_CONFIG)があるときだけ使える`, "not-configured");
|
|
2723
|
+
}
|
|
2724
|
+
function folderNameOf(appId, appName) {
|
|
2725
|
+
const name = safeFileName(appName, "");
|
|
2726
|
+
return name ? `${appId}-${name}` : String(appId);
|
|
2727
|
+
}
|
|
2728
|
+
function profileDirOf(cwd, profile) {
|
|
2729
|
+
if (!PROFILE_NAME_RE.test(profile)) throw new WorkspaceError(`profile の名前が使えない: ${profile.slice(0, 40)}`);
|
|
2730
|
+
return path9.join(cwd, KINTONE_ROOT, profile);
|
|
2731
|
+
}
|
|
2732
|
+
function assertInsideWorkspace(cwd, p) {
|
|
2733
|
+
const rel2 = path9.relative(cwd, p);
|
|
2734
|
+
const root = realResolve(".", cwd);
|
|
2735
|
+
if (!isInside(realResolve(rel2, cwd), root)) throw new PathError(`${rel2} の実体が作業フォルダーの外を指している(symlink など)`);
|
|
2736
|
+
}
|
|
2737
|
+
function findAppDir(cwd, profile, appId) {
|
|
2738
|
+
const dir = profileDirOf(cwd, profile);
|
|
2739
|
+
if (!existsSync4(dir)) return null;
|
|
2740
|
+
assertInsideWorkspace(cwd, path9.join(cwd, KINTONE_ROOT));
|
|
2741
|
+
assertInsideWorkspace(cwd, dir);
|
|
2742
|
+
const hits = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && (d.name === String(appId) || d.name.startsWith(`${appId}-`))).map((d) => d.name);
|
|
2743
|
+
if (hits.length > 1) throw new WorkspaceError(`アプリ ${appId} のフォルダーが 2 つある: ${hits.join(", ")}(kintone/${profile}/ の下を 1 つにする)`);
|
|
2744
|
+
return hits.length ? path9.join(dir, hits[0]) : null;
|
|
2745
|
+
}
|
|
2746
|
+
function appDirFor(cwd, profile, appId, appName) {
|
|
2747
|
+
return findAppDir(cwd, profile, appId) ?? path9.join(profileDirOf(cwd, profile), folderNameOf(appId, appName));
|
|
2748
|
+
}
|
|
2749
|
+
function appFolderOfFile(cwd, file) {
|
|
2750
|
+
const rel2 = path9.relative(path9.join(cwd, KINTONE_ROOT), path9.resolve(cwd, file)).replace(/\\/g, "/");
|
|
2751
|
+
if (!rel2 || rel2.startsWith("..") || path9.isAbsolute(rel2)) return null;
|
|
2752
|
+
const [profile, folder, ...rest] = rel2.split("/");
|
|
2753
|
+
if (!profile || !folder || rest.length === 0 || !PROFILE_NAME_RE.test(profile)) return null;
|
|
2754
|
+
const m = folder.match(/^([1-9]\d*)(?:-|$)/);
|
|
2755
|
+
if (!m) return null;
|
|
2756
|
+
return { dir: path9.join(cwd, KINTONE_ROOT, profile, folder), profile, appId: Number(m[1]) };
|
|
2757
|
+
}
|
|
2758
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
2759
|
+
function snapshotNameOf(appId, now = /* @__PURE__ */ new Date()) {
|
|
2760
|
+
return `${SNAPSHOT_PREFIX}-app${appId}-${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}.json`;
|
|
2761
|
+
}
|
|
2762
|
+
function editNameOf(snapshotFile) {
|
|
2763
|
+
return snapshotFile.replace(/\.json$/, "-edit.json");
|
|
2764
|
+
}
|
|
2765
|
+
function markOf(def, appId) {
|
|
2766
|
+
return { schemaVersion: 1, profile: def.profile, origin: def.baseUrl, guestSpaceId: def.guestSpaceId, appId };
|
|
2767
|
+
}
|
|
2768
|
+
function readAppMark(dir) {
|
|
2769
|
+
const file = path9.join(dir, APP_MARKER);
|
|
2770
|
+
let st;
|
|
2771
|
+
try {
|
|
2772
|
+
st = lstatSync2(file);
|
|
2773
|
+
} catch {
|
|
2774
|
+
return { state: "missing" };
|
|
2775
|
+
}
|
|
2776
|
+
if (!st.isFile() || st.size > MARKER_MAX_BYTES) return { state: "invalid" };
|
|
2777
|
+
try {
|
|
2778
|
+
const v = JSON.parse(readFileSync7(file, "utf8"));
|
|
2779
|
+
const ok = v && v.schemaVersion === 1 && typeof v.profile === "string" && typeof v.origin === "string" && (v.guestSpaceId === null || typeof v.guestSpaceId === "number" && Number.isSafeInteger(v.guestSpaceId)) && typeof v.appId === "number" && Number.isSafeInteger(v.appId);
|
|
2780
|
+
return ok ? { state: "ok", mark: v } : { state: "invalid" };
|
|
2781
|
+
} catch {
|
|
2782
|
+
return { state: "invalid" };
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
function assertAppMark(dir, def, appId) {
|
|
2786
|
+
const shownDir = `kintone/${def.profile}/${path9.basename(dir)}`;
|
|
2787
|
+
const r = readAppMark(dir);
|
|
2788
|
+
if (r.state === "missing") {
|
|
2789
|
+
throw new ConnectionError(`${shownDir} に印(${APP_MARKER})が無い。tools が作ったフォルダーでないので使わない(名前を変えて退け、fields か pull で作り直す。中のファイルは後で移す)`, "app-marker-missing");
|
|
2790
|
+
}
|
|
2791
|
+
if (r.state === "invalid") throw new ConnectionError(`${shownDir} の印(${APP_MARKER})が壊れている。名前を変えて退け、fields か pull で作り直す`, "app-marker-invalid");
|
|
2792
|
+
const m = r.mark;
|
|
2793
|
+
if (m.appId !== appId || !sameIdentity({ profile: m.profile, baseUrl: m.origin, host: "", guestSpaceId: m.guestSpaceId }, { profile: def.profile, baseUrl: def.baseUrl, host: "", guestSpaceId: def.guestSpaceId })) {
|
|
2794
|
+
throw new ConnectionError(`${shownDir} は別の接続(profile・接続先・ゲストスペース・アプリのどれかが違う)のフォルダー。profile の接続を変えたなら、名前を変えて退け、fields か pull で作り直す`, "app-identity-conflict");
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
function ensureAppFolder(dir, def, appId) {
|
|
2798
|
+
if (existsSync4(dir)) {
|
|
2799
|
+
assertAppMark(dir, def, appId);
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
const parent = path9.dirname(dir);
|
|
2803
|
+
mkdirSync(parent, { recursive: true });
|
|
2804
|
+
const tmp = path9.join(parent, `.${path9.basename(dir)}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
2805
|
+
mkdirSync(tmp);
|
|
2806
|
+
try {
|
|
2807
|
+
writeFileSync(path9.join(tmp, APP_MARKER), JSON.stringify(markOf(def, appId), null, 2) + "\n", { encoding: "utf8", flag: "wx" });
|
|
2808
|
+
if (existsSync4(dir)) {
|
|
2809
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
2810
|
+
} else {
|
|
2811
|
+
try {
|
|
2812
|
+
renameSync(tmp, dir);
|
|
2813
|
+
} catch (e) {
|
|
2814
|
+
const code = e.code;
|
|
2815
|
+
if (code !== "EEXIST" && code !== "ENOTEMPTY" && code !== "EPERM") throw e;
|
|
2816
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
} catch (e) {
|
|
2820
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
2821
|
+
throw e;
|
|
2822
|
+
}
|
|
2823
|
+
assertAppMark(dir, def, appId);
|
|
2824
|
+
}
|
|
2825
|
+
function assertUsableInKintone(cwd, file, mode) {
|
|
2826
|
+
const abs = path9.resolve(cwd, file);
|
|
2827
|
+
if (!isInside(abs, path9.join(cwd, KINTONE_ROOT))) return;
|
|
2828
|
+
if (mode.kind !== "profiles") {
|
|
2829
|
+
if (mode.kind === "legacy-blocked") throw legacyBlockedError();
|
|
2830
|
+
throw new PermissionError("kintone/ の下は、kintone の接続のファイルがあるときだけ使う(今の形では settings/ などを使う)");
|
|
2831
|
+
}
|
|
2832
|
+
const folder = appFolderOfFile(cwd, abs);
|
|
2833
|
+
if (!folder) throw new PermissionError(`アプリのフォルダー(kintone/<profile>/<番号>-…/)の中でない: ${path9.relative(cwd, abs)}`);
|
|
2834
|
+
const def = mode.connections.set.profiles.get(folder.profile);
|
|
2835
|
+
if (!def) throw new PermissionError(`profile「${folder.profile}」は接続のファイル(${mode.connections.set.fileName})に無い(使われていないフォルダー)`);
|
|
2836
|
+
assertAppMark(folder.dir, def, folder.appId);
|
|
2837
|
+
}
|
|
2838
|
+
var stampOf = (name) => {
|
|
2839
|
+
const m = name.match(/-(\d{8})-(\d{6})(?:-edit)?\.json$/);
|
|
2840
|
+
return m ? `${m[1]}${m[2]}` : "";
|
|
2841
|
+
};
|
|
2842
|
+
var newestFirst = (a, b) => stampOf(b).localeCompare(stampOf(a)) || b.localeCompare(a);
|
|
2843
|
+
function listAppFolder(dir, cwd) {
|
|
2844
|
+
if (cwd !== void 0) {
|
|
2845
|
+
for (const d of [dir, path9.join(dir, "records")]) if (existsSync4(d)) assertInsideWorkspace(cwd, d);
|
|
2846
|
+
}
|
|
2847
|
+
const files = existsSync4(dir) ? readdirSync(dir, { withFileTypes: true }) : [];
|
|
2848
|
+
const names = files.filter((d) => d.isFile() && d.name.endsWith(".json") && !d.name.startsWith(".")).map((d) => d.name);
|
|
2849
|
+
const sub = (name, filter) => {
|
|
2850
|
+
const p = path9.join(dir, name);
|
|
2851
|
+
return existsSync4(p) ? readdirSync(p).filter((n) => !n.startsWith(".") && filter(n)).sort() : [];
|
|
2852
|
+
};
|
|
2853
|
+
return {
|
|
2854
|
+
dir,
|
|
2855
|
+
hasFields: names.includes("fields.json"),
|
|
2856
|
+
snapshots: names.filter((n) => SNAPSHOT_RE.test(n)).sort(newestFirst),
|
|
2857
|
+
edits: names.filter((n) => EDIT_RE.test(n)).sort(newestFirst),
|
|
2858
|
+
others: names.filter((n) => n !== "fields.json" && !SNAPSHOT_RE.test(n) && !EDIT_RE.test(n)).sort(),
|
|
2859
|
+
records: sub("records", (n) => n.endsWith(".json"))
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
function appFoldersOf(cwd, def) {
|
|
2863
|
+
const dir = profileDirOf(cwd, def.profile);
|
|
2864
|
+
if (!existsSync4(dir)) return [];
|
|
2865
|
+
assertInsideWorkspace(cwd, path9.join(cwd, KINTONE_ROOT));
|
|
2866
|
+
assertInsideWorkspace(cwd, dir);
|
|
2867
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => {
|
|
2868
|
+
const m = d.name.match(/^([1-9]\d*)(?:-|$)/);
|
|
2869
|
+
const appId = m ? Number(m[1]) : null;
|
|
2870
|
+
const r = readAppMark(path9.join(dir, d.name));
|
|
2871
|
+
if (r.state !== "ok") return { name: d.name, appId, mark: r.state };
|
|
2872
|
+
const ok = appId !== null && r.mark.appId === appId && r.mark.profile === def.profile && r.mark.origin === def.baseUrl && r.mark.guestSpaceId === def.guestSpaceId;
|
|
2873
|
+
return { name: d.name, appId, mark: ok ? "ok" : "conflict" };
|
|
2874
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
2875
|
+
}
|
|
2876
|
+
function unusedProfileDirs(cwd, set) {
|
|
2877
|
+
const root = path9.join(cwd, KINTONE_ROOT);
|
|
2878
|
+
if (!existsSync4(root)) return [];
|
|
2879
|
+
assertInsideWorkspace(cwd, root);
|
|
2880
|
+
return readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !set.profiles.has(d.name)).map((d) => d.name).sort();
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// src/permission.ts
|
|
2884
|
+
var ALLOWED_CLASSES = {
|
|
2885
|
+
settings: ["edit", "newsettings"],
|
|
2886
|
+
snapshot: ["snapshot"],
|
|
2887
|
+
fields: ["fields"],
|
|
2888
|
+
record: ["record"]
|
|
2889
|
+
};
|
|
2890
|
+
var OP_LABEL = {
|
|
2891
|
+
settings: "設定(-edit.json と新しい設定)",
|
|
2892
|
+
snapshot: "新しい名前のダウンロード / pull",
|
|
2893
|
+
fields: "fields.json",
|
|
2894
|
+
record: "records/ の下"
|
|
2895
|
+
};
|
|
2896
|
+
var CREATES_FOLDER = ["fields", "snapshot"];
|
|
2897
|
+
function classOfAppPath(relInFolder) {
|
|
2898
|
+
const rel2 = relInFolder.replace(/\\/g, "/");
|
|
2899
|
+
if (rel2.split("/").some((seg) => seg.startsWith("."))) return "other";
|
|
2900
|
+
if (rel2.startsWith("records/")) return rel2.toLowerCase().endsWith(".json") && rel2.split("/").length === 2 ? "record" : "other";
|
|
2901
|
+
if (rel2.includes("/")) return "other";
|
|
2902
|
+
if (rel2 === "fields.json") return "fields";
|
|
2903
|
+
if (SNAPSHOT_RE.test(rel2)) return "snapshot";
|
|
2904
|
+
if (EDIT_RE.test(rel2)) return "edit";
|
|
2905
|
+
if (rel2.toLowerCase().endsWith(".json")) return "newsettings";
|
|
2906
|
+
return "other";
|
|
2907
|
+
}
|
|
2908
|
+
function assertChangeAllowed(root, file, op, mode) {
|
|
2909
|
+
const abs = path10.resolve(root, file);
|
|
2910
|
+
if (!isInside(abs, path10.join(root, KINTONE_ROOT))) return;
|
|
2911
|
+
const rel2 = path10.relative(root, abs);
|
|
2912
|
+
if (mode.kind !== "profiles") {
|
|
2913
|
+
if (mode.kind === "legacy-blocked") throw legacyBlockedError();
|
|
2914
|
+
throw new PermissionError("kintone/ の下は、kintone の接続のファイルがあるときだけ変える(今の形では settings/ などを使う)");
|
|
2915
|
+
}
|
|
2916
|
+
const folder = appFolderOfFile(root, abs);
|
|
2917
|
+
if (!folder) throw new PermissionError(`アプリのフォルダー(kintone/<profile>/<番号>-…/)の中でない: ${rel2}`);
|
|
2918
|
+
const { set, reload } = mode.connections;
|
|
2919
|
+
const def = set.profiles.get(folder.profile);
|
|
2920
|
+
if (!def) throw new PermissionError(`profile「${folder.profile}」は接続のファイル(${set.fileName})に無い(使われていないフォルダーは変えない)`);
|
|
2921
|
+
const cls = classOfAppPath(path10.relative(folder.dir, abs));
|
|
2922
|
+
if (cls === "snapshot" && existsSync5(abs)) throw new PermissionError(`ダウンロード / pull のファイルは書き換えない: ${rel2}。edit で -edit.json に写して直す`);
|
|
2923
|
+
if (!ALLOWED_CLASSES[op].includes(cls)) throw new PermissionError(`この操作でアプリのフォルダーに書けるのは ${OP_LABEL[op]} だけ: ${rel2}`);
|
|
2924
|
+
if (existsSync5(folder.dir)) assertAppMark(folder.dir, def, folder.appId);
|
|
2925
|
+
else if (!CREATES_FOLDER.includes(op)) throw new PermissionError(`アプリのフォルダーが無い: ${path10.relative(root, folder.dir)}(先に fields か pull で作る)`);
|
|
2926
|
+
assertSameConnection({ set, def }, reload, folder.appId);
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
// src/commit-file.ts
|
|
2930
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
2931
|
+
import { constants, copyFileSync, linkSync, lstatSync as lstatSync3, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2932
|
+
import path11 from "node:path";
|
|
2933
|
+
var LOCK_STALE_MS = 6e4;
|
|
2934
|
+
var FileExistsError = class extends Error {
|
|
2935
|
+
};
|
|
2936
|
+
var LockBusyError = class extends Error {
|
|
2937
|
+
};
|
|
2938
|
+
var errCode = (e) => e?.code;
|
|
2939
|
+
var rand = () => randomBytes3(6).toString("hex");
|
|
2940
|
+
function sleepSync(ms) {
|
|
2941
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
2942
|
+
}
|
|
2943
|
+
function withRetry(fn, tries = 3) {
|
|
2944
|
+
for (let i = 1; ; i++) {
|
|
2945
|
+
try {
|
|
2946
|
+
return fn();
|
|
2947
|
+
} catch (e) {
|
|
2948
|
+
const code = errCode(e);
|
|
2949
|
+
if (i < tries && (code === "EPERM" || code === "EBUSY")) {
|
|
2950
|
+
sleepSync(20 * i);
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
throw e;
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
function cleanupAll(steps, tries = 3) {
|
|
2958
|
+
const errors = [];
|
|
2959
|
+
for (const step of steps) {
|
|
2960
|
+
try {
|
|
2961
|
+
withRetry(step, tries);
|
|
2962
|
+
} catch (e) {
|
|
2963
|
+
errors.push(e instanceof Error ? e.message : String(e));
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
return errors;
|
|
2967
|
+
}
|
|
2968
|
+
function tempPathFor(target) {
|
|
2969
|
+
return path11.join(path11.dirname(target), `.${path11.basename(target)}.${rand()}.tmp`);
|
|
2970
|
+
}
|
|
2971
|
+
function placeNew(tmp, target, link = linkSync) {
|
|
2972
|
+
try {
|
|
2973
|
+
link(tmp, target);
|
|
2974
|
+
return;
|
|
2975
|
+
} catch (e) {
|
|
2976
|
+
const code = errCode(e);
|
|
2977
|
+
if (code === "EEXIST") throw new FileExistsError(`確定の直前に同じ名前のファイルができた: ${path11.basename(target)}`);
|
|
2978
|
+
if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV" && code !== "ENOSYS") throw e;
|
|
2979
|
+
}
|
|
2980
|
+
try {
|
|
2981
|
+
copyFileSync(tmp, target, constants.COPYFILE_EXCL);
|
|
2982
|
+
} catch (e) {
|
|
2983
|
+
if (errCode(e) === "EEXIST") throw new FileExistsError(`確定の直前に同じ名前のファイルができた: ${path11.basename(target)}`);
|
|
2984
|
+
throw e;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
function writeNewFile(target, text, recheck = () => {
|
|
2988
|
+
}) {
|
|
2989
|
+
const tmp = tempPathFor(target);
|
|
2990
|
+
let placed = false;
|
|
2991
|
+
let failure;
|
|
2992
|
+
try {
|
|
2993
|
+
recheck();
|
|
2994
|
+
writeFileSync2(tmp, text, { encoding: "utf8", flag: "wx" });
|
|
2995
|
+
recheck();
|
|
2996
|
+
placeNew(tmp, target);
|
|
2997
|
+
placed = true;
|
|
2998
|
+
} catch (e) {
|
|
2999
|
+
failure = e;
|
|
3000
|
+
}
|
|
3001
|
+
const cleanup = cleanupAll([() => rmSync2(tmp, { force: true })]);
|
|
3002
|
+
if (!placed) throw failure;
|
|
3003
|
+
return { cleanup };
|
|
3004
|
+
}
|
|
3005
|
+
function lockPathFor(target) {
|
|
3006
|
+
return path11.join(path11.dirname(target), `.${path11.basename(target)}.formula-lock`);
|
|
3007
|
+
}
|
|
3008
|
+
function lstatOrNull(p) {
|
|
3009
|
+
try {
|
|
3010
|
+
return lstatSync3(p);
|
|
3011
|
+
} catch (e) {
|
|
3012
|
+
if (errCode(e) === "ENOENT") return null;
|
|
3013
|
+
throw e;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
function acquireLock(target, staleMs = LOCK_STALE_MS, hooks = {}) {
|
|
3017
|
+
const file = lockPathFor(target);
|
|
3018
|
+
const token = randomBytes3(16).toString("hex");
|
|
3019
|
+
const busy = (why = "同じファイルへの別の保存が進行中。少し待ってからやり直す") => new LockBusyError(why);
|
|
3020
|
+
const tryCreate = () => {
|
|
3021
|
+
try {
|
|
3022
|
+
writeFileSync2(file, token, { flag: "wx" });
|
|
3023
|
+
return true;
|
|
3024
|
+
} catch (e) {
|
|
3025
|
+
if (errCode(e) === "EEXIST") return false;
|
|
3026
|
+
throw e;
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
const isStale = (st) => Date.now() - st.mtimeMs > staleMs;
|
|
3030
|
+
if (!tryCreate()) {
|
|
3031
|
+
const st = lstatOrNull(file);
|
|
3032
|
+
if (st && !st.isFile()) throw busy(`ロックのファイルが普通のファイルでない: ${path11.basename(file)}(利用者が確かめて消す)`);
|
|
3033
|
+
if (st && !isStale(st)) throw busy();
|
|
3034
|
+
if (st) {
|
|
3035
|
+
const aside = `${file}.${rand()}.stale`;
|
|
3036
|
+
hooks.beforeClaim?.();
|
|
3037
|
+
try {
|
|
3038
|
+
withRetry(() => renameSync2(file, aside));
|
|
3039
|
+
} catch (e) {
|
|
3040
|
+
if (errCode(e) !== "ENOENT") throw e;
|
|
3041
|
+
}
|
|
3042
|
+
const moved = lstatOrNull(aside);
|
|
3043
|
+
if (moved) {
|
|
3044
|
+
if (!moved.isFile()) throw busy(`ロックのファイルが普通のファイルでない: ${path11.basename(aside)}(利用者が確かめて消す)`);
|
|
3045
|
+
if (!isStale(moved)) {
|
|
3046
|
+
hooks.beforeRestore?.();
|
|
3047
|
+
try {
|
|
3048
|
+
withRetry(() => placeNew(aside, file));
|
|
3049
|
+
} catch (e) {
|
|
3050
|
+
if (!(e instanceof FileExistsError)) throw busy(`新しいロックを戻せなかった(${errCode(e) ?? e.message}): ${path11.basename(aside)} を確かめて ${path11.basename(file)} に戻すか消す`);
|
|
3051
|
+
}
|
|
3052
|
+
cleanupAll([() => rmSync2(aside, { force: true })]);
|
|
3053
|
+
throw busy();
|
|
3054
|
+
}
|
|
3055
|
+
cleanupAll([() => rmSync2(aside, { force: true })]);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
if (!tryCreate()) throw busy();
|
|
3059
|
+
}
|
|
3060
|
+
const owned = () => {
|
|
3061
|
+
try {
|
|
3062
|
+
const st = lstatSync3(file);
|
|
3063
|
+
return st.isFile() && readFileSync8(file, "utf8") === token;
|
|
3064
|
+
} catch {
|
|
3065
|
+
return false;
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
return {
|
|
3069
|
+
owned,
|
|
3070
|
+
release: () => {
|
|
3071
|
+
if (!owned()) return false;
|
|
3072
|
+
withRetry(() => rmSync2(file, { force: true }));
|
|
3073
|
+
return true;
|
|
3074
|
+
}
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
// src/commands/take.ts
|
|
3079
|
+
function removeSource(src, warnings, rel2) {
|
|
3080
|
+
try {
|
|
3081
|
+
withRetry(() => unlinkSync(src));
|
|
3082
|
+
return true;
|
|
3083
|
+
} catch (e) {
|
|
3084
|
+
warnings.push(`${rel2} は行き先に置いたが、inbox から消せなかった(${e.code ?? "理由は不明"})。消してよい`);
|
|
3085
|
+
return false;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
function readInbox(src) {
|
|
3089
|
+
let bytes;
|
|
3090
|
+
let fd;
|
|
3091
|
+
try {
|
|
3092
|
+
fd = openSync2(src, "r");
|
|
3093
|
+
if (!fstatSync2(fd).isFile()) return { reason: "普通のファイルでない" };
|
|
3094
|
+
const buf = Buffer.alloc(MAX_INPUT_BYTES + 1);
|
|
3095
|
+
let off = 0;
|
|
3096
|
+
for (; ; ) {
|
|
3097
|
+
const n = readSync2(fd, buf, off, buf.length - off, off);
|
|
3098
|
+
if (n === 0) break;
|
|
3099
|
+
off += n;
|
|
3100
|
+
if (off > MAX_INPUT_BYTES) return { reason: `大きすぎる(上限 ${MAX_INPUT_BYTES.toLocaleString()} バイト)` };
|
|
3101
|
+
}
|
|
3102
|
+
bytes = buf.subarray(0, off);
|
|
3103
|
+
} catch (e) {
|
|
3104
|
+
return { reason: `読めない(${e.code ?? "理由は不明"})` };
|
|
3105
|
+
} finally {
|
|
3106
|
+
if (fd !== void 0) closeSync2(fd);
|
|
3107
|
+
}
|
|
3108
|
+
const text = bytes.toString("utf8");
|
|
3109
|
+
let data;
|
|
3110
|
+
try {
|
|
3111
|
+
data = JSON.parse(text);
|
|
3112
|
+
} catch (e) {
|
|
3113
|
+
return { reason: `JSON として読めない${jsonErrorWhere(e)}(中身は返さない)` };
|
|
3114
|
+
}
|
|
3115
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return { reason: "JSON の最上位がオブジェクトでない" };
|
|
3116
|
+
return { text, data };
|
|
3117
|
+
}
|
|
3118
|
+
function takeInbox(cwdIn, mode, profile) {
|
|
3119
|
+
const connections = requireProfiles(mode, "take");
|
|
3120
|
+
const def = pickProfile(connections.set, profile);
|
|
3121
|
+
const cwd = realResolve(".", cwdIn);
|
|
3122
|
+
const result = { moved: [], skipped: [], warnings: [] };
|
|
3123
|
+
const inbox = path12.join(cwd, INBOX);
|
|
3124
|
+
if (!existsSync6(inbox)) return result;
|
|
3125
|
+
assertInsideWorkspace(cwd, inbox);
|
|
3126
|
+
const candidates = [];
|
|
3127
|
+
for (const name of readdirSync2(inbox).filter((n) => n.toLowerCase().endsWith(".json")).sort()) {
|
|
3128
|
+
const rel2 = `${INBOX}/${name}`;
|
|
3129
|
+
const src = resolveRead(rel2, cwd);
|
|
3130
|
+
const read = readInbox(src);
|
|
3131
|
+
if ("reason" in read) {
|
|
3132
|
+
result.skipped.push({ file: rel2, reason: read.reason });
|
|
3133
|
+
continue;
|
|
3134
|
+
}
|
|
3135
|
+
const data = read.data;
|
|
3136
|
+
if (data.pluginID !== FORMULA_CONFIG_PLUGIN_ID) {
|
|
3137
|
+
result.skipped.push({ file: rel2, reason: "計算式プラグインの設定のファイルではない(pluginID が違う)" });
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
const appId = data.appId;
|
|
3141
|
+
if (typeof appId !== "number" || !Number.isSafeInteger(appId) || appId <= 0) {
|
|
3142
|
+
result.skipped.push({ file: rel2, reason: "封筒の appId が無い" });
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
if (!SNAPSHOT_RE.test(name)) {
|
|
3146
|
+
result.skipped.push({ file: rel2, reason: "ダウンロードの名前(rex0220-formula-app<番号>-<日時>.json)ではない。設定画面のダウンロードの名前のまま inbox/ に置く" });
|
|
3147
|
+
continue;
|
|
3148
|
+
}
|
|
3149
|
+
const m = name.match(/-app(\d+)-/);
|
|
3150
|
+
if (m && Number(m[1]) !== appId) {
|
|
3151
|
+
result.skipped.push({ file: rel2, reason: `ファイル名のアプリ ${m[1]} と封筒の appId ${appId} が違う` });
|
|
3152
|
+
continue;
|
|
3153
|
+
}
|
|
3154
|
+
candidates.push({ rel: rel2, src, name, appId, appName: typeof data.appName === "string" ? data.appName : "", text: read.text });
|
|
3155
|
+
}
|
|
3156
|
+
const dirByApp = /* @__PURE__ */ new Map();
|
|
3157
|
+
for (const appId of new Set(candidates.map((c) => c.appId))) {
|
|
3158
|
+
const newest = candidates.filter((c) => c.appId === appId).reduce((a, b) => b.name > a.name ? b : a);
|
|
3159
|
+
dirByApp.set(appId, appDirFor(cwd, def.profile, appId, newest.appName));
|
|
3160
|
+
}
|
|
3161
|
+
const plans = candidates.map((c) => {
|
|
3162
|
+
const dir = dirByApp.get(c.appId);
|
|
3163
|
+
return { ...c, dir, dest: resolveWrite(path12.join(dir, c.name), WRITE_ROOTS.kintone, cwd) };
|
|
3164
|
+
});
|
|
3165
|
+
const checked = /* @__PURE__ */ new Set();
|
|
3166
|
+
for (const p of plans) {
|
|
3167
|
+
if (checked.has(p.dir) || !existsSync6(p.dir)) continue;
|
|
3168
|
+
assertAppMark(p.dir, def, p.appId);
|
|
3169
|
+
checked.add(p.dir);
|
|
3170
|
+
}
|
|
3171
|
+
for (const p of plans) {
|
|
3172
|
+
try {
|
|
3173
|
+
if (existsSync6(p.dest)) {
|
|
3174
|
+
if (readFileSync9(p.dest, "utf8") !== p.text) {
|
|
3175
|
+
result.skipped.push({ file: p.rel, reason: `行き先に同じ名前の別の中身がある: ${path12.relative(cwd, p.dest)}` });
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
assertAppMark(p.dir, def, p.appId);
|
|
3179
|
+
assertSameConnection({ set: connections.set, def }, connections.reload, p.appId);
|
|
3180
|
+
const removed2 = removeSource(p.src, result.warnings, p.rel);
|
|
3181
|
+
result.moved.push({ file: p.rel, to: path12.relative(cwd, p.dest), same: true, ...removed2 ? {} : { leftInInbox: true } });
|
|
3182
|
+
continue;
|
|
3183
|
+
}
|
|
3184
|
+
if (!existsSync6(p.dir)) {
|
|
3185
|
+
const found = findAppDir(cwd, def.profile, p.appId);
|
|
3186
|
+
if (found !== null && found !== p.dir) {
|
|
3187
|
+
result.skipped.push({ file: p.rel, reason: `同じアプリのフォルダーが移す途中で別の名前で作られた: ${path12.relative(cwd, found)}。もう一度 take する` });
|
|
3188
|
+
continue;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
assertChangeAllowed(cwd, p.dest, "snapshot", mode);
|
|
3192
|
+
ensureAppFolder(p.dir, def, p.appId);
|
|
3193
|
+
const { cleanup } = writeNewFile(p.dest, p.text, () => {
|
|
3194
|
+
if (resolveWrite(path12.join(p.dir, p.name), WRITE_ROOTS.kintone, cwd) !== p.dest) throw new PermissionError(`書く先のフォルダーが途中で変わった(symlink など): ${path12.relative(cwd, p.dest)}`);
|
|
3195
|
+
assertChangeAllowed(cwd, p.dest, "snapshot", mode);
|
|
3196
|
+
});
|
|
3197
|
+
for (const c of cleanup) result.warnings.push(`${p.rel} は行き先に置いたが、一時ファイルを消せなかった(${c})。${path12.relative(cwd, path12.dirname(p.dest))} の .${p.name}.*.tmp を消してよい`);
|
|
3198
|
+
} catch (e) {
|
|
3199
|
+
if (e instanceof ConnectionError) {
|
|
3200
|
+
throw new ConnectionError(`${e.message}(この呼び出しで移したのは ${result.moved.length} 件。inbox に残ったものは、直した後にもう一度 take する)`, e.code);
|
|
3201
|
+
}
|
|
3202
|
+
if (e instanceof PermissionError) {
|
|
3203
|
+
result.skipped.push({ file: p.rel, reason: e.message });
|
|
3204
|
+
continue;
|
|
3205
|
+
}
|
|
3206
|
+
if (e instanceof FileExistsError) {
|
|
3207
|
+
result.skipped.push({ file: p.rel, reason: `行き先に同じ名前のファイルができた(移す途中で): ${path12.relative(cwd, p.dest)}。もう一度 take する` });
|
|
3208
|
+
continue;
|
|
3209
|
+
}
|
|
3210
|
+
throw e;
|
|
3211
|
+
}
|
|
3212
|
+
const removed = removeSource(p.src, result.warnings, p.rel);
|
|
3213
|
+
result.moved.push({ file: p.rel, to: path12.relative(cwd, p.dest), same: false, ...removed ? {} : { leftInInbox: true } });
|
|
3214
|
+
}
|
|
3215
|
+
return result;
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
// src/commands/save.ts
|
|
3219
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3220
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync10, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3221
|
+
import path14 from "node:path";
|
|
3222
|
+
|
|
2252
3223
|
// src/worker/run.ts
|
|
2253
|
-
import { existsSync as
|
|
3224
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
2254
3225
|
import { Worker } from "node:worker_threads";
|
|
2255
3226
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2256
|
-
import
|
|
3227
|
+
import path13 from "node:path";
|
|
2257
3228
|
|
|
2258
3229
|
// src/worker/eval-run.ts
|
|
2259
3230
|
async function runEvalJob(job) {
|
|
2260
|
-
const engine = await loadEngine({ pluginZip: job.pluginZip, devPluginDir: job.devPluginDir, mode: job.mode });
|
|
3231
|
+
const engine = await loadEngine({ pluginZip: job.pluginZip, devPluginDir: job.devPluginDir, mode: job.mode, vendorDir: job.vendorDir });
|
|
2261
3232
|
if (job.baseUrl) engine.setContext({ baseUrl: job.baseUrl });
|
|
2262
3233
|
const usersInfo = buildUsersInfo(job.usersSource);
|
|
2263
3234
|
const usersLabel = job.usersOrigin.kind === "file" ? job.usersOrigin.file : "既定のテストデータ(架空)";
|
|
@@ -2274,7 +3245,16 @@ async function runEvalJob(job) {
|
|
|
2274
3245
|
usersOrigin: job.usersOrigin
|
|
2275
3246
|
});
|
|
2276
3247
|
const outcome = {
|
|
2277
|
-
normalize: {
|
|
3248
|
+
normalize: {
|
|
3249
|
+
summary: res.summary,
|
|
3250
|
+
hasErrors: res.findings.hasErrors,
|
|
3251
|
+
warnings: res.findings.count("warning"),
|
|
3252
|
+
findings: [...res.findings.items],
|
|
3253
|
+
output: res.output,
|
|
3254
|
+
checkDiffs: res.checkDiffs,
|
|
3255
|
+
appId: res.appId,
|
|
3256
|
+
size: res.size
|
|
3257
|
+
},
|
|
2278
3258
|
engineWarnings: [...engine.warnings]
|
|
2279
3259
|
};
|
|
2280
3260
|
if (job.kind === "calc" && job.calc && !res.findings.hasErrors) {
|
|
@@ -2292,7 +3272,7 @@ async function runEvalJob(job) {
|
|
|
2292
3272
|
usersInfo,
|
|
2293
3273
|
usersOrigin: job.usersOrigin
|
|
2294
3274
|
});
|
|
2295
|
-
outcome.calc = { text: r.text, failed: r.records.some((x) => x.errors.length > 0 || x.failed) };
|
|
3275
|
+
outcome.calc = { text: r.text, failed: r.records.some((x) => x.errors.length > 0 || x.failed), records: r.records, contextLine: r.contextLine, notice: r.notice, summary: r.summary };
|
|
2296
3276
|
}
|
|
2297
3277
|
return outcome;
|
|
2298
3278
|
}
|
|
@@ -2313,71 +3293,359 @@ var EvalError = class extends Error {
|
|
|
2313
3293
|
};
|
|
2314
3294
|
function workerEntry() {
|
|
2315
3295
|
const here2 = fileURLToPath2(import.meta.url);
|
|
2316
|
-
const dir =
|
|
2317
|
-
const built =
|
|
2318
|
-
if (here2.endsWith(".mjs") &&
|
|
2319
|
-
const src =
|
|
2320
|
-
if (
|
|
3296
|
+
const dir = path13.dirname(here2);
|
|
3297
|
+
const built = path13.join(dir, "eval-worker.mjs");
|
|
3298
|
+
if (here2.endsWith(".mjs") && existsSync7(built)) return built;
|
|
3299
|
+
const src = path13.join(dir, "eval-worker.ts");
|
|
3300
|
+
if (existsSync7(src)) return src;
|
|
2321
3301
|
throw new Error(`Worker の入口が無い: ${built}`);
|
|
2322
3302
|
}
|
|
2323
3303
|
async function runEval(job, opt = {}) {
|
|
2324
3304
|
const timeoutSec = Math.min(Math.max(opt.timeoutSec ?? DEFAULT_TIMEOUT_SEC, 1), MAX_TIMEOUT_SEC);
|
|
2325
3305
|
if (opt.inline) return runEvalJob(job);
|
|
2326
|
-
|
|
3306
|
+
let entry;
|
|
3307
|
+
if (opt.workerFile !== void 0) {
|
|
3308
|
+
if (!path13.isAbsolute(opt.workerFile) || !existsSync7(opt.workerFile)) throw new Error(`Worker の入口が無い(絶対パスで指定する): ${opt.workerFile}`);
|
|
3309
|
+
entry = opt.workerFile;
|
|
3310
|
+
} else entry = workerEntry();
|
|
3311
|
+
const capture = opt.captureOutput;
|
|
2327
3312
|
return new Promise((resolve, reject) => {
|
|
2328
|
-
const worker = new Worker(entry, { workerData: job, stdout:
|
|
3313
|
+
const worker = new Worker(entry, { workerData: job, stdout: capture !== void 0, stderr: capture !== void 0 });
|
|
3314
|
+
if (capture) {
|
|
3315
|
+
worker.stdout.on("data", (d) => capture(String(d), "stdout"));
|
|
3316
|
+
worker.stderr.on("data", (d) => capture(String(d), "stderr"));
|
|
3317
|
+
}
|
|
2329
3318
|
let settled = false;
|
|
2330
|
-
const
|
|
3319
|
+
const finish2 = (fn) => {
|
|
2331
3320
|
if (settled) return;
|
|
2332
3321
|
settled = true;
|
|
2333
3322
|
clearTimeout(timer);
|
|
2334
3323
|
fn();
|
|
2335
3324
|
};
|
|
2336
3325
|
const timer = setTimeout(() => {
|
|
2337
|
-
|
|
3326
|
+
finish2(() => {
|
|
2338
3327
|
void worker.terminate();
|
|
2339
3328
|
reject(new EvalTimeoutError(`評価が ${timeoutSec} 秒を超えたので止めた(式が重いか無限に近い繰り返し。--timeout <秒> で上げられる。上限 ${MAX_TIMEOUT_SEC})`));
|
|
2340
3329
|
});
|
|
2341
3330
|
}, timeoutSec * 1e3);
|
|
2342
3331
|
worker.on("message", (m) => {
|
|
2343
|
-
|
|
3332
|
+
finish2(() => {
|
|
2344
3333
|
if (m.ok) resolve(m.outcome);
|
|
2345
3334
|
else reject(new EvalError(m.error.message, m.error.name, m.error.code));
|
|
2346
3335
|
});
|
|
2347
3336
|
});
|
|
2348
|
-
worker.on("error", (e) =>
|
|
2349
|
-
worker.on("exit", (code) =>
|
|
3337
|
+
worker.on("error", (e) => finish2(() => reject(e)));
|
|
3338
|
+
worker.on("exit", (code) => finish2(() => reject(new Error(`評価の Worker が結果を返さずに終わった(exit ${code})`))));
|
|
2350
3339
|
});
|
|
2351
3340
|
}
|
|
2352
3341
|
|
|
3342
|
+
// src/commands/save.ts
|
|
3343
|
+
var MAX_SAVE_INPUT_BYTES = 256 * 1024;
|
|
3344
|
+
var MAX_UPDATE_ROWS = 50;
|
|
3345
|
+
function digestOf(text) {
|
|
3346
|
+
return createHash4("sha256").update(text).digest("hex");
|
|
3347
|
+
}
|
|
3348
|
+
var rel = (root, abs) => path14.relative(root, abs).replace(/\\/g, "/");
|
|
3349
|
+
var SaveDenied = class extends Error {
|
|
3350
|
+
};
|
|
3351
|
+
var ConflictError = class extends Error {
|
|
3352
|
+
};
|
|
3353
|
+
var isPlainObject2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3354
|
+
function parseInput(text, what) {
|
|
3355
|
+
try {
|
|
3356
|
+
return JSON.parse(text);
|
|
3357
|
+
} catch (e) {
|
|
3358
|
+
throw new InputError(`${what} を JSON として読めない${jsonErrorWhere(e)}(中身は返さない)`);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
function tooLarge(text) {
|
|
3362
|
+
return Buffer.byteLength(text, "utf8") > MAX_SAVE_INPUT_BYTES;
|
|
3363
|
+
}
|
|
3364
|
+
function fieldsFileFor(ctx, target, fields, envelope) {
|
|
3365
|
+
const folder = appFolderOfFile(ctx.root, target);
|
|
3366
|
+
if (folder) {
|
|
3367
|
+
const own = resolveRead(path14.join(folder.dir, "fields.json"), ctx.root);
|
|
3368
|
+
assertUsableInKintone(ctx.root, own, ctx.mode);
|
|
3369
|
+
if (!existsSync8(own)) throw new InputError(`アプリのフォルダーに fields.json が無い: ${rel(ctx.root, own)}(fields コマンドか fml_fields で取る)`);
|
|
3370
|
+
if (fields !== void 0 && resolveRead(fields, ctx.root) !== own) throw new SaveDenied(`アプリのフォルダーの中の設定は、同じフォルダーの fields.json で検査する(fields は渡さないか、${rel(ctx.root, own)} にする)`);
|
|
3371
|
+
return own;
|
|
3372
|
+
}
|
|
3373
|
+
const appId = typeof envelope.appId === "number" && Number.isInteger(envelope.appId) && envelope.appId > 0 ? envelope.appId : void 0;
|
|
3374
|
+
const relFile = fields ?? (appId !== void 0 ? path14.join("fields", `${appId}.json`) : void 0);
|
|
3375
|
+
if (!relFile) throw new InputError("fields(項目定義のファイル)が要る(設定の封筒に appId があれば fields/<appId>.json を既定にする。アプリのフォルダー kintone/<profile>/<番号>-…/ の中なら同じフォルダーの fields.json を使う)");
|
|
3376
|
+
const file = resolveRead(relFile, ctx.root);
|
|
3377
|
+
assertUsableInKintone(ctx.root, file, ctx.mode);
|
|
3378
|
+
if (!existsSync8(file)) throw new InputError(`項目定義のファイルが無い: ${rel(ctx.root, file)}(fields コマンドか fml_fields で取る)`);
|
|
3379
|
+
return file;
|
|
3380
|
+
}
|
|
3381
|
+
async function normalizeText(ctx, target, text, fieldsFile) {
|
|
3382
|
+
const job = {
|
|
3383
|
+
kind: "normalize",
|
|
3384
|
+
pluginZip: ctx.source.pluginZip,
|
|
3385
|
+
devPluginDir: ctx.source.devPluginDir,
|
|
3386
|
+
mode: ctx.source.mode,
|
|
3387
|
+
vendorDir: ctx.source.vendorDir,
|
|
3388
|
+
baseUrl: ctx.baseUrl,
|
|
3389
|
+
settingsText: text,
|
|
3390
|
+
settingsFile: rel(ctx.root, target),
|
|
3391
|
+
fields: readFieldsFile(fieldsFile),
|
|
3392
|
+
usersSource: ctx.usersSource ?? DEFAULT_USERS,
|
|
3393
|
+
usersOrigin: ctx.usersOrigin ?? { kind: "default" },
|
|
3394
|
+
policy: ctx.policy,
|
|
3395
|
+
lang: ctx.lang
|
|
3396
|
+
};
|
|
3397
|
+
return runEval(job, ctx.run);
|
|
3398
|
+
}
|
|
3399
|
+
function commit(ctx, target, text, mode, recheck) {
|
|
3400
|
+
const tmp = tempPathFor(target);
|
|
3401
|
+
let lock;
|
|
3402
|
+
let digest;
|
|
3403
|
+
let failure;
|
|
3404
|
+
const checkTarget = () => {
|
|
3405
|
+
if (resolveWrite(rel(ctx.root, target), WRITE_ROOTS.settings, ctx.root) !== target) throw new SaveDenied("書く先のフォルダーが途中で変わった(symlink など)");
|
|
3406
|
+
assertChangeAllowed(ctx.root, target, "settings", ctx.mode);
|
|
3407
|
+
if (SNAPSHOT_RE.test(path14.basename(target))) throw new SaveDenied("ダウンロード / pull のファイルは書き換えない。edit で -edit.json に写して直す");
|
|
3408
|
+
};
|
|
3409
|
+
try {
|
|
3410
|
+
checkTarget();
|
|
3411
|
+
mkdirSync2(path14.dirname(target), { recursive: true });
|
|
3412
|
+
writeFileSync3(tmp, text, { encoding: "utf8", flag: "wx" });
|
|
3413
|
+
lock = acquireLock(target);
|
|
3414
|
+
checkTarget();
|
|
3415
|
+
recheck();
|
|
3416
|
+
if (!lock.owned()) throw new ConflictError("ロックを失った(保存が 60 秒より長く止まった)。fml_summary で読み直してやり直す");
|
|
3417
|
+
if (mode === "new") withRetry(() => placeNew(tmp, target));
|
|
3418
|
+
else withRetry(() => renameSync3(tmp, target));
|
|
3419
|
+
digest = digestOf(text);
|
|
3420
|
+
ctx.afterPlace?.();
|
|
3421
|
+
} catch (e) {
|
|
3422
|
+
failure = e;
|
|
3423
|
+
}
|
|
3424
|
+
const cleanup = cleanupAll([...lock ? [() => void lock.release()] : [], () => rmSync3(tmp, { force: true })]);
|
|
3425
|
+
if (digest === void 0) throw failure;
|
|
3426
|
+
return { digest, cleanup };
|
|
3427
|
+
}
|
|
3428
|
+
function statusOf(e) {
|
|
3429
|
+
if (e instanceof ConnectionError) return connectionStatusOf(e);
|
|
3430
|
+
if (e instanceof ConflictError || e instanceof FileExistsError || e instanceof LockBusyError) return "conflict";
|
|
3431
|
+
if (e instanceof SaveDenied || e instanceof PermissionError || e instanceof PathError) return "denied";
|
|
3432
|
+
if (e instanceof InputError || e instanceof SyntaxError) return "invalid";
|
|
3433
|
+
if (e instanceof EvalError && e.innerName === "InputError") return "invalid";
|
|
3434
|
+
return "failed";
|
|
3435
|
+
}
|
|
3436
|
+
function connectionStatusOf(e) {
|
|
3437
|
+
switch (e.code) {
|
|
3438
|
+
case "inside-workspace":
|
|
3439
|
+
return "denied";
|
|
3440
|
+
case "unknown-profile":
|
|
3441
|
+
case "guest-mismatch":
|
|
3442
|
+
return "invalid";
|
|
3443
|
+
case "app-marker-missing":
|
|
3444
|
+
case "app-marker-invalid":
|
|
3445
|
+
case "app-identity-conflict":
|
|
3446
|
+
case "connection-changed":
|
|
3447
|
+
return "conflict";
|
|
3448
|
+
default:
|
|
3449
|
+
return "failed";
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
function codeOf(e) {
|
|
3453
|
+
if (e instanceof ConnectionError) return e.code;
|
|
3454
|
+
if (e instanceof ConflictError || e instanceof FileExistsError || e instanceof LockBusyError) return "ConflictError";
|
|
3455
|
+
if (e instanceof SaveDenied || e instanceof PermissionError) return "PermissionError";
|
|
3456
|
+
if (e instanceof EvalError) return e.innerName;
|
|
3457
|
+
if (e instanceof EvalTimeoutError) return "EvalTimeoutError";
|
|
3458
|
+
return e instanceof Error ? e.constructor.name : "Error";
|
|
3459
|
+
}
|
|
3460
|
+
function errorResult(root, target, e, findings = []) {
|
|
3461
|
+
return { status: statusOf(e), code: codeOf(e), path: rel(root, target), findings, message: e instanceof Error ? e.message : String(e) };
|
|
3462
|
+
}
|
|
3463
|
+
function finish(ctx, target, outcome, run, extra = {}) {
|
|
3464
|
+
const findings = outcome.normalize.findings;
|
|
3465
|
+
try {
|
|
3466
|
+
const { digest, cleanup } = run();
|
|
3467
|
+
return {
|
|
3468
|
+
status: "ok",
|
|
3469
|
+
path: rel(ctx.root, target),
|
|
3470
|
+
digest,
|
|
3471
|
+
findings,
|
|
3472
|
+
summary: outcome.normalize.summary,
|
|
3473
|
+
...extra,
|
|
3474
|
+
...cleanup.length ? { cleanup, message: `確定したが、一時ファイルかロックを消せなかった(.${path14.basename(target)}.* を消してよい)` } : {}
|
|
3475
|
+
};
|
|
3476
|
+
} catch (e) {
|
|
3477
|
+
return { ...errorResult(ctx.root, target, e, findings), summary: outcome.normalize.summary };
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
async function saveNewSettings(ctx, opt) {
|
|
3481
|
+
if (opt.expectedAbsent !== true) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: "新しい設定の保存は expectedAbsent: true が要る(既存のファイルを直すなら fml_update_fields)" };
|
|
3482
|
+
if (tooLarge(opt.content)) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: `content は ${MAX_SAVE_INPUT_BYTES.toLocaleString()} バイトまで` };
|
|
3483
|
+
let target;
|
|
3484
|
+
try {
|
|
3485
|
+
target = resolveWrite(opt.path, WRITE_ROOTS.settings, ctx.root);
|
|
3486
|
+
const name = path14.basename(target);
|
|
3487
|
+
if (SNAPSHOT_RE.test(name)) throw new SaveDenied("ダウンロード / pull の名前(rex0220-formula-app<番号>-<日時>.json)では新しく保存しない");
|
|
3488
|
+
if (EDIT_RE.test(name)) throw new SaveDenied("-edit.json の名前では新しく保存しない(ダウンロード / pull を直すなら edit(fml_edit)で写してから fml_update_fields)");
|
|
3489
|
+
assertChangeAllowed(ctx.root, target, "settings", ctx.mode);
|
|
3490
|
+
} catch (e) {
|
|
3491
|
+
return errorResult(ctx.root, path14.resolve(ctx.root, opt.path), e);
|
|
3492
|
+
}
|
|
3493
|
+
if (existsSync8(target)) return { status: "conflict", code: "ConflictError", path: rel(ctx.root, target), findings: [], message: "同じ名前のファイルがある(新しい設定の保存は上書きしない。直すなら fml_update_fields)" };
|
|
3494
|
+
let outcome;
|
|
3495
|
+
try {
|
|
3496
|
+
const envelope = parseInput(opt.content, "content");
|
|
3497
|
+
if (!isPlainObject2(envelope)) throw new InputError("content の最上位はオブジェクト(封筒形式の設定 JSON)");
|
|
3498
|
+
outcome = await normalizeText(ctx, target, opt.content, fieldsFileFor(ctx, target, opt.fields, envelope));
|
|
3499
|
+
} catch (e) {
|
|
3500
|
+
return errorResult(ctx.root, target, e);
|
|
3501
|
+
}
|
|
3502
|
+
if (outcome.normalize.hasErrors || !outcome.normalize.output) return { status: "invalid", code: "NormalizeError", path: rel(ctx.root, target), findings: outcome.normalize.findings, summary: outcome.normalize.summary, message: "normalize のエラーがあるので保存しない" };
|
|
3503
|
+
const text = JSON.stringify(outcome.normalize.output, null, 2) + "\n";
|
|
3504
|
+
return finish(
|
|
3505
|
+
ctx,
|
|
3506
|
+
target,
|
|
3507
|
+
outcome,
|
|
3508
|
+
() => commit(ctx, target, text, "new", () => {
|
|
3509
|
+
if (existsSync8(target)) throw new ConflictError("確定の直前に同じ名前のファイルができた");
|
|
3510
|
+
})
|
|
3511
|
+
);
|
|
3512
|
+
}
|
|
3513
|
+
function parseRows(text, current) {
|
|
3514
|
+
const v = parseInput(text, "rows");
|
|
3515
|
+
if (!Array.isArray(v)) throw new InputError("rows は行(fieldcode を持つオブジェクト)の配列");
|
|
3516
|
+
if (v.length === 0) throw new InputError("rows が空");
|
|
3517
|
+
if (v.length > MAX_UPDATE_ROWS) throw new InputError(`rows は 1 回に ${MAX_UPDATE_ROWS} 行まで(${v.length} 行)`);
|
|
3518
|
+
const known = new Set(ROW_SOURCE_KEYS);
|
|
3519
|
+
const codes = new Set(current.map((r) => isPlainObject2(r) ? String(r.fieldcode ?? "") : ""));
|
|
3520
|
+
const out = /* @__PURE__ */ new Map();
|
|
3521
|
+
v.forEach((row, i) => {
|
|
3522
|
+
if (!isPlainObject2(row)) throw new InputError(`rows[${i}] はオブジェクト`);
|
|
3523
|
+
const code = row.fieldcode;
|
|
3524
|
+
if (typeof code !== "string" || !code) throw new InputError(`rows[${i}] に fieldcode(文字列)が要る`);
|
|
3525
|
+
const unknown = Object.keys(row).filter((k) => !known.has(k));
|
|
3526
|
+
if (unknown.length) throw new InputError(`rows[${i}](${code})に行の元の値でないキーがある: ${unknown.join(", ")}(使えるのは ${ROW_SOURCE_KEYS.join(", ")}。画面の値(type、fieldlabel など)は normalize が項目定義から補う)`);
|
|
3527
|
+
if (out.has(code)) throw new InputError(`rows の fieldcode が重複: ${code}`);
|
|
3528
|
+
if (!codes.has(code)) throw new InputError(`設定に無い fieldcode: ${code}(項目はアプリの項目定義で決まる。fml_fields で項目定義を取り直し、fml_summary で設定の項目を確かめる)`);
|
|
3529
|
+
out.set(code, row);
|
|
3530
|
+
});
|
|
3531
|
+
return out;
|
|
3532
|
+
}
|
|
3533
|
+
function parseCommon(text) {
|
|
3534
|
+
const v = parseInput(text, "common");
|
|
3535
|
+
if (!isPlainObject2(v)) throw new InputError("common は最上位のキー(pluginMiscInfo、pluginListInfo、authority、pluginEnable など)を持つオブジェクト");
|
|
3536
|
+
const bad = Object.keys(v).filter((k) => k === "pluginInfos" || ENVELOPE_KEYS2.includes(k));
|
|
3537
|
+
if (bad.length) throw new InputError(`common で変えられないキー: ${bad.join(", ")}(項目の行は rows、封筒(appId など)は変えない)`);
|
|
3538
|
+
if (!Object.keys(v).length) throw new InputError("common が空(変えないなら渡さない)");
|
|
3539
|
+
return v;
|
|
3540
|
+
}
|
|
3541
|
+
async function updateFields(ctx, opt) {
|
|
3542
|
+
if (tooLarge(opt.rows)) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: `rows は ${MAX_SAVE_INPUT_BYTES.toLocaleString()} バイトまで` };
|
|
3543
|
+
if (opt.common !== void 0 && tooLarge(opt.common)) return { status: "invalid", code: "InputError", path: opt.path, findings: [], message: `common は ${MAX_SAVE_INPUT_BYTES.toLocaleString()} バイトまで` };
|
|
3544
|
+
let target;
|
|
3545
|
+
try {
|
|
3546
|
+
target = resolveRead(opt.path, ctx.root);
|
|
3547
|
+
resolveWrite(opt.path, WRITE_ROOTS.settings, ctx.root);
|
|
3548
|
+
if (SNAPSHOT_RE.test(path14.basename(target))) throw new SaveDenied("ダウンロード / pull のファイルは書き換えない。edit(fml_edit)で -edit.json に写して直す");
|
|
3549
|
+
assertUsableInKintone(ctx.root, target, ctx.mode);
|
|
3550
|
+
assertChangeAllowed(ctx.root, target, "settings", ctx.mode);
|
|
3551
|
+
} catch (e) {
|
|
3552
|
+
return errorResult(ctx.root, path14.resolve(ctx.root, opt.path), e);
|
|
3553
|
+
}
|
|
3554
|
+
if (!existsSync8(target)) return { status: "conflict", code: "ConflictError", path: rel(ctx.root, target), findings: [], message: "ファイルが無い(新しい設定なら fml_save_settings)" };
|
|
3555
|
+
const current = readFileSync10(target);
|
|
3556
|
+
if (digestOf(current) !== opt.expectedDigest) return { status: "conflict", code: "ConflictError", path: rel(ctx.root, target), findings: [], message: "読んだ後にファイルが変わった(digest が違う)。fml_summary で読み直す" };
|
|
3557
|
+
let settings;
|
|
3558
|
+
try {
|
|
3559
|
+
const v = parseInput(current.toString("utf8").replace(/^/, ""), "設定のファイル");
|
|
3560
|
+
if (!isPlainObject2(v)) throw new InputError("設定のファイルの最上位はオブジェクト");
|
|
3561
|
+
if (v.pluginID !== FORMULA_CONFIG_PLUGIN_ID) throw new InputError(`計算式プラグインの設定のファイルではない(封筒の pluginID が "${FORMULA_CONFIG_PLUGIN_ID}" でない)`);
|
|
3562
|
+
if (!Array.isArray(v.pluginInfos)) throw new InputError("設定のファイルに pluginInfos(項目の行)が無い");
|
|
3563
|
+
settings = v;
|
|
3564
|
+
} catch (e) {
|
|
3565
|
+
return errorResult(ctx.root, target, e);
|
|
3566
|
+
}
|
|
3567
|
+
const rows = settings.pluginInfos;
|
|
3568
|
+
let next;
|
|
3569
|
+
try {
|
|
3570
|
+
const replace = parseRows(opt.rows, rows);
|
|
3571
|
+
const common = opt.common !== void 0 ? parseCommon(opt.common) : {};
|
|
3572
|
+
const merged = rows.map((r) => {
|
|
3573
|
+
const code = isPlainObject2(r) ? String(r.fieldcode ?? "") : "";
|
|
3574
|
+
const rep = replace.get(code);
|
|
3575
|
+
return rep ? { ...r, ...rep } : r;
|
|
3576
|
+
});
|
|
3577
|
+
next = { ...settings, ...common, pluginInfos: merged };
|
|
3578
|
+
} catch (e) {
|
|
3579
|
+
return errorResult(ctx.root, target, e);
|
|
3580
|
+
}
|
|
3581
|
+
let outcome;
|
|
3582
|
+
try {
|
|
3583
|
+
outcome = await normalizeText(ctx, target, JSON.stringify(next), fieldsFileFor(ctx, target, opt.fields, settings));
|
|
3584
|
+
} catch (e) {
|
|
3585
|
+
return errorResult(ctx.root, target, e);
|
|
3586
|
+
}
|
|
3587
|
+
if (outcome.normalize.hasErrors || !outcome.normalize.output) return { status: "invalid", code: "NormalizeError", path: rel(ctx.root, target), findings: outcome.normalize.findings, summary: outcome.normalize.summary, message: "normalize のエラーがあるので保存しない(ファイルは変えていない)" };
|
|
3588
|
+
const output = outcome.normalize.output;
|
|
3589
|
+
const text = JSON.stringify(output, null, 2) + "\n";
|
|
3590
|
+
const name = rel(ctx.root, target);
|
|
3591
|
+
const d = diffSettings(settings, output, { before: `${name}(前)`, after: name });
|
|
3592
|
+
return finish(
|
|
3593
|
+
ctx,
|
|
3594
|
+
target,
|
|
3595
|
+
outcome,
|
|
3596
|
+
() => commit(ctx, target, text, "replace", () => {
|
|
3597
|
+
if (digestOf(readFileSync10(target)) !== opt.expectedDigest) throw new ConflictError("確定の直前にファイルが変わった(digest が違う)。fml_summary で読み直す");
|
|
3598
|
+
}),
|
|
3599
|
+
{ diff: { fields: d.fields, common: d.common } }
|
|
3600
|
+
);
|
|
3601
|
+
}
|
|
3602
|
+
|
|
2353
3603
|
// src/core.ts
|
|
2354
|
-
var CORE_API_VERSION =
|
|
3604
|
+
var CORE_API_VERSION = 2;
|
|
2355
3605
|
export {
|
|
2356
3606
|
ALLOWED_APIS,
|
|
2357
3607
|
ALLOWED_USER_APIS,
|
|
3608
|
+
APP_MARKER,
|
|
2358
3609
|
AuthError,
|
|
3610
|
+
CONNECTION_MAX_BYTES,
|
|
2359
3611
|
CORE_API_VERSION,
|
|
3612
|
+
ConnectionError,
|
|
2360
3613
|
DEFAULT_CONTEXT_BASE_URL,
|
|
2361
3614
|
DEFAULT_EVENT,
|
|
2362
3615
|
DEFAULT_MAX_CALLS,
|
|
2363
3616
|
DEFAULT_POLICY,
|
|
2364
3617
|
DEFAULT_TIMEOUT_SEC,
|
|
2365
3618
|
DEFAULT_USERS,
|
|
3619
|
+
EDIT_RE,
|
|
3620
|
+
ENVELOPE_KEYS2 as ENVELOPE_KEYS,
|
|
2366
3621
|
EVENT_RE,
|
|
2367
3622
|
EvalError,
|
|
2368
3623
|
EvalTimeoutError,
|
|
3624
|
+
FALLBACK_PROFILE,
|
|
3625
|
+
FORMULA_CONFIG_PLUGIN_ID,
|
|
2369
3626
|
FORMULA_PLUGIN_ID,
|
|
3627
|
+
FileExistsError,
|
|
2370
3628
|
Findings,
|
|
3629
|
+
INBOX,
|
|
2371
3630
|
InputError,
|
|
3631
|
+
KINTONE_ROOT,
|
|
2372
3632
|
KintoneUrlError,
|
|
3633
|
+
LEGACY_WORKSPACE_FILE,
|
|
2373
3634
|
LIST_LIMIT,
|
|
3635
|
+
LOCK_STALE_MS,
|
|
3636
|
+
LockBusyError,
|
|
2374
3637
|
MAX_INPUT_BYTES,
|
|
3638
|
+
MAX_SAVE_INPUT_BYTES,
|
|
2375
3639
|
MAX_TIMEOUT_SEC,
|
|
3640
|
+
MAX_UPDATE_ROWS,
|
|
2376
3641
|
MIN_PLUGIN_VERSION,
|
|
2377
3642
|
NotAllowedError,
|
|
3643
|
+
OUTER_KEYS,
|
|
2378
3644
|
PLUGIN_NAME,
|
|
2379
3645
|
POLICY_FILE,
|
|
3646
|
+
PROFILE_NAME_RE,
|
|
2380
3647
|
PathError,
|
|
3648
|
+
PermissionError,
|
|
2381
3649
|
PluginZipError,
|
|
2382
3650
|
PolicyError,
|
|
2383
3651
|
QUERY_MAX,
|
|
@@ -2385,17 +3653,36 @@ export {
|
|
|
2385
3653
|
RECEIVE_LIMITS,
|
|
2386
3654
|
RECORDS_MAX,
|
|
2387
3655
|
REQUIRED_API,
|
|
3656
|
+
ROW_SCREEN_KEYS,
|
|
3657
|
+
ROW_SOURCE_KEYS,
|
|
2388
3658
|
RestError,
|
|
2389
3659
|
SENTINEL_USER,
|
|
2390
3660
|
SHAPE_TEXT_MAX,
|
|
3661
|
+
SNAPSHOT_PREFIX,
|
|
3662
|
+
SNAPSHOT_RE,
|
|
2391
3663
|
SUPPORTED_API_VERSIONS,
|
|
3664
|
+
USER_INFO_FUNCTIONS,
|
|
2392
3665
|
UsersDataError,
|
|
2393
3666
|
UsersPullError,
|
|
2394
3667
|
WRITE_ROOTS,
|
|
3668
|
+
WorkspaceError,
|
|
3669
|
+
acquireLock,
|
|
3670
|
+
appDirFor,
|
|
3671
|
+
appFolderOfFile,
|
|
3672
|
+
appFoldersOf,
|
|
3673
|
+
assertAppMark,
|
|
3674
|
+
assertChangeAllowed,
|
|
3675
|
+
assertInsideWorkspace,
|
|
3676
|
+
assertSameConnection,
|
|
3677
|
+
assertUsableInKintone,
|
|
3678
|
+
authFor,
|
|
2395
3679
|
baseUrlFromEnv,
|
|
2396
3680
|
bodyOf,
|
|
2397
3681
|
buildUsersInfo,
|
|
2398
3682
|
calcSettings,
|
|
3683
|
+
classOfAppPath,
|
|
3684
|
+
cleanupAll,
|
|
3685
|
+
connectionStatusOf,
|
|
2399
3686
|
contextTargetsOf,
|
|
2400
3687
|
createContext,
|
|
2401
3688
|
createRestClient,
|
|
@@ -2404,57 +3691,96 @@ export {
|
|
|
2404
3691
|
describeAuth,
|
|
2405
3692
|
describeRecord,
|
|
2406
3693
|
diffSettings,
|
|
3694
|
+
digestOf,
|
|
3695
|
+
editNameOf,
|
|
3696
|
+
ensureAppFolder,
|
|
2407
3697
|
fetchFields,
|
|
2408
3698
|
fetchRecord,
|
|
3699
|
+
findAppDir,
|
|
2409
3700
|
findCycles,
|
|
3701
|
+
folderNameOf,
|
|
2410
3702
|
formatDate,
|
|
3703
|
+
hasLegacyConfig,
|
|
3704
|
+
identityOf,
|
|
2411
3705
|
isInside,
|
|
2412
3706
|
isKintoneBaseUrl,
|
|
2413
3707
|
isSupportedPluginVersion,
|
|
2414
3708
|
jsonDiff,
|
|
3709
|
+
jsonErrorText,
|
|
3710
|
+
jsonErrorWhere,
|
|
2415
3711
|
kintoneConfigPath,
|
|
3712
|
+
legacyBlockedError,
|
|
3713
|
+
listAppFolder,
|
|
2416
3714
|
listFields,
|
|
2417
3715
|
listQueryOf,
|
|
2418
3716
|
listRecordShapes,
|
|
2419
3717
|
loadAuth,
|
|
3718
|
+
loadConnections,
|
|
2420
3719
|
loadEngine,
|
|
2421
3720
|
loadPolicy,
|
|
3721
|
+
loadUserApiAuth,
|
|
3722
|
+
lockPathFor,
|
|
3723
|
+
markOf,
|
|
2422
3724
|
materialsOf,
|
|
3725
|
+
modeOf,
|
|
3726
|
+
momentFileOf,
|
|
2423
3727
|
narrowRecord,
|
|
2424
3728
|
normalizeKintoneBaseUrl,
|
|
2425
3729
|
normalizeSettings,
|
|
2426
3730
|
parsePolicy,
|
|
3731
|
+
pickProfile,
|
|
3732
|
+
placeNew,
|
|
2427
3733
|
pluginIdOf,
|
|
2428
3734
|
pluginZipPath,
|
|
3735
|
+
profileDirOf,
|
|
3736
|
+
pullReferencedUsers,
|
|
2429
3737
|
pullSettings,
|
|
2430
3738
|
pullUsers,
|
|
3739
|
+
readAppMark,
|
|
2431
3740
|
readFieldsFile,
|
|
2432
3741
|
readJsonLimited,
|
|
2433
3742
|
readPluginZip,
|
|
2434
3743
|
readTextLimited,
|
|
2435
3744
|
realResolve,
|
|
2436
3745
|
referenceTablesOf,
|
|
3746
|
+
referencedCodesOf,
|
|
2437
3747
|
relativeSettingsPath,
|
|
3748
|
+
requireProfiles,
|
|
2438
3749
|
resolvePluginSources,
|
|
2439
3750
|
resolveRead,
|
|
2440
3751
|
resolveWrite,
|
|
3752
|
+
resolveWriteDir,
|
|
2441
3753
|
rowsOf,
|
|
2442
3754
|
runEval,
|
|
2443
3755
|
runEvalJob,
|
|
3756
|
+
safeFileName,
|
|
3757
|
+
sameIdentity,
|
|
3758
|
+
saveNewSettings,
|
|
2444
3759
|
scanFormula,
|
|
2445
3760
|
scanFormulas,
|
|
3761
|
+
scanUserInfoCalls,
|
|
2446
3762
|
schemaRevisionOf,
|
|
3763
|
+
semanticDigest,
|
|
2447
3764
|
shapeLines,
|
|
3765
|
+
snapshotNameOf,
|
|
3766
|
+
snapshotOf,
|
|
2448
3767
|
stronglyConnected,
|
|
2449
3768
|
summarizeFields,
|
|
2450
3769
|
summarizeRecord,
|
|
2451
3770
|
summarizeSettings,
|
|
2452
3771
|
summarizeUsers,
|
|
3772
|
+
takeInbox,
|
|
3773
|
+
tempPathFor,
|
|
2453
3774
|
toolsMeta,
|
|
2454
3775
|
unquote,
|
|
3776
|
+
unusedProfileDirs,
|
|
3777
|
+
updateFields,
|
|
2455
3778
|
usedFieldCodes,
|
|
3779
|
+
userApiAuthFor,
|
|
2456
3780
|
usersFileNameOf,
|
|
2457
3781
|
validateEvent,
|
|
2458
3782
|
validateUsersSource,
|
|
2459
|
-
viewsOf
|
|
3783
|
+
viewsOf,
|
|
3784
|
+
withRetry,
|
|
3785
|
+
writeNewFile
|
|
2460
3786
|
};
|