@hitch42/applet 0.1.1-beta.1 → 0.1.1-beta.5
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/dist/applet.js +10 -7
- package/dist/{build-Rk3xDZV7.js → build-D1S5XCRV.js} +3 -4
- package/dist/build-pvl3vj91.js +2 -0
- package/dist/ci-D0nC_Ho7.js +52 -0
- package/dist/{deploy-D3BGDqgC.js → deploy-BDonkPqF.js} +72 -24
- package/dist/{generate-CH0VQmUi.js → generate-C6eAL5eo.js} +19 -3
- package/dist/generate-D5rSXXa-.js +2 -0
- package/dist/{init-BS1Y-eNL.js → init-VzV28EFT.js} +2 -2
- package/dist/{project-CxN-Dtc7.js → project-mW0vsN8A.js} +18 -8
- package/dist/vite.js +7 -4
- package/package.json +2 -1
- package/scaffold/.agents/skills/hitch-applets/SKILL.md +13 -10
- package/scaffold/.agents/skills/hitch-applets/references/performance.md +74 -0
- package/scaffold/.agents/skills/hitch-applets/references/records.md +6 -5
- package/scaffold/.agents/skills/hitch-cli/SKILL.md +127 -0
- package/scaffold/.agents/skills/hitch-data/SKILL.md +51 -0
- package/scaffold/.agents/skills/hitch-data/references/attributes.md +121 -0
- package/scaffold/.agents/skills/hitch-data/references/limits.md +64 -0
- package/scaffold/.github/workflows/hitch.yml +21 -0
- package/dist/build-CEku1lyb.js +0 -2
- package/dist/generate-6Nbt7uii.js +0 -2
package/dist/applet.js
CHANGED
|
@@ -35,6 +35,7 @@ Deploy options:
|
|
|
35
35
|
--changeset <id> Deploy against a pending schema changeset. The build
|
|
36
36
|
generates against its schema and the release promotes
|
|
37
37
|
when the changeset applies. Manifest mismatches warn.
|
|
38
|
+
Run applet deploy in the workspace root to deploy every applet.
|
|
38
39
|
|
|
39
40
|
Logs options:
|
|
40
41
|
--since <duration> Show logs from this period. Default: 1h. Maximum: 7d.
|
|
@@ -51,7 +52,9 @@ Variables options:
|
|
|
51
52
|
--secret Store the value as a secret.
|
|
52
53
|
-y, --yes Skip the live applet confirmation.
|
|
53
54
|
|
|
54
|
-
CI: set HITCH_API_KEY (and HITCH_API_URL for a non-default instance)
|
|
55
|
+
CI: set HITCH_API_KEY (and HITCH_API_URL for a non-default instance).
|
|
56
|
+
GitHub Actions: generate, build, and deploy use the job's OIDC token instead of
|
|
57
|
+
a key. Grant the job id-token: write.`;
|
|
55
58
|
async function main() {
|
|
56
59
|
const { positionals, values } = parseArgs({
|
|
57
60
|
allowPositionals: true,
|
|
@@ -82,23 +85,23 @@ async function main() {
|
|
|
82
85
|
const noLogFlags = values.run === void 0 && values.filter === void 0 && values.level === void 0 && values.limit === void 0 && values.live === void 0 && values.search === void 0 && values.since === void 0;
|
|
83
86
|
const noModeFlags = values["client-only"] === void 0 && values["server-only"] === void 0;
|
|
84
87
|
if (positionals.length === 1 && positionals[0] === "bootstrap" && values.changeset === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
|
|
85
|
-
await (await import("./init-
|
|
88
|
+
await (await import("./init-VzV28EFT.js")).runBootstrap({ install: values["no-install"] !== true });
|
|
86
89
|
return;
|
|
87
90
|
}
|
|
88
91
|
if (positionals.length === 1 && positionals[0] === "generate" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
|
|
89
|
-
await (await import("./generate-
|
|
92
|
+
await (await import("./generate-D5rSXXa-.js")).runGenerate({ changesetId: values.changeset });
|
|
90
93
|
return;
|
|
91
94
|
}
|
|
92
95
|
if (positionals.length === 1 && positionals[0] === "build" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
|
|
93
|
-
await (await import("./build-
|
|
96
|
+
await (await import("./build-pvl3vj91.js")).runBuild({ changesetId: values.changeset });
|
|
94
97
|
return;
|
|
95
98
|
}
|
|
96
99
|
if (positionals.length === 1 && positionals[0] === "deploy" && values["no-install"] === void 0 && noModeFlags && noGitFlags && noVariableFlags && noLogFlags) {
|
|
97
|
-
await (await import("./deploy-
|
|
100
|
+
await (await import("./deploy-BDonkPqF.js")).runDeploy({ changesetId: values.changeset });
|
|
98
101
|
return;
|
|
99
102
|
}
|
|
100
103
|
if (positionals.length === 2 && positionals[0] === "init" && values.changeset === void 0 && noModeFlags && noVariableFlags && noLogFlags) {
|
|
101
|
-
await (await import("./init-
|
|
104
|
+
await (await import("./init-VzV28EFT.js")).runInit(positionals[1], {
|
|
102
105
|
commit: values["no-commit"] !== true,
|
|
103
106
|
git: values["no-git"] !== true,
|
|
104
107
|
install: values["no-install"] !== true
|
|
@@ -106,7 +109,7 @@ async function main() {
|
|
|
106
109
|
return;
|
|
107
110
|
}
|
|
108
111
|
if (positionals.length === 2 && positionals[0] === "new" && values.changeset === void 0 && noGitFlags && noVariableFlags && noLogFlags) {
|
|
109
|
-
await (await import("./init-
|
|
112
|
+
await (await import("./init-VzV28EFT.js")).runNew({
|
|
110
113
|
code: positionals[1],
|
|
111
114
|
clientOnly: values["client-only"],
|
|
112
115
|
install: values["no-install"] !== true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as prepareAppletProject } from "./project-
|
|
1
|
+
import { r as prepareAppletProject } from "./project-mW0vsN8A.js";
|
|
2
2
|
import { rm } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
@@ -28,8 +28,7 @@ async function runPnpm(args, cwd) {
|
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
-
async function buildApplet(changesetId, outDir = ".hitch/dist") {
|
|
32
|
-
const appletRoot = process.cwd();
|
|
31
|
+
async function buildApplet(appletRoot, changesetId, outDir = ".hitch/dist") {
|
|
33
32
|
const { workspaceRoot } = await prepareAppletProject(appletRoot);
|
|
34
33
|
await rm(resolve(appletRoot, ".hitch"), {
|
|
35
34
|
force: true,
|
|
@@ -68,7 +67,7 @@ async function buildApplet(changesetId, outDir = ".hitch/dist") {
|
|
|
68
67
|
});
|
|
69
68
|
}
|
|
70
69
|
async function runBuild(options = {}) {
|
|
71
|
-
await buildApplet(options.changesetId);
|
|
70
|
+
await buildApplet(process.cwd(), options.changesetId);
|
|
72
71
|
}
|
|
73
72
|
//#endregion
|
|
74
73
|
export { runBuild as n, buildApplet as t };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { i as resolveInstanceUrl } from "./auth-DaVK0k5R.js";
|
|
2
|
+
//#region src/ci.ts
|
|
3
|
+
function githubActionsContext(env = process.env) {
|
|
4
|
+
if (env.GITHUB_ACTIONS !== "true" || env.HITCH_API_KEY) return void 0;
|
|
5
|
+
const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
6
|
+
const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
7
|
+
if (!requestUrl || !requestToken) throw new Error("GitHub Actions did not provide an OIDC token. Grant the job the id-token: write permission, or set HITCH_API_KEY.");
|
|
8
|
+
return {
|
|
9
|
+
requestUrl,
|
|
10
|
+
requestToken
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async function hitchResponseError(response, summary) {
|
|
14
|
+
let details = "";
|
|
15
|
+
try {
|
|
16
|
+
const body = await response.json();
|
|
17
|
+
if (body !== null && typeof body === "object") details = ["code" in body && typeof body.code === "string" ? body.code : void 0, "message" in body && typeof body.message === "string" ? body.message : void 0].filter((value) => value !== void 0).join(": ");
|
|
18
|
+
} catch {
|
|
19
|
+
details = "";
|
|
20
|
+
}
|
|
21
|
+
return /* @__PURE__ */ new Error(`${summary} (${response.status})${details ? `: ${details}` : ""}.`);
|
|
22
|
+
}
|
|
23
|
+
async function githubActionsToken(context, instanceUrl, fetcher) {
|
|
24
|
+
const url = new URL(context.requestUrl);
|
|
25
|
+
url.searchParams.set("audience", new URL(instanceUrl).origin);
|
|
26
|
+
const response = await fetcher(url, { headers: { Authorization: `bearer ${context.requestToken}` } });
|
|
27
|
+
if (!response.ok) throw new Error(`GitHub did not issue an OIDC token (${response.status}).`);
|
|
28
|
+
const body = await response.json();
|
|
29
|
+
if (body === null || typeof body !== "object" || !("value" in body)) throw new Error("GitHub returned an OIDC token response without a value.");
|
|
30
|
+
if (typeof body.value !== "string") throw new Error("GitHub returned an OIDC token response without a value.");
|
|
31
|
+
return body.value;
|
|
32
|
+
}
|
|
33
|
+
async function fetchWithGitHubActions(context, options, request) {
|
|
34
|
+
const instanceUrl = await resolveInstanceUrl(void 0, options.store);
|
|
35
|
+
return request(instanceUrl, await githubActionsToken(context, instanceUrl, options.fetch ?? globalThis.fetch));
|
|
36
|
+
}
|
|
37
|
+
async function loadGitHubActionsBuildInputs(context, options) {
|
|
38
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
39
|
+
const response = await fetchWithGitHubActions(context, options, (instanceUrl, accessToken) => fetcher(`${instanceUrl}/api/ci/v1/build-inputs`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
42
|
+
}));
|
|
43
|
+
if (!response.ok) throw await hitchResponseError(response, "Failed to load the CI build inputs");
|
|
44
|
+
const body = await response.json();
|
|
45
|
+
if (body === null || typeof body !== "object" || !("openapi" in body) || !("theme" in body) || typeof body.theme !== "string") throw new Error("The CI build inputs must contain an openapi document and a theme string.");
|
|
46
|
+
return {
|
|
47
|
+
openapi: body.openapi,
|
|
48
|
+
theme: body.theme
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { loadGitHubActionsBuildInputs as i, githubActionsContext as n, hitchResponseError as r, fetchWithGitHubActions as t };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { t as fetchWithOrgAuth } from "./auth-DaVK0k5R.js";
|
|
2
|
-
import { t as
|
|
2
|
+
import { n as githubActionsContext, r as hitchResponseError, t as fetchWithGitHubActions } from "./ci-D0nC_Ho7.js";
|
|
3
|
+
import { r as readWorkspaceConfig } from "./load-config-BnxocLAQ.js";
|
|
4
|
+
import { t as buildApplet } from "./build-D1S5XCRV.js";
|
|
3
5
|
import { createRequire } from "node:module";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
4
7
|
import { readFile, readdir } from "node:fs/promises";
|
|
5
8
|
import { relative, resolve, sep } from "node:path";
|
|
6
9
|
import { execFile } from "node:child_process";
|
|
@@ -2355,14 +2358,17 @@ async function createBundle(distDir) {
|
|
|
2355
2358
|
body.set(bundle);
|
|
2356
2359
|
return body;
|
|
2357
2360
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2361
|
+
function gitEnvironment() {
|
|
2362
|
+
return Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("GIT_")));
|
|
2363
|
+
}
|
|
2364
|
+
async function readGitInfo(appletRoot) {
|
|
2365
|
+
const env = gitEnvironment();
|
|
2360
2366
|
try {
|
|
2361
2367
|
const [{ stdout: status }, { stdout: sha }] = await Promise.all([execFileAsync("git", ["status", "--porcelain"], {
|
|
2362
|
-
cwd:
|
|
2368
|
+
cwd: appletRoot,
|
|
2363
2369
|
env
|
|
2364
2370
|
}), execFileAsync("git", ["rev-parse", "HEAD"], {
|
|
2365
|
-
cwd:
|
|
2371
|
+
cwd: appletRoot,
|
|
2366
2372
|
env
|
|
2367
2373
|
})]);
|
|
2368
2374
|
return {
|
|
@@ -2373,26 +2379,26 @@ async function readGitInfo() {
|
|
|
2373
2379
|
return { gitDirty: false };
|
|
2374
2380
|
}
|
|
2375
2381
|
}
|
|
2376
|
-
async function
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
return /* @__PURE__ */ new Error(`Deploy failed (${response.status})${details ? `: ${details}` : ""}.`);
|
|
2382
|
+
async function repositoryDirectory(appletRoot) {
|
|
2383
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
|
|
2384
|
+
cwd: appletRoot,
|
|
2385
|
+
env: gitEnvironment()
|
|
2386
|
+
});
|
|
2387
|
+
const directory = relative(stdout.trim(), appletRoot).split(sep).join("/") || ".";
|
|
2388
|
+
if (directory === ".." || directory.startsWith("../")) throw new Error(`${appletRoot} is outside its git repository ${stdout.trim()}.`);
|
|
2389
|
+
return directory;
|
|
2385
2390
|
}
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
+
function releaseForm(manifest, bundle) {
|
|
2392
|
+
const body = new FormData();
|
|
2393
|
+
body.set("manifest", manifest.raw);
|
|
2394
|
+
body.set("bundle", new File([bundle], "bundle.tar.gz", { type: "application/gzip" }));
|
|
2395
|
+
return body;
|
|
2396
|
+
}
|
|
2397
|
+
async function deployWithOrgAuth(appletRoot, distDir, manifest, options) {
|
|
2391
2398
|
const fetcher = options.fetch ?? globalThis.fetch;
|
|
2392
|
-
const
|
|
2393
|
-
|
|
2394
|
-
body
|
|
2395
|
-
body.set("bundle", new File([bundle], "bundle.tar.gz", { type: "application/gzip" }));
|
|
2399
|
+
const [bundle, git] = await Promise.all([createBundle(distDir), readGitInfo(appletRoot)]);
|
|
2400
|
+
return fetchWithOrgAuth(manifest.org, options, (instanceUrl, accessToken) => {
|
|
2401
|
+
const body = releaseForm(manifest, bundle);
|
|
2396
2402
|
body.set("gitDirty", String(git.gitDirty));
|
|
2397
2403
|
if (git.gitSha !== void 0) body.set("gitSha", git.gitSha);
|
|
2398
2404
|
if (options.changesetId !== void 0) body.set("changeset_id", options.changesetId);
|
|
@@ -2402,7 +2408,28 @@ async function runDeploy(options) {
|
|
|
2402
2408
|
body
|
|
2403
2409
|
});
|
|
2404
2410
|
});
|
|
2405
|
-
|
|
2411
|
+
}
|
|
2412
|
+
async function deployWithGitHubActions(context, appletRoot, distDir, manifest, options) {
|
|
2413
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
2414
|
+
const [bundle, sourceDirectory] = await Promise.all([createBundle(distDir), repositoryDirectory(appletRoot)]);
|
|
2415
|
+
return fetchWithGitHubActions(context, options, (instanceUrl, accessToken) => {
|
|
2416
|
+
const body = releaseForm(manifest, bundle);
|
|
2417
|
+
body.set("source_directory", sourceDirectory);
|
|
2418
|
+
return fetcher(`${instanceUrl}/api/ci/v1/applets/${manifest.code}/releases`, {
|
|
2419
|
+
method: "POST",
|
|
2420
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
2421
|
+
body
|
|
2422
|
+
});
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
async function deployApplet(appletRoot, options) {
|
|
2426
|
+
const github = githubActionsContext(options.store?.env ?? process.env);
|
|
2427
|
+
if (github !== void 0 && options.changesetId !== void 0) throw new Error("A changeset deploy is not available in GitHub Actions.");
|
|
2428
|
+
if (options.dist === void 0) await buildApplet(appletRoot, options.changesetId);
|
|
2429
|
+
const distDir = resolve(appletRoot, options.dist ?? ".hitch/dist");
|
|
2430
|
+
const manifest = await readManifest(distDir);
|
|
2431
|
+
const response = github === void 0 ? await deployWithOrgAuth(appletRoot, distDir, manifest, options) : await deployWithGitHubActions(github, appletRoot, distDir, manifest, options);
|
|
2432
|
+
if (!response.ok) throw await hitchResponseError(response, "Deploy failed");
|
|
2406
2433
|
const result = await response.json();
|
|
2407
2434
|
console.log(`Deployed release ${result.data.id} (${result.promoted ? "promoted" : "not promoted"}).`);
|
|
2408
2435
|
if (result.warnings?.length) {
|
|
@@ -2412,5 +2439,26 @@ async function runDeploy(options) {
|
|
|
2412
2439
|
if (result.unset_variables?.length) console.warn(`Warning: declared variables not set: ${result.unset_variables.join(", ")}. Set them with applet variables set.`);
|
|
2413
2440
|
return result;
|
|
2414
2441
|
}
|
|
2442
|
+
async function workspaceAppletRoots(workspaceRoot) {
|
|
2443
|
+
const appletsDir = resolve(workspaceRoot, "applets");
|
|
2444
|
+
const roots = (await readdir(appletsDir, { withFileTypes: true }).catch((error) => {
|
|
2445
|
+
if (isErrno(error, "ENOENT")) return [];
|
|
2446
|
+
throw error;
|
|
2447
|
+
})).filter((entry) => entry.isDirectory()).map((entry) => resolve(appletsDir, entry.name)).filter((root) => existsSync(resolve(root, "applet.manifest.ts"))).sort();
|
|
2448
|
+
if (roots.length === 0) throw new Error(`No applets were found under ${appletsDir}.`);
|
|
2449
|
+
return roots;
|
|
2450
|
+
}
|
|
2451
|
+
async function runDeploy(options) {
|
|
2452
|
+
const root = process.cwd();
|
|
2453
|
+
if (existsSync(resolve(root, "applet.manifest.ts"))) {
|
|
2454
|
+
await deployApplet(root, options);
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
if (readWorkspaceConfig(root) === void 0) throw new Error("Run applet deploy in an applet folder or in the workspace root.");
|
|
2458
|
+
for (const appletRoot of await workspaceAppletRoots(root)) {
|
|
2459
|
+
console.log(`Deploying ${relative(root, appletRoot)}...`);
|
|
2460
|
+
await deployApplet(appletRoot, options);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2415
2463
|
//#endregion
|
|
2416
2464
|
export { runDeploy };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { i as resolveInstanceUrl, n as organizationInstanceUrl, t as fetchWithOrgAuth } from "./auth-DaVK0k5R.js";
|
|
2
|
+
import { i as loadGitHubActionsBuildInputs, n as githubActionsContext } from "./ci-D0nC_Ho7.js";
|
|
2
3
|
import { t as loadAppletConfig } from "./load-config-BnxocLAQ.js";
|
|
3
|
-
import { r as prepareAppletProject } from "./project-
|
|
4
|
+
import { r as prepareAppletProject } from "./project-mW0vsN8A.js";
|
|
4
5
|
import { createRequire } from "node:module";
|
|
5
6
|
import { existsSync } from "node:fs";
|
|
6
7
|
import { access, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
@@ -352,6 +353,8 @@ async function responseJson(response) {
|
|
|
352
353
|
}
|
|
353
354
|
}
|
|
354
355
|
async function loadAuthenticatedSpec(orgCode, options) {
|
|
356
|
+
const github = githubActionsContext(options.store?.env ?? process.env);
|
|
357
|
+
if (github !== void 0) return (await loadGitHubActionsBuildInputs(github, options)).openapi;
|
|
355
358
|
const fetcher = options.fetch ?? globalThis.fetch;
|
|
356
359
|
const response = await fetchWithOrgAuth(orgCode, options, (instanceUrl, accessToken) => fetcher(`${instanceUrl}/api/v1/openapi.json${options.changesetId === void 0 ? "" : `?changeset_id=${encodeURIComponent(options.changesetId)}`}`, { headers: { Authorization: `Bearer ${accessToken}` } }));
|
|
357
360
|
if (!response.ok) throw new Error(`Failed to load the organization's OpenAPI spec (${response.status}).`);
|
|
@@ -364,6 +367,20 @@ async function loadThemeStylesheet(orgCode, options) {
|
|
|
364
367
|
if (!response.ok) throw new Error(`Failed to load the organization's theme (${response.status}).`);
|
|
365
368
|
return response.text();
|
|
366
369
|
}
|
|
370
|
+
async function loadBuildInputs(orgCode, options) {
|
|
371
|
+
const github = githubActionsContext(options.store?.env ?? process.env);
|
|
372
|
+
if (github !== void 0) {
|
|
373
|
+
const inputs = await loadGitHubActionsBuildInputs(github, options);
|
|
374
|
+
return {
|
|
375
|
+
spec: inputs.openapi,
|
|
376
|
+
theme: inputs.theme
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
spec: await loadAuthenticatedSpec(orgCode, options),
|
|
381
|
+
theme: await loadThemeStylesheet(orgCode, options)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
367
384
|
async function generateContract(spec, outDir) {
|
|
368
385
|
await mkdir(outDir, { recursive: true });
|
|
369
386
|
const paths = spec !== null && typeof spec === "object" && "paths" in spec ? spec.paths : void 0;
|
|
@@ -432,8 +449,7 @@ async function runGenerate(options) {
|
|
|
432
449
|
const root = process.cwd();
|
|
433
450
|
const outDir = resolve("./.hitch");
|
|
434
451
|
const manifest = await loadAppletConfig(process.cwd());
|
|
435
|
-
const spec = await
|
|
436
|
-
const themeStylesheet = await loadThemeStylesheet(manifest.org, options);
|
|
452
|
+
const { spec, theme: themeStylesheet } = await loadBuildInputs(manifest.org, options);
|
|
437
453
|
const stagedDir = await mkdtemp(resolve("./.hitch.tmp-"));
|
|
438
454
|
const backupDir = `${stagedDir}.backup`;
|
|
439
455
|
let staged = true;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as ORG_CODE_PATTERN, i as APPLET_CODE_PATTERN } from "./manifest-DyoJ1MIL.js";
|
|
2
2
|
import { i as resolveInstanceUrl, r as resolveAuthForOrg } from "./auth-DaVK0k5R.js";
|
|
3
|
-
import { n as runGenerate } from "./generate-
|
|
3
|
+
import { n as runGenerate } from "./generate-C6eAL5eo.js";
|
|
4
4
|
import { n as loadAppletSourceDefinition, r as readWorkspaceConfig } from "./load-config-BnxocLAQ.js";
|
|
5
|
-
import { a as writeWorkspaceSupport, i as writeAppletSupport, n as loadCliManifest, t as assertWorkspaceCli } from "./project-
|
|
5
|
+
import { a as writeWorkspaceSupport, i as writeAppletSupport, n as loadCliManifest, t as assertWorkspaceCli } from "./project-mW0vsN8A.js";
|
|
6
6
|
import { access, cp, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
7
7
|
import { relative, resolve } from "node:path";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
@@ -122,13 +122,18 @@ allowBuilds:
|
|
|
122
122
|
workerd: true
|
|
123
123
|
minimumReleaseAge: 4320
|
|
124
124
|
minimumReleaseAgeExclude:
|
|
125
|
-
- "
|
|
125
|
+
- "@hitch42/*"
|
|
126
126
|
`;
|
|
127
127
|
}
|
|
128
128
|
const GITIGNORE_START = "# BEGIN Hitch generated files";
|
|
129
129
|
const GITIGNORE_END = "# END Hitch generated files";
|
|
130
|
+
const SKILLS = [
|
|
131
|
+
"hitch-applets",
|
|
132
|
+
"hitch-cli",
|
|
133
|
+
"hitch-data"
|
|
134
|
+
];
|
|
130
135
|
const GITIGNORE = `${GITIGNORE_START}
|
|
131
|
-
|
|
136
|
+
${SKILLS.map((skill) => `/.agents/skills/${skill}/`).join("\n")}
|
|
132
137
|
/.node-version
|
|
133
138
|
/AGENTS.md
|
|
134
139
|
/CLAUDE.md
|
|
@@ -169,16 +174,21 @@ async function writeWorkspaceSupport(root, config, cli) {
|
|
|
169
174
|
const resolvedCli = cli ?? await loadCliManifest();
|
|
170
175
|
if (resolvedCli === void 0) throw new Error("The CLI package manifest is not published.");
|
|
171
176
|
assertWorkspaceCli(config, resolvedCli);
|
|
172
|
-
const
|
|
177
|
+
const deployWorkflowTarget = resolve(root, ".github/workflows/hitch.yml");
|
|
173
178
|
const agents = await readFile(new URL("../scaffold/AGENTS.md", import.meta.url), "utf8");
|
|
174
|
-
await Promise.all([mkdir(resolve(root, ".agents/skills"), { recursive: true }), rm(skillTarget, {
|
|
175
|
-
force: true,
|
|
176
|
-
recursive: true
|
|
177
|
-
})]);
|
|
178
179
|
await Promise.all([
|
|
179
|
-
|
|
180
|
+
mkdir(resolve(root, ".agents/skills"), { recursive: true }),
|
|
181
|
+
mkdir(resolve(root, ".github/workflows"), { recursive: true }),
|
|
182
|
+
...SKILLS.map((skill) => rm(resolve(root, ".agents/skills", skill), {
|
|
183
|
+
force: true,
|
|
184
|
+
recursive: true
|
|
185
|
+
}))
|
|
186
|
+
]);
|
|
187
|
+
await Promise.all([
|
|
188
|
+
...SKILLS.map((skill) => cp(new URL(`../scaffold/.agents/skills/${skill}`, import.meta.url), resolve(root, ".agents/skills", skill), { recursive: true })),
|
|
180
189
|
writeFile(resolve(root, "package.json"), `${JSON.stringify(workspacePackage(config.org), null, 2)}\n`),
|
|
181
190
|
writeFile(resolve(root, "pnpm-workspace.yaml"), workspaceFile(resolvedCli)),
|
|
191
|
+
existsSync(deployWorkflowTarget) ? void 0 : cp(new URL("../scaffold/.github/workflows/hitch.yml", import.meta.url), deployWorkflowTarget),
|
|
182
192
|
writeFile(resolve(root, ".node-version"), "24\n"),
|
|
183
193
|
writeGitignore(root),
|
|
184
194
|
writeFile(resolve(root, "AGENTS.md"), agents)
|
package/dist/vite.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as APPLET_CODE_PATTERN, n as appletManifestSchema, r as appletSourceDefinitionSchema } from "./manifest-DyoJ1MIL.js";
|
|
2
2
|
import { i as resolveInstanceUrl, n as organizationInstanceUrl } from "./auth-DaVK0k5R.js";
|
|
3
|
-
import { c as writeClientEntry, i as writeRegisterDeclaration, l as writeManifestGen, r as syncContract, s as writeAppletRoot, u as writeServerEntry } from "./generate-
|
|
3
|
+
import { c as writeClientEntry, i as writeRegisterDeclaration, l as writeManifestGen, r as syncContract, s as writeAppletRoot, u as writeServerEntry } from "./generate-C6eAL5eo.js";
|
|
4
4
|
import { i as resolveWorkspaceOrg } from "./load-config-BnxocLAQ.js";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { appendFile, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -239,7 +239,10 @@ function applet(sourceDefinition, options = {}) {
|
|
|
239
239
|
hasServer ? writeServerEntry(root, hasHitchGen, manifest.code) : void 0,
|
|
240
240
|
hasHitchGen ? void 0 : writeRegisterDeclaration(resolve(root, ".hitch"))
|
|
241
241
|
]);
|
|
242
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
base: `/${manifest.code}/`,
|
|
244
|
+
...hasServer && { environments: { worker: { build: { rollupOptions: { output: { codeSplitting: false } } } } } }
|
|
245
|
+
};
|
|
243
246
|
},
|
|
244
247
|
configResolved(config) {
|
|
245
248
|
apiManifest = resolveAppletConfig(config.root).manifest;
|
|
@@ -313,7 +316,7 @@ function applet(sourceDefinition, options = {}) {
|
|
|
313
316
|
const workerDir = outputOptions.dir ?? resolve(buildRootOutDir, "worker");
|
|
314
317
|
const mainModule = Object.values(bundle).find((output) => output.type === "chunk" && output.isEntry)?.fileName;
|
|
315
318
|
const strayModules = (await readdir(workerDir, { recursive: true })).filter((path) => /\.(js|mjs)$/.test(path) && path !== mainModule);
|
|
316
|
-
if (strayModules.length > 0) throw new Error(`The applet server must bundle to a single worker/index.js module; found: ${strayModules.join(", ")}
|
|
319
|
+
if (strayModules.length > 0) throw new Error(`The applet server must bundle to a single worker/index.js module; found: ${strayModules.join(", ")}.`);
|
|
317
320
|
console.log(`server → ${relative(requireAppletConfig().root, resolve(workerDir, mainModule ?? ""))}`);
|
|
318
321
|
}
|
|
319
322
|
}
|
|
@@ -350,7 +353,7 @@ function applet(sourceDefinition, options = {}) {
|
|
|
350
353
|
routesDirectory: "src/client/routes",
|
|
351
354
|
routeFileIgnorePattern: "__root",
|
|
352
355
|
virtualRouteConfig
|
|
353
|
-
}), react()] : [],
|
|
356
|
+
}), react({ compiler: true })] : [],
|
|
354
357
|
cloudflarePlugins
|
|
355
358
|
];
|
|
356
359
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hitch42/applet",
|
|
3
|
-
"version": "0.1.1-beta.
|
|
3
|
+
"version": "0.1.1-beta.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"applet": "./dist/applet.js"
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"clsx": "^2.1.1",
|
|
67
67
|
"croner": "^10.0.1",
|
|
68
68
|
"lucide-react": "^1.33.0",
|
|
69
|
+
"oxc-transform-react": "^0.149.0",
|
|
69
70
|
"react-aria-components": "^1.20.0",
|
|
70
71
|
"shadcn": "^4.16.1",
|
|
71
72
|
"streamdown": "^2.6.0",
|
|
@@ -13,16 +13,19 @@ Treat each applet as an isolated microfrontend. Keep it separate from the data s
|
|
|
13
13
|
|
|
14
14
|
This file holds the common rules. Read each matching reference before you edit an applet. Paths are relative to this file.
|
|
15
15
|
|
|
16
|
-
| Read this
|
|
17
|
-
|
|
|
18
|
-
| `references/client.md`
|
|
19
|
-
| `references/records.md`
|
|
20
|
-
| `references/files.md`
|
|
21
|
-
| `references/markdown.md`
|
|
22
|
-
| `references/server.md`
|
|
23
|
-
| `references/schedules.md`
|
|
24
|
-
| `references/workflows.md`
|
|
25
|
-
| `references/manifest.md`
|
|
16
|
+
| Read this | When the task involves |
|
|
17
|
+
| --------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
18
|
+
| `references/client.md` | Client routes, navigation, page layout, forms, controls, or other user interface work |
|
|
19
|
+
| `references/records.md` | Listing, querying, creating, or updating records; filters, includes, aggregates; a missing record method |
|
|
20
|
+
| `references/files.md` | Uploading or reading files, attachment attributes, download links |
|
|
21
|
+
| `references/markdown.md` | Rendering or editing markdown, image uploads, Hitch file links |
|
|
22
|
+
| `references/server.md` | The applet server: secrets, env variables, external services, the `api` client |
|
|
23
|
+
| `references/schedules.md` | Cron schedules and background runs |
|
|
24
|
+
| `references/workflows.md` | Multi-step server work: `createWorkflow`, `step.do`, `step.sleep`, starting, reading, and listing runs |
|
|
25
|
+
| `references/manifest.md` | The applet definition in `applet.manifest.ts`: objects, access, variables, and packages |
|
|
26
|
+
| `references/performance.md` | Data access design: client versus server, N+1 reads, includes, aggregates, pagination, batch writes |
|
|
27
|
+
|
|
28
|
+
For attribute types, filters, and API limits, read `../hitch-data/SKILL.md`. To inspect or change Hitch data and settings from the shell, read `../hitch-cli/SKILL.md`.
|
|
26
29
|
|
|
27
30
|
## Work with the workspace
|
|
28
31
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Performance
|
|
2
|
+
|
|
3
|
+
Design the data access of a screen or a job before you write it. Decide the number of requests, the page size, and the largest dataset the code can touch.
|
|
4
|
+
|
|
5
|
+
## Prefer the client SDK
|
|
6
|
+
|
|
7
|
+
- Read and write from the browser with the client SDK for normal user work. It uses the current user's access and needs no server.
|
|
8
|
+
- Add a server procedure only when the work needs a secret, an external service, a schedule, a workflow, or fan-in of many reads into one response.
|
|
9
|
+
- Do not proxy a single Hitch call through the server. The proxy adds a hop and adds nothing.
|
|
10
|
+
|
|
11
|
+
## Consolidate on the server when needed
|
|
12
|
+
|
|
13
|
+
- When a report or dashboard needs many Hitch reads, make one applet server request, not hundreds of browser requests. A server call started by a user request keeps that user's identity.
|
|
14
|
+
- On the server, collect the pages, join the sets, and return one shaped result.
|
|
15
|
+
- Cache a server result in TanStack Query with a JSON-serializable value only. Do not put `Set`, `Map`, `Date`, or class instances in the cache.
|
|
16
|
+
- Version the query key when the result shape changes.
|
|
17
|
+
|
|
18
|
+
## Do not write N+1 reads
|
|
19
|
+
|
|
20
|
+
Never call `get` once per row of a list. Use one of these instead:
|
|
21
|
+
|
|
22
|
+
- **Include.** Pass `include` on the query to hydrate owner reference and user attributes in the same page. Include a reference only when each record links to a small number of targets. Include has no cap on the number of linked records it returns.
|
|
23
|
+
- **One page plus one related query.** Collect the IDs from the page, then run one `query` on the related object with `$in` on its reference attribute, for example `{ company_id: { $in: companyIds } }`. Hitch has no filter on record `id`. The related query is a page too, so pass its cursor when it can exceed 200 records.
|
|
24
|
+
- **Aggregate.** Use `aggregate` for a count, sum, average, minimum, or maximum. Group by up to two dimensions. Give each aggregate its own `filter` to get several metrics in one call. Never load records to count them in JavaScript.
|
|
25
|
+
- **Promise.all.** Run a known, small, independent set of requests concurrently. Do not run one request per row this way.
|
|
26
|
+
|
|
27
|
+
## Filter in Hitch, not in the browser
|
|
28
|
+
|
|
29
|
+
- Put every filter, sort, and search term in the query body.
|
|
30
|
+
- A search box that filters the loaded page hides every record on later pages. That is not search.
|
|
31
|
+
- Ordered, text-match, and sort operations need the `indexed` flag on the attribute. Read `../../hitch-data/references/limits.md`.
|
|
32
|
+
|
|
33
|
+
## Page every list
|
|
34
|
+
|
|
35
|
+
- Every list request returns at most 200 records. A larger `limit` is clamped to 200 with no error.
|
|
36
|
+
- Show a page size or a bounded scroll area in the product, with a next-page control that passes the returned `cursor`.
|
|
37
|
+
- Loop the cursor only when the code must visit the whole dataset. Name the bound in a comment, and stop at it.
|
|
38
|
+
- Do not write a whole-dataset loop for a number that `aggregate` can return.
|
|
39
|
+
|
|
40
|
+
## Batch writes
|
|
41
|
+
|
|
42
|
+
Send many writes as one batch. A batch runs in order, in one transaction, and holds at most 100 entries.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { hitch } from "@hitch42/applet/server";
|
|
46
|
+
|
|
47
|
+
const companyId = crypto.randomUUID();
|
|
48
|
+
|
|
49
|
+
await hitch.batch(
|
|
50
|
+
(tx) => {
|
|
51
|
+
tx.companies.create({ body: { id: companyId, name: "Acme" } });
|
|
52
|
+
tx.deals.create({ body: { name: "Acme renewal", company_id: companyId } });
|
|
53
|
+
tx.deals.update({ params: { id: dealId }, body: { stage: "won" } });
|
|
54
|
+
tx.deals.delete({ params: { id: staleDealId } });
|
|
55
|
+
tx.deals.updateWhere({ body: { filter: { stage: "open" }, set: { stage: "lost" } } });
|
|
56
|
+
},
|
|
57
|
+
{ idempotencyKey: `import-${runId}-${chunkIndex}` },
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- Call `hitch.batch(...)` on the server. Call `hitch.batch.call(...)` on the client.
|
|
62
|
+
- The `tx` calls buffer. Nothing sends until the callback resolves.
|
|
63
|
+
- A failed entry rolls back the whole batch. The error names the entry index and the entry error.
|
|
64
|
+
- A later entry can reference a record that an earlier entry created. Supply the record `id` as a UUID on create.
|
|
65
|
+
- Pass `dryRun: true` to validate a batch with no write. The response carries `dry_run: true`.
|
|
66
|
+
- Pass `idempotencyKey` on every batch that a retry can send twice. After a successful batch, the same key replays the stored response with `replayed: true`. The same key with a different body fails with HTTP 422. A failed batch or a dry run does not consume the key.
|
|
67
|
+
- Chunk a large import into batches of 100. Give each chunk its own idempotency key.
|
|
68
|
+
- Single `create`, `update`, and `delete` calls have no idempotency key. Use a batch when a retry must be safe.
|
|
69
|
+
|
|
70
|
+
## Bound background work
|
|
71
|
+
|
|
72
|
+
- Process an import, sync, or backfill in batches. Save a checkpoint after each durable batch.
|
|
73
|
+
- Resume from the last checkpoint. Do not restart from the beginning.
|
|
74
|
+
- Make a skipped or failed item visible in the run summary. Never convert a failure into an empty result.
|
|
@@ -22,7 +22,7 @@ Use the generated Hitch contract as the typed data plane.
|
|
|
22
22
|
- `updateWhere` sends `POST /v1/<objectCode>/update` and updates matching records.
|
|
23
23
|
- `aggregate` sends `POST /v1/<objectCode>/aggregate` and calculates aggregate values.
|
|
24
24
|
|
|
25
|
-
- Pass
|
|
25
|
+
- Pass a record ID in `params.id` for `get`, `update`, and `delete`.
|
|
26
26
|
- Pass record fields in `body` for `create` and `update`.
|
|
27
27
|
- Pass structured query data in `body` for `query`.
|
|
28
28
|
|
|
@@ -86,7 +86,7 @@ const page = await hitch.records.deals.query({
|
|
|
86
86
|
sort: { attribute: "amount", direction: "desc" },
|
|
87
87
|
limit: 50,
|
|
88
88
|
cursor,
|
|
89
|
-
include:
|
|
89
|
+
include: "company",
|
|
90
90
|
},
|
|
91
91
|
});
|
|
92
92
|
```
|
|
@@ -100,7 +100,8 @@ const page = await hitch.records.deals.query({
|
|
|
100
100
|
- Use only `$not_empty` for attachment fields.
|
|
101
101
|
- Use `$eq`, `$in`, `$not_empty`, `$contains`, `$starts_with`, or `$ends_with` for text fields.
|
|
102
102
|
- Use `$eq`, `$in`, `$not_empty`, `$lt`, `$lte`, `$gt`, or `$gte` for ordered fields.
|
|
103
|
-
-
|
|
103
|
+
- Use `$contains`, `$starts_with`, `$ends_with`, `$lt`, `$lte`, `$gt`, `$gte`, and `sort` only on an attribute with the `indexed` flag. Read `../../hitch-data/references/limits.md`.
|
|
104
|
+
- Pass `true` to `$not_empty` for set values, or `false` for empty values.
|
|
104
105
|
- Pass an array to `$in`.
|
|
105
106
|
- Pass a string to `$contains`, `$starts_with`, and `$ends_with`.
|
|
106
107
|
- Sort with `{ attribute, direction }`.
|
|
@@ -110,12 +111,12 @@ const page = await hitch.records.deals.query({
|
|
|
110
111
|
- Use a positive integer for `limit`.
|
|
111
112
|
- The service caps `limit` at 200 and uses 50 by default.
|
|
112
113
|
- Pass the returned pagination cursor back as `cursor`.
|
|
113
|
-
- Pass one
|
|
114
|
+
- Pass `include` as one string. Use the include name, which is the code without its `_id` or `_ids` suffix. Separate several names with commas.
|
|
114
115
|
|
|
115
116
|
## Aggregate records
|
|
116
117
|
|
|
117
118
|
Pass `filter`, `aggregates`, and `groupBy` in the aggregate body.
|
|
118
|
-
An aggregate or group attribute can name one reference
|
|
119
|
+
An aggregate or group attribute can name one single-value owner reference and a target attribute, for example `company_id.industry`.
|
|
119
120
|
Each aggregate can have its own `filter` with the same syntax as the top-level `filter`, including nested reference filters.
|
|
120
121
|
`groupBy` accepts an array of up to two dimensions.
|
|
121
122
|
The array form returns `keys` and `labels` arrays per group.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hitch-cli
|
|
3
|
+
description: Use the `hitch` command line to sign in, select an organization, and read or write Hitch data and administration from the shell. Use when a task needs to inspect records, try a filter or aggregate, create test data, manage teams, grants, or capabilities, apply a changeset, or check applet access without writing applet code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hitch CLI
|
|
7
|
+
|
|
8
|
+
The `hitch` command talks to one organization on one Hitch instance. Its data and admin commands are generated from that organization's API, so the command set matches the organization's objects.
|
|
9
|
+
|
|
10
|
+
`hitch` is separate from `applet`. Use `applet` to create, generate, build, and deploy applets. Use `hitch` to work with the organization's data and settings. Read the `hitch-applets` skill for `applet`.
|
|
11
|
+
|
|
12
|
+
## Sign in
|
|
13
|
+
|
|
14
|
+
- Run `hitch login` to sign in with the device flow. The command prints a URL and a code. Open the URL and confirm the code. Add `--no-browser` when the shell cannot open a browser.
|
|
15
|
+
- Run `hitch login --api-key` to store an API key instead. The command prompts for the key. Pass `--api-key <key>` to skip the prompt.
|
|
16
|
+
- Set `HITCH_API_KEY` to use a key for one shell session. It overrides every stored login. While it is set, `hitch login` and `hitch logout` refuse to run.
|
|
17
|
+
- Run `hitch whoami` to print the current person, email, organization name, and organization id as JSON.
|
|
18
|
+
- Run `hitch logout` to remove every stored login and cached command set.
|
|
19
|
+
- Set `HITCH_API_URL` or pass `--instance <url>` to `hitch login` for an instance other than the default.
|
|
20
|
+
|
|
21
|
+
## Select the organization
|
|
22
|
+
|
|
23
|
+
The CLI resolves the organization in this order. The first match wins.
|
|
24
|
+
|
|
25
|
+
1. `--org <code>` on any command.
|
|
26
|
+
2. The workspace organization: the `hitch.org` field in the nearest `package.json`, or in the workspace root when the current directory is an applet under `applets/`.
|
|
27
|
+
3. `HITCH_DEFAULT_ORG`.
|
|
28
|
+
|
|
29
|
+
There is no remembered active organization. When `--org` and the workspace disagree, the command fails with `--org "x" does not match workspace organization "y"`. When several organizations are stored and none is selected, the command fails and names the three ways to choose.
|
|
30
|
+
|
|
31
|
+
## Refresh the commands
|
|
32
|
+
|
|
33
|
+
- Run `hitch sync` after a sign-in, after a schema change, and whenever `hitch --help` lacks an object. The command downloads the organization's API and caches the generated commands. It prints `Synced 3 objects.`
|
|
34
|
+
- If the CLI prints `the cached commands ... no longer load — run hitch sync`, run `hitch sync`. Until then only the built-in commands work.
|
|
35
|
+
- The `admin` commands appear only when the signed-in identity holds an admin capability. A missing `admin` command means no capability, not a broken CLI.
|
|
36
|
+
|
|
37
|
+
## Read the command names
|
|
38
|
+
|
|
39
|
+
Every generated command follows the API route.
|
|
40
|
+
|
|
41
|
+
- Data commands: `hitch <object> <verb>`. The object is its code, for example `deals`. The verbs are `list`, `get`, `create`, `update`, `delete`, `query`, `update-where`, and `aggregate`. `hitch batch` sends a batch.
|
|
42
|
+
- Admin commands: `hitch admin <route segments> <verb>`. The verb comes from the HTTP method: `list` or `get` for GET, `create` for POST, `update` for PATCH, `set` for PUT, `delete` for DELETE. For example `PUT /admin/v1/teams/{id}/members/{userId}` is `hitch admin teams members set`.
|
|
43
|
+
- Every path, query, and body field becomes a flag in kebab case: `--id`, `--team-id`, `--can-read`. An object or array field takes JSON: `--filter '{"stage":"won"}'`.
|
|
44
|
+
- A required field is a required flag. The CLI does not prompt for a missing value. It prints the usage error.
|
|
45
|
+
- Run `hitch <object> <verb> --help` to see the flags for that organization's schema.
|
|
46
|
+
- Every `delete` command asks for confirmation. Pass `--yes` to skip it. A script without a terminal must pass `--yes`.
|
|
47
|
+
- A command prints its result as JSON. A list prints one item per line. A command with no result prints nothing.
|
|
48
|
+
- An error prints `hitch: <message>` and exits with code 1.
|
|
49
|
+
|
|
50
|
+
## Work with records
|
|
51
|
+
|
|
52
|
+
Examples use an object called `deals` with attributes `amount`, `stage`, and `owner_id`. Ids are UUIDs.
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
hitch deals list --limit 20 --include owner
|
|
56
|
+
hitch deals get --id 0192f1a2-3b4c-7d5e-8f60-123456789abc --include owner
|
|
57
|
+
hitch deals create --name "Acme renewal" --amount 5000 --stage open
|
|
58
|
+
hitch deals update --id 0192f1a2-3b4c-7d5e-8f60-123456789abc --stage won
|
|
59
|
+
hitch deals delete --id 0192f1a2-3b4c-7d5e-8f60-123456789abc --yes
|
|
60
|
+
hitch deals query --filter '{"stage":{"$in":["open","won"]}}' --sort '{"attribute":"createdAt","direction":"desc"}' --limit 50
|
|
61
|
+
hitch deals update-where --filter '{"stage":"open"}' --set '{"stage":"lost"}' --expect 3
|
|
62
|
+
hitch deals aggregate --aggregates '[{"op":"sum","attribute":"amount","as":"total"}]' --group-by '[{"attribute":"stage"}]'
|
|
63
|
+
hitch batch --requests '[{"method":"POST","path":"/v1/deals","body":{"name":"A","stage":"open"}}]'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- The filter, sort, include, aggregate, and batch bodies use the same shapes as the SDK. Read the `hitch-data` skill for the rules and limits.
|
|
67
|
+
- A list returns one page of at most 200 records and a cursor. Pass `--cursor <value>` for the next page.
|
|
68
|
+
- Data commands act as the signed-in person with that person's grants. An API key acts as the member who made it.
|
|
69
|
+
|
|
70
|
+
## Administer the organization
|
|
71
|
+
|
|
72
|
+
Admin commands need the matching capability. Examples use fake ids.
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
hitch admin teams list
|
|
76
|
+
hitch admin teams create --name Sales
|
|
77
|
+
hitch admin teams members set --id <teamId> --user-id <userId>
|
|
78
|
+
hitch admin capabilities list
|
|
79
|
+
hitch admin capabilities grants set --capability objects:manage --team-id <teamId>
|
|
80
|
+
hitch admin objects list
|
|
81
|
+
hitch admin objects access list --object-id <objectId>
|
|
82
|
+
hitch admin teams object-grants set --team-id <teamId> --object-id <objectId> --can-create --can-read --can-update --no-can-delete
|
|
83
|
+
hitch admin teams object-grants conditions create --team-id <teamId> --object-id <objectId> --operator equals --operand current_user --path <attributeId>
|
|
84
|
+
hitch admin objects default-grant set --object-id <objectId> --can-read --no-can-create --no-can-update --no-can-delete
|
|
85
|
+
hitch admin applets access list --code sales_portal
|
|
86
|
+
hitch admin applets grants set --code sales_portal --team-id <teamId>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- Object grants for a team live under `admin teams object-grants`. The default grant that every member holds lives under `admin objects default-grant`. Each has `conditions` and `attributes` subcommands.
|
|
90
|
+
- Add `--delegate-attribute-id <attributeId>` to an object grant to delegate through a reference attribute.
|
|
91
|
+
- `admin objects access list` shows which grantees cannot read an object. `admin applets access list` shows which grantees cannot use what an applet reads.
|
|
92
|
+
|
|
93
|
+
## Change the schema
|
|
94
|
+
|
|
95
|
+
Schema changes go through a changeset. The CLI names the lifecycle as state, not as actions.
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
hitch admin changesets create
|
|
99
|
+
hitch admin objects create --name Deal --changeset-id <changesetId>
|
|
100
|
+
hitch admin objects attributes create --code deals --name Amount --type currency --changeset-id <changesetId>
|
|
101
|
+
hitch admin changesets blockers list --id <changesetId>
|
|
102
|
+
hitch admin changesets update --id <changesetId> --status applied
|
|
103
|
+
hitch admin changesets update --id <changesetId> --status abandoned
|
|
104
|
+
hitch admin changesets update --id <changesetId> --adopt <otherChangesetId>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- A schema command without `--changeset-id` applies at once when it has no blockers. With a blocker, Hitch stages it in a new pending changeset and names it in the error.
|
|
108
|
+
- Add `--dry-run` to evaluate a schema command without a change.
|
|
109
|
+
- Add `--indexed` to an attribute that a query must sort or filter with ordered or text operators. Add `--is-multi` for a multi-value attribute. Pass `--type-config` and `--options` as JSON.
|
|
110
|
+
- Run `blockers list` before an apply. Any blocker stops the apply with HTTP 409.
|
|
111
|
+
- Run `hitch sync` after an apply, so the new objects and attributes appear as commands.
|
|
112
|
+
- Run `pnpm --dir applets/<code> run generate` after an apply, so applet types match.
|
|
113
|
+
|
|
114
|
+
## Fix common errors
|
|
115
|
+
|
|
116
|
+
| Message | Cause and fix |
|
|
117
|
+
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
118
|
+
| `Not logged in. Run hitch login.` | No stored login and no `HITCH_API_KEY`. Sign in. |
|
|
119
|
+
| `HITCH_API_KEY was rejected.` | The key in the environment is invalid or revoked. Unset it or replace it. |
|
|
120
|
+
| `API key is scoped to organization "x", not "y".` | The key belongs to another organization. Select `x` or use another key. |
|
|
121
|
+
| `--org "x" does not match workspace organization` | Run the command outside the workspace, or drop `--org`. |
|
|
122
|
+
| `Multiple organizations are stored ...` | Pass `--org`, set `hitch.org`, or set `HITCH_DEFAULT_ORG`. |
|
|
123
|
+
| `the cached commands ... no longer load` | Run `hitch sync`. |
|
|
124
|
+
| `Refusing to delete without --yes` | Add `--yes` in a script. |
|
|
125
|
+
| `hitch <object>` is an unknown command | Run `hitch sync`. The object is new or the cache is stale. |
|
|
126
|
+
| `User's teams lack the `read` grant on this object.` | The identity has no grant on that object. Ask for a team grant, or use another identity. |
|
|
127
|
+
| `hitch admin` is missing | The identity holds no admin capability. |
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hitch-data
|
|
3
|
+
description: The Hitch data model and data API rules. Use when a task defines or changes objects and attributes, models a relationship, writes or validates record values, builds a filter or sort, or hits an API limit. Covers attribute types, references, required and null rules, the indexed flag, and the enforced read, write, and aggregate limits.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hitch data
|
|
7
|
+
|
|
8
|
+
An organization's data lives in Hitch objects. Each object has attributes. Each record holds one value per attribute. Hitch validates, normalizes, and stores every value. Applets read and write records through the SDK and never keep a private copy.
|
|
9
|
+
|
|
10
|
+
## Go deeper
|
|
11
|
+
|
|
12
|
+
| Read this | When the task involves |
|
|
13
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
14
|
+
| `references/attributes.md` | The behavior of one attribute type: accepted values, null and empty rules, normalization, config |
|
|
15
|
+
| `references/limits.md` | A page size, a batch size, an aggregate limit, a filter that fails, or a sort that fails |
|
|
16
|
+
|
|
17
|
+
## Objects and records
|
|
18
|
+
|
|
19
|
+
- An object has a code, a name, and attributes. A record belongs to one object.
|
|
20
|
+
- Every record has an `id`, a required `name`, `createdAt`, `updatedAt`, and `deletedAt`.
|
|
21
|
+
- A record is JSON. Every owner attribute key is present on read. An empty attribute reads as `null`. An inverse reference face has no key.
|
|
22
|
+
- A record can be created with a client-supplied UUID `id`. Use this to link records inside one batch.
|
|
23
|
+
|
|
24
|
+
## Attributes
|
|
25
|
+
|
|
26
|
+
- An attribute is schema, not a free JSON field. Choose a type. Hitch validates the value against the type and its configuration.
|
|
27
|
+
- Types: text, rich_text, integer, decimal, currency, checkbox, date, datetime, time, domain, url, email, phone, select, reference, user, attachment.
|
|
28
|
+
- A value is a non-empty value or `null`. No type accepts an empty string.
|
|
29
|
+
- Cardinality is configuration. Text, select, reference, user, and attachment can be multi-value.
|
|
30
|
+
- A required attribute rejects `null` and rejects an empty array.
|
|
31
|
+
- Store a fixed set of values as a select, not as text. Store a link to another record as a reference, not as text or as an id in a text field. Text cannot be a group dimension, and text matching needs the `indexed` flag.
|
|
32
|
+
|
|
33
|
+
## References
|
|
34
|
+
|
|
35
|
+
- A reference attribute names a target object. Write the target record id.
|
|
36
|
+
- A reference can expose an inverse face on the target object. The owner face stores the link. The inverse face is read-only and is absent from the record JSON.
|
|
37
|
+
- One-to-many is a single reference on one side and a multi inverse on the other. Many-to-many is multi on both sides. `targetSingle` limits each target record to one link through the attribute.
|
|
38
|
+
- Do not model a plain relationship as a separate join object. A reference gives typed links, link cleanup when a target is deleted, filters through either face, and access through grants. Use a join object only when the relationship needs its own fields or lifecycle.
|
|
39
|
+
|
|
40
|
+
## Filters and sorting
|
|
41
|
+
|
|
42
|
+
- Every attribute accepts `$eq` and `$not_empty`. Every attribute except checkbox accepts `$in`.
|
|
43
|
+
- Ordered operators, text-match operators, and `sort` need the `indexed` flag on the attribute. A new attribute starts with the flag off. Ask for the flag on the attributes a query needs, not on every attribute.
|
|
44
|
+
- A filter can nest through a reference, through an inverse face, and can express an anti-join with `$not`.
|
|
45
|
+
- An aggregate filter does not need the flag. Sum, average, minimum, and maximum need a single-value numeric attribute. A group dimension is a checkbox, select, reference, user, or a bucketed date or datetime.
|
|
46
|
+
|
|
47
|
+
## Schema changes
|
|
48
|
+
|
|
49
|
+
- Every object or attribute change goes through a changeset. Hitch evaluates the change against existing records and dependent applets.
|
|
50
|
+
- A change that would break existing data, such as a unique constraint with duplicates, is blocked until the data is fixed.
|
|
51
|
+
- An applet release can bind to a pending changeset. The changeset apply switches the schema and promotes the release together.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Attribute types
|
|
2
|
+
|
|
3
|
+
Hitch validates and normalizes every attribute value on write. Read this before you build a form, an import, or a write path.
|
|
4
|
+
|
|
5
|
+
## Rules for every type
|
|
6
|
+
|
|
7
|
+
- A value is a non-empty value or `null`. Every type rejects an empty string.
|
|
8
|
+
- Send `null` to clear a value. A required attribute rejects `null` on create and on update.
|
|
9
|
+
- A single-value attribute rejects an array. A multi-value attribute rejects a scalar.
|
|
10
|
+
- A multi-value attribute holds an ordered array. An empty array is accepted when the attribute is not required, and reads back as `null`.
|
|
11
|
+
- Only text, select, reference, user, and attachment can be multi-value. Text allows duplicate entries. The other four reject duplicates.
|
|
12
|
+
- A `default` in the attribute configuration applies on create only, when the body omits the attribute.
|
|
13
|
+
- An `immutable` attribute rejects a change once it holds a non-null value. A null immutable attribute can still be set once.
|
|
14
|
+
- A `unique` attribute rejects a duplicate value with HTTP 409. Unique is never allowed on a multi-value attribute.
|
|
15
|
+
- Every owner attribute key is present on read. An empty attribute reads as `null`. An inverse reference face has no key.
|
|
16
|
+
- Every attribute accepts the `$eq` and `$not_empty` filters. Every attribute except checkbox accepts `$in`. Other operators and `sort` need the `indexed` flag. Read `limits.md`.
|
|
17
|
+
|
|
18
|
+
## Text and rich text
|
|
19
|
+
|
|
20
|
+
| Type | Write | Normalization | Config | Unique | Multi |
|
|
21
|
+
| --------- | ------------------------------------------- | ------------- | --------------------------------------------------------- | ------ | ----- |
|
|
22
|
+
| text | Non-empty string, at most 256 characters | None | default, immutable, unique, minLength, maxLength, pattern | Yes | Yes |
|
|
23
|
+
| rich_text | Non-empty string, no cap unless `maxLength` | None | default, immutable, minLength, maxLength | No | No |
|
|
24
|
+
|
|
25
|
+
- `pattern` is a JavaScript regular expression string. It is not anchored.
|
|
26
|
+
- Rich text is stored and returned as written. Hitch does not sanitize or parse it. Rich text supports identity filters only and never sorts.
|
|
27
|
+
|
|
28
|
+
## Numbers
|
|
29
|
+
|
|
30
|
+
| Type | Write | Read shape | Config | Unique |
|
|
31
|
+
| -------- | ------------------------------------------------------- | ---------- | ------------------------------------------- | ------ |
|
|
32
|
+
| integer | A JavaScript number that is a safe integer | number | default, immutable, unique, min, max | Yes |
|
|
33
|
+
| decimal | A decimal string or a number, at most 12 integer digits | string | default, immutable, unique, min, max, scale | Yes |
|
|
34
|
+
| currency | Same as decimal, scale defaults to 2 | string | decimal config plus `code` | Yes |
|
|
35
|
+
|
|
36
|
+
- Integer rejects a numeric string on write.
|
|
37
|
+
- Decimal returns a canonical string. Leading zeros are removed. With `scale`, the value is padded to exactly that many places. Without `scale`, trailing zeros are removed. Excess precision is rejected, never rounded.
|
|
38
|
+
- Decimal `min` and `max` are decimal strings. `scale` is 0 to 8 for decimal and 0 to 4 for currency.
|
|
39
|
+
- Currency `code` is three uppercase letters. It is metadata on the attribute, not part of the value. Hitch does no conversion.
|
|
40
|
+
|
|
41
|
+
## Checkbox
|
|
42
|
+
|
|
43
|
+
- Write a boolean. Hitch does not coerce strings on write.
|
|
44
|
+
- Config: default, immutable. No unique, no multi.
|
|
45
|
+
- Filters: `$eq` and `$not_empty` only.
|
|
46
|
+
|
|
47
|
+
## Date and time
|
|
48
|
+
|
|
49
|
+
| Type | Write | Read shape | Timezone | Config |
|
|
50
|
+
| -------- | ---------------------------------- | ----------------------- | --------------------- | ------------------------------------ |
|
|
51
|
+
| date | `YYYY-MM-DD`, a real calendar date | `YYYY-MM-DD` | None | default, immutable, unique, min, max |
|
|
52
|
+
| datetime | Any string `Date.parse` accepts | ISO 8601 in UTC, `...Z` | Converted to UTC | default, immutable, unique, min, max |
|
|
53
|
+
| time | `HH:MM` or `HH:MM:SS`, 24-hour | `HH:MM:SS` | None, wall clock only | default, immutable, unique |
|
|
54
|
+
|
|
55
|
+
- A datetime with an offset reads back in UTC. Store the organization timezone elsewhere when the display needs it.
|
|
56
|
+
- Date `min` and `max` are `YYYY-MM-DD` strings. Datetime `min` and `max` are datetime strings. Time has no `min` or `max`.
|
|
57
|
+
|
|
58
|
+
## Domain, URL, email, phone
|
|
59
|
+
|
|
60
|
+
| Type | Write | Normalization | Config | Unique |
|
|
61
|
+
| ------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------- | ------ |
|
|
62
|
+
| domain | Host name with at least two labels, no scheme, no path | Trimmed, lowercased, one trailing dot removed | default, immutable, unique | Yes |
|
|
63
|
+
| url | `http` or `https` URL, at most 2048 bytes | Canonical WHATWG form: lowercased host, trailing slash on a bare origin | default, immutable, unique | Yes |
|
|
64
|
+
| email | One `@`, a dot in the domain part, at most 256 characters | Trimmed, lowercased | default, immutable, unique | Yes |
|
|
65
|
+
| phone | 7 to 14 digits, optional leading `+`, with space, `(`, `)`, `.`, `-` separators | Separators removed, leading `+` kept | default, immutable | No |
|
|
66
|
+
|
|
67
|
+
- Email has no deliverability check.
|
|
68
|
+
- Phone has no region parsing. Hitch adds no country code. Send the full international form when the organization needs it.
|
|
69
|
+
|
|
70
|
+
## Select
|
|
71
|
+
|
|
72
|
+
- Write the option label, not the option id. Only an active option label matches.
|
|
73
|
+
- Hitch stores the option id and returns the label.
|
|
74
|
+
- A deleted option reads as `null` for a single select. It is dropped from the array for a multi select.
|
|
75
|
+
- Config: default, immutable. No unique. Multi-value allowed, duplicates rejected.
|
|
76
|
+
- Filters: identity operators only. Sort: allowed on a single select with `indexed`.
|
|
77
|
+
|
|
78
|
+
## Reference
|
|
79
|
+
|
|
80
|
+
- Write the target record UUID, or an array of UUIDs for a multi reference. The target must be a live record of the declared target object.
|
|
81
|
+
- A reference has two faces. The owner face stores the link. The inverse face on the target object is read-only and is absent from the record JSON. A write to it fails with `read_only_field`.
|
|
82
|
+
- `targetSingle` means each target record can be linked at most once through this attribute. A second link fails with HTTP 409 `reference_conflict`.
|
|
83
|
+
- Config: none. A reference accepts no default, immutable, or unique.
|
|
84
|
+
- Read: the UUID or array of UUIDs under the attribute code. With `include`, the expanded record appears under the include name, which is the code without its `_id` or `_ids` suffix. Include is one level deep.
|
|
85
|
+
- Filters: identity operators on the id, plus a nested filter on the target object's attributes. Sort: never.
|
|
86
|
+
|
|
87
|
+
## User
|
|
88
|
+
|
|
89
|
+
- Write the user UUID. The user must be a member of the organization.
|
|
90
|
+
- Config: default, immutable, unique. `default` is a UUID. Multi-value allowed, duplicates rejected.
|
|
91
|
+
- Read: the UUID. With `include`, the expansion is `{ id, firstName, lastName, email }`.
|
|
92
|
+
- Filters: identity operators only. Sort: never.
|
|
93
|
+
|
|
94
|
+
## Attachment
|
|
95
|
+
|
|
96
|
+
- Write the file id from a completed upload. An unknown id fails with `file not found`. A file with no uploaded bytes fails with `file has not arrived`. Read `../../hitch-applets/references/files.md`.
|
|
97
|
+
- Config: immutable only. No default, no unique. Multi-value allowed, duplicates rejected.
|
|
98
|
+
- Read: the file id. `include` does not expand attachments. Call the files API for a download link.
|
|
99
|
+
- Filters: identity operators only. Sort: never.
|
|
100
|
+
|
|
101
|
+
## Errors
|
|
102
|
+
|
|
103
|
+
| Situation | Status and code |
|
|
104
|
+
| -------------------------------------- | -------------------------------------------------------- |
|
|
105
|
+
| Value fails validation | 400 `invalid_field` |
|
|
106
|
+
| Required attribute missing on create | 400 `invalid_field` |
|
|
107
|
+
| Unknown attribute code in the body | 400 `unknown_field` |
|
|
108
|
+
| Write to an inverse reference face | 400 `read_only_field` |
|
|
109
|
+
| Change to a set immutable value | 400 `immutable_field` |
|
|
110
|
+
| Missing or blank record name | 400 `invalid_name` |
|
|
111
|
+
| Client-supplied `id` already exists | 409 `id_conflict` |
|
|
112
|
+
| Reference target missing | 400 `invalid_field` |
|
|
113
|
+
| User not a member | 400 `invalid_field` |
|
|
114
|
+
| Unique value already used | 409 `unique_conflict` |
|
|
115
|
+
| Single-target reference already linked | 409 `reference_conflict` |
|
|
116
|
+
| Operator not allowed for the attribute | 400 `invalid_filter` |
|
|
117
|
+
| Attribute cannot sort | 400 `invalid_sort` |
|
|
118
|
+
| Bad `limit`, `cursor`, or `include` | 400 `invalid_limit`, `invalid_cursor`, `invalid_include` |
|
|
119
|
+
| Bad aggregate or group | 400 `invalid_aggregate`, `invalid_group_by` |
|
|
120
|
+
|
|
121
|
+
Every record also has a `name`. It is a required non-empty string on create.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Hitch limits
|
|
2
|
+
|
|
3
|
+
Hitch limits the data API on purpose. The limits force good software design. No screen accidentally loads 500,000 records. No report runs one request per row. No import writes half of its rows and then fails. The limits also protect the organization: one applet or one user cannot ruin the experience for everyone else, and the cost of running the data stays sensible. When a limit stops you, the design is wrong, not the limit. Change the design: page the list, aggregate in Hitch, batch the writes, or narrow the filter.
|
|
4
|
+
|
|
5
|
+
Some limits clamp with no error. Some reject the request. Design for both.
|
|
6
|
+
|
|
7
|
+
## Reads
|
|
8
|
+
|
|
9
|
+
A page is the unit of a read. A screen shows one page and asks for the next one. A page holds at most 200 root records. An included reference has no cap on linked records, so include only small links.
|
|
10
|
+
|
|
11
|
+
| Limit | Value | Past the limit |
|
|
12
|
+
| ---------------------------------- | ----------- | -------------------------------- |
|
|
13
|
+
| `limit` on `list` and `query` | 50 default | Clamped to 200, no error |
|
|
14
|
+
| `limit` on `list` and `query`, max | 200 | Clamped to 200, no error |
|
|
15
|
+
| Filter nesting depth | 20 | HTTP 400 |
|
|
16
|
+
| `include` depth | 1 | HTTP 400 for a dotted path |
|
|
17
|
+
| `include` on an inverse reference | Not allowed | HTTP 400 |
|
|
18
|
+
| Records per included reference | No cap | All linked records return |
|
|
19
|
+
| IDs in one `$in` array | No cap | Only 200 records return per page |
|
|
20
|
+
|
|
21
|
+
## Aggregates
|
|
22
|
+
|
|
23
|
+
An aggregate answers a question about many records with a small result. The caps keep the result small enough to render. A question that needs more cells is two questions.
|
|
24
|
+
|
|
25
|
+
| Limit | Value | Past the limit |
|
|
26
|
+
| --------------------------------------- | ------------------------------------------------------------ | ------------------------ |
|
|
27
|
+
| Aggregates per call | 20 | HTTP 400 |
|
|
28
|
+
| Group dimensions | 2 | HTTP 400 |
|
|
29
|
+
| Multi-value group dimensions | 1 | HTTP 400 |
|
|
30
|
+
| Distinct keys per dimension, default | 50 | Clamped, no error |
|
|
31
|
+
| Distinct keys per dimension, max | 200 | Clamped to 200, no error |
|
|
32
|
+
| Cells, the product of dimension limits | 2500 | HTTP 400 |
|
|
33
|
+
| Reference steps in an aggregate path | 1 | HTTP 400 |
|
|
34
|
+
| Sum, average, min, max on a multi-value | Not allowed | HTTP 400 |
|
|
35
|
+
| Group dimension type | checkbox, select, reference, user, bucketed date or datetime | HTTP 400 |
|
|
36
|
+
| Aggregate alias `as` | 64 characters | HTTP 400 |
|
|
37
|
+
| Sum, average, min, max on a non-numeric | Not allowed | HTTP 400 |
|
|
38
|
+
|
|
39
|
+
## Writes
|
|
40
|
+
|
|
41
|
+
A write is bounded and atomic. A batch either commits every entry or none. An `updateWhere` refuses a filter that matches more than it can hold in one page, so a loose filter cannot rewrite a whole object.
|
|
42
|
+
|
|
43
|
+
| Limit | Value | Past the limit |
|
|
44
|
+
| ------------------------------------- | ----------- | --------------------------------------- |
|
|
45
|
+
| Entries per batch | 100 | HTTP 400 before any write |
|
|
46
|
+
| Records per `updateWhere` | 200 | HTTP 400 `update_too_broad`, no write |
|
|
47
|
+
| `expect` on `updateWhere` | Exact count | HTTP 409 `expectation_failed`, no write |
|
|
48
|
+
| Idempotency key with a different body | Not allowed | HTTP 422 `idempotency_key_reuse` |
|
|
49
|
+
| Idempotency key retention | 24 hours | The key is forgotten |
|
|
50
|
+
|
|
51
|
+
## Filters and sorting
|
|
52
|
+
|
|
53
|
+
Advanced filters and sorting are opt-in per attribute. This keeps each record cheap to write, and makes the attributes a query depends on an explicit decision.
|
|
54
|
+
|
|
55
|
+
- Every attribute accepts `$eq` and `$not_empty`. Every attribute except checkbox accepts `$in`.
|
|
56
|
+
- Ordered operators, text-match operators, and `sort` need the `indexed` flag on the attribute. Without it the request fails with HTTP 400 and a message that starts with `Enable advanced filters and sort for`.
|
|
57
|
+
- New attributes start with `indexed` off. Ask the organization admin to turn it on for the attributes a query needs. Do not ask for it on every attribute.
|
|
58
|
+
- No attribute allows `sort` when it is multi-value, rich text, reference, user, or attachment.
|
|
59
|
+
- An aggregate filter does not need `indexed`. Sum, average, minimum, and maximum need a single-value numeric attribute.
|
|
60
|
+
|
|
61
|
+
## Grant conditions
|
|
62
|
+
|
|
63
|
+
- A condition path has at most two hops.
|
|
64
|
+
- A record whose condition target is empty is invisible to that grant. A `get` on it returns HTTP 404.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Deploy Hitch applets
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
paths:
|
|
6
|
+
- "applets/**"
|
|
7
|
+
- ".github/workflows/hitch.yml"
|
|
8
|
+
- "hitch.config.ts"
|
|
9
|
+
- "pnpm-lock.yaml"
|
|
10
|
+
- "pnpm-workspace.yaml"
|
|
11
|
+
|
|
12
|
+
permissions: {}
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
deploy:
|
|
16
|
+
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
id-token: write
|
|
20
|
+
# Hitch verifies the called workflow's commit SHA from the OIDC token.
|
|
21
|
+
uses: hitch-42/actions/.github/workflows/deploy.yml@b03cda4a4369decfc376d87425c21bcb61bddc57
|
package/dist/build-CEku1lyb.js
DELETED