@mapled/cli 0.5.0 → 0.7.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 +25 -4
- package/dist/args.js +1 -1
- package/dist/commands.d.ts +8 -3
- package/dist/commands.js +55 -0
- package/dist/index.js +10 -1
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +9 -2
- package/dist/pin.d.ts +5 -2
- package/dist/pin.js +25 -9
- package/dist/schema.d.ts +4 -0
- package/dist/types.js +19 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ npx @mapled/cli doctor
|
|
|
14
14
|
|
|
15
15
|
Node 18 or newer. No dependencies. Prefer a global install? `npm install -g @mapled/cli` puts `mapled` on your PATH, and every command below works as `mapled …`.
|
|
16
16
|
|
|
17
|
+
Connecting a site without an AI agent? [Integrate by hand](https://www.npmjs.com/package/@mapled/next#integrate-by-hand) in the `@mapled/next` README walks through the whole integration — modeling the content, reading it, the publish webhook, preview, forms — and uses these commands along the way.
|
|
18
|
+
|
|
17
19
|
## Sign in
|
|
18
20
|
|
|
19
21
|
```bash
|
|
@@ -84,7 +86,9 @@ Articles (articles)
|
|
|
84
86
|
4 breaking changes, 2 safe changes, judged for a site that reads the content. Update the site where needed, then run `mapled types generate` (it refreshes mapled/schema.json as well).
|
|
85
87
|
```
|
|
86
88
|
|
|
87
|
-
Breaking: a collection, field or option the site may rely on disappears, a type changes, a required field becomes optional, a field becomes sensitive (it stops being delivered), a relation points elsewhere. Safe: additions, renamed labels, help texts, validation rules. `--json` for scripts and agents; `--exit-code` exits 1 when anything changed, for CI.
|
|
89
|
+
Breaking: a collection, field or option the site may rely on disappears, a type changes, a required field becomes optional, a field becomes sensitive (it stops being delivered), a relation points elsewhere. Safe: additions, renamed labels, help texts, validation rules — and a renamed key while its old one is still an alias (`> price renamed to “amount” — “price” still works as an alias`): the site keeps reading it, the rest of the field is compared under the new key, and `types generate` types the old key `@deprecated`. Once the alias is removed in Mapled the old key is a removed field. `--json` for scripts and agents; `--exit-code` exits 1 when anything changed, for CI.
|
|
90
|
+
|
|
91
|
+
The pin keeps formulas of computed fields and previous keys. (A pin written by 0.5.0 lost the formulas, so its hash never matched a project that has computed fields — run `schema pull` once after upgrading.)
|
|
88
92
|
|
|
89
93
|
## Scan the code into a manifest
|
|
90
94
|
|
|
@@ -125,6 +129,23 @@ npx @mapled/cli bindings pull
|
|
|
125
129
|
|
|
126
130
|
`bindings pull` writes the pushed manifest into `mapled/manifest.json` — say, after your agent pushed one from a chat and you want the repository's copy. It refuses to overwrite a local file that differs unless you pass `--force`.
|
|
127
131
|
|
|
132
|
+
## Create a form
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx @mapled/cli forms create "Contact"
|
|
136
|
+
npx @mapled/cli forms list
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`forms create` makes a form the site can post submissions to — the same call your AI agent's `create_form` makes, so an integration done by hand needs no agent for it:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
Created the form “Contact” (contact) in Dolphin Landing.
|
|
143
|
+
The site posts submissions to https://api.mapled.io/v1/delivery/forms/contact — flat JSON with the delivery key in X-Mapled-Key, an empty hidden _gotcha field, and the consent label as _consent when the form has a consent checkbox.
|
|
144
|
+
Submissions show up in Mapled under Forms.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The key comes from the name; `--key <key>` chooses it (lowercase letters, digits and dashes), `--no-spam-protection` turns the honeypot off. Creating a form changes structure, so it needs the builder plan. `forms list` shows every form with the URL the site posts to (`--json` for scripts). The component that posts to it is in the [`@mapled/next` README](https://www.npmjs.com/package/@mapled/next#forms).
|
|
148
|
+
|
|
128
149
|
## Leave a guide for the next agent
|
|
129
150
|
|
|
130
151
|
```bash
|
|
@@ -153,8 +174,8 @@ Mapled doctor — Dolphin Landing
|
|
|
153
174
|
✓ MAPLED.md MAPLED.md matches the schema (3f9a1c2b4d5e) and manifest v3
|
|
154
175
|
✓ Environment MAPLED_KEY and MAPLED_WEBHOOK_SECRET in .env.local
|
|
155
176
|
✓ Secrets in git No env files or Mapled secrets are tracked.
|
|
156
|
-
✓ @mapled/next 0.
|
|
157
|
-
✓ mapled CLI 0.
|
|
177
|
+
✓ @mapled/next 0.7.1 (current)
|
|
178
|
+
✓ mapled CLI 0.7.0 (current)
|
|
158
179
|
✓ Revalidation route app/api/mapled/revalidate/route.ts
|
|
159
180
|
⚠ Preview route app/api/mapled/preview/route.ts is missing — mount createPreviewHandler from "@mapled/next/server" so Preview from Mapled works.
|
|
160
181
|
✓ Site reads content Last read 3m ago
|
|
@@ -173,7 +194,7 @@ Exit code 1 when something is marked ✗; `--json` prints the checks for scripts
|
|
|
173
194
|
|
|
174
195
|
- `--api <origin>` — a Mapled API other than `https://api.mapled.io` (or set `MAPLED_API_URL`); `project link` remembers it in `mapled.json`
|
|
175
196
|
- `--no-browser` — print the sign-in link instead of opening a browser
|
|
176
|
-
- `--json` — machine-readable output (`doctor`, `schema diff`, `scan`, `manifest validate`)
|
|
197
|
+
- `--json` — machine-readable output (`doctor`, `schema diff`, `scan`, `manifest validate`, `forms list`)
|
|
177
198
|
- `MAPLED_CONFIG_DIR` — where credentials live (default: `$XDG_CONFIG_HOME/mapled` or `~/.config/mapled`)
|
|
178
199
|
|
|
179
200
|
## Files in the repository
|
package/dist/args.js
CHANGED
|
@@ -2,7 +2,7 @@ import { CliError } from "./errors.js";
|
|
|
2
2
|
/** Minimal argument parsing: the leading words name the command;
|
|
3
3
|
`--flag`, `--flag=value` and `--flag value` (for the flags that take
|
|
4
4
|
one) follow. Nothing here is user content beyond the command line. */
|
|
5
|
-
const VALUE_FLAGS = new Set(["api", "project", "out"]);
|
|
5
|
+
const VALUE_FLAGS = new Set(["api", "project", "out", "key"]);
|
|
6
6
|
export function parseArgs(argv) {
|
|
7
7
|
const words = [];
|
|
8
8
|
const flags = {};
|
package/dist/commands.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { type ParsedArgs } from "./args.js";
|
|
|
2
2
|
import { type FoundConfig } from "./config.js";
|
|
3
3
|
import { type Connection } from "./credentials.js";
|
|
4
4
|
import { type Fetch } from "./oauth.js";
|
|
5
|
-
/** The commands of waves 1 and 2 (§31)
|
|
6
|
-
world comes in through `Ctx`, so the
|
|
7
|
-
fake API and a temp directory. */
|
|
5
|
+
/** The commands of waves 1 and 2 (§31), MAPLED.md (§21.3) and forms.
|
|
6
|
+
Everything that talks to the world comes in through `Ctx`, so the
|
|
7
|
+
commands run in tests against a fake API and a temp directory. */
|
|
8
8
|
export type Ctx = {
|
|
9
9
|
cwd: string;
|
|
10
10
|
env: NodeJS.ProcessEnv;
|
|
@@ -29,6 +29,11 @@ export declare function manifestValidate(ctx: Ctx, flags: Flags): Promise<number
|
|
|
29
29
|
export declare function bindingsPush(ctx: Ctx, flags: Flags): Promise<void>;
|
|
30
30
|
export declare function bindingsPull(ctx: Ctx, flags: Flags): Promise<void>;
|
|
31
31
|
export declare function mdPull(ctx: Ctx, flags: Flags): Promise<void>;
|
|
32
|
+
/** `mapled forms create "Contact"` — a form the site posts submissions to.
|
|
33
|
+
The same call an AI agent's `create_form` makes, so an integration
|
|
34
|
+
done by hand needs no agent for it. */
|
|
35
|
+
export declare function formsCreate(ctx: Ctx, flags: Flags, words: string[]): Promise<void>;
|
|
36
|
+
export declare function formsList(ctx: Ctx, flags: Flags): Promise<void>;
|
|
32
37
|
export declare function scan(ctx: Ctx, flags: Flags): Promise<void>;
|
|
33
38
|
export declare function doctor(ctx: Ctx, flags: Flags): Promise<number>;
|
|
34
39
|
export {};
|
package/dist/commands.js
CHANGED
|
@@ -390,6 +390,61 @@ export async function mdPull(ctx, flags) {
|
|
|
390
390
|
else
|
|
391
391
|
ctx.out(`Updated ${MD_FILE} — ${summary}; the notes below the notes line were kept.`);
|
|
392
392
|
}
|
|
393
|
+
const FORM_KEY = /^[a-z][a-z0-9-]{0,63}$/;
|
|
394
|
+
const submitUrl = (api, key) => `${api}/v1/delivery/forms/${key}`;
|
|
395
|
+
/** `mapled forms create "Contact"` — a form the site posts submissions to.
|
|
396
|
+
The same call an AI agent's `create_form` makes, so an integration
|
|
397
|
+
done by hand needs no agent for it. */
|
|
398
|
+
export async function formsCreate(ctx, flags, words) {
|
|
399
|
+
const displayName = words.join(" ").trim();
|
|
400
|
+
if (!displayName)
|
|
401
|
+
throw new CliError('Name the form: `mapled forms create "Contact"`.');
|
|
402
|
+
if (displayName.length > 80)
|
|
403
|
+
throw new CliError("A form name is up to 80 characters.");
|
|
404
|
+
const key = stringFlag(flags, "key");
|
|
405
|
+
if (flags.key !== undefined && (!key || !FORM_KEY.test(key))) {
|
|
406
|
+
throw new CliError("A form key is lowercase letters, digits and dashes, starting with a letter (up to 64 characters).");
|
|
407
|
+
}
|
|
408
|
+
const { session } = await openSession(ctx, flags);
|
|
409
|
+
let created;
|
|
410
|
+
try {
|
|
411
|
+
created = await session.send("POST", "/v1/agent/forms", {
|
|
412
|
+
displayName,
|
|
413
|
+
...(key ? { key } : {}),
|
|
414
|
+
...(flags["no-spam-protection"] ? { spamProtection: false } : {}),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
if (err instanceof ApiError && err.code === "BUILDER_PLAN_REQUIRED") {
|
|
419
|
+
throw new CliError("Creating a form changes the project's structure, which needs a builder plan — ask the project owner to upgrade.");
|
|
420
|
+
}
|
|
421
|
+
throw err;
|
|
422
|
+
}
|
|
423
|
+
const form = created.form;
|
|
424
|
+
ctx.out(`Created the form “${form.displayName}” (${form.key}) in ${session.conn.projectName}.`);
|
|
425
|
+
ctx.out(`The site posts submissions to ${submitUrl(session.conn.api, form.key)} — flat JSON with the delivery key in X-Mapled-Key` +
|
|
426
|
+
`${form.spamProtection ? ", an empty hidden _gotcha field" : ""}, and the consent label as _consent when the form has a consent checkbox.`);
|
|
427
|
+
ctx.out("Submissions show up in Mapled under Forms.");
|
|
428
|
+
}
|
|
429
|
+
export async function formsList(ctx, flags) {
|
|
430
|
+
const { session } = await openSession(ctx, flags);
|
|
431
|
+
const { forms } = await session.get("/v1/agent/forms");
|
|
432
|
+
if (flags.json) {
|
|
433
|
+
ctx.out(JSON.stringify({ forms: forms.map((f) => ({ ...f, submitUrl: submitUrl(session.conn.api, f.key) })) }, null, 2));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (forms.length === 0) {
|
|
437
|
+
ctx.out(`No forms in ${session.conn.projectName} yet. Create one with \`mapled forms create "Contact"\`.`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const keyWidth = Math.max(...forms.map((f) => f.key.length));
|
|
441
|
+
const nameWidth = Math.max(...forms.map((f) => f.displayName.length));
|
|
442
|
+
ctx.out(`Forms of ${session.conn.projectName}`);
|
|
443
|
+
ctx.out("");
|
|
444
|
+
for (const f of forms) {
|
|
445
|
+
ctx.out(`${f.key.padEnd(keyWidth)} ${f.displayName.padEnd(nameWidth)} spam protection ${f.spamProtection ? "on " : "off"} ${submitUrl(session.conn.api, f.key)}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
393
448
|
/* ---- wave 2: the scanner ---- */
|
|
394
449
|
function describeScan(result, schema) {
|
|
395
450
|
const singles = new Set(schema.collections.filter((c) => c.kind === "single").map((c) => c.key));
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { parseArgs } from "./args.js";
|
|
5
5
|
import { openBrowser } from "./browser.js";
|
|
6
|
-
import { bindingsPull, bindingsPush, doctor, generate, link, login, logout, manifestValidate, mdPull, scan, schemaDiff, schemaPull, } from "./commands.js";
|
|
6
|
+
import { bindingsPull, bindingsPush, doctor, formsCreate, formsList, generate, link, login, logout, manifestValidate, mdPull, scan, schemaDiff, schemaPull, } from "./commands.js";
|
|
7
7
|
import { credentialsPath } from "./credentials.js";
|
|
8
8
|
import { CliError } from "./errors.js";
|
|
9
9
|
/** `mapled` — sign in, link a repository to its project, generate types
|
|
@@ -24,6 +24,8 @@ Usage
|
|
|
24
24
|
mapled bindings push [--dry-run] Push mapled/manifest.json so Mapled grades every binding
|
|
25
25
|
mapled bindings pull [--force] Write the pushed manifest into mapled/manifest.json
|
|
26
26
|
mapled md pull [--force] Write MAPLED.md, the guide for the next agent, from the project
|
|
27
|
+
mapled forms create <name> [--key <key>] Create a form the site posts submissions to (--no-spam-protection to skip the honeypot)
|
|
28
|
+
mapled forms list [--json] List the project's forms and where the site posts to
|
|
27
29
|
mapled doctor [--json] Check the integration end to end
|
|
28
30
|
|
|
29
31
|
Options
|
|
@@ -108,6 +110,12 @@ async function main(argv) {
|
|
|
108
110
|
case "md pull":
|
|
109
111
|
await mdPull(ctx, flags);
|
|
110
112
|
return 0;
|
|
113
|
+
case "forms create":
|
|
114
|
+
await formsCreate(ctx, flags, words.slice(2));
|
|
115
|
+
return 0;
|
|
116
|
+
case "forms list":
|
|
117
|
+
await formsList(ctx, flags);
|
|
118
|
+
return 0;
|
|
111
119
|
case "doctor":
|
|
112
120
|
return doctor(ctx, flags);
|
|
113
121
|
case "auth":
|
|
@@ -117,6 +125,7 @@ async function main(argv) {
|
|
|
117
125
|
case "manifest":
|
|
118
126
|
case "bindings":
|
|
119
127
|
case "md":
|
|
128
|
+
case "forms":
|
|
120
129
|
console.log(HELP);
|
|
121
130
|
return 2;
|
|
122
131
|
default:
|
package/dist/manifest.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export type SchemaIndex = Map<string, {
|
|
|
62
62
|
displayName: string;
|
|
63
63
|
type: string;
|
|
64
64
|
}>;
|
|
65
|
+
/** previous key → current key (§33.5): a binding on it still works, through the alias */
|
|
66
|
+
aliases: Map<string, string>;
|
|
65
67
|
}>;
|
|
66
68
|
export declare function indexSchema(schema: Schema): SchemaIndex;
|
|
67
69
|
/** What a well-formed manifest still gets wrong: bindings the schema
|
package/dist/manifest.js
CHANGED
|
@@ -242,13 +242,16 @@ export function indexSchema(schema) {
|
|
|
242
242
|
const index = new Map();
|
|
243
243
|
for (const c of schema.collections) {
|
|
244
244
|
const fields = new Map();
|
|
245
|
+
const aliases = new Map();
|
|
245
246
|
// a computed field (§14.9) binds as a field of its result type
|
|
246
247
|
const COMPUTED_AS = { number: "number", text: "short_text", boolean: "boolean", date: "date", datetime: "datetime" };
|
|
247
248
|
for (const f of c.fields) {
|
|
248
249
|
const type = f.type === "computed" && f.computed ? (COMPUTED_AS[f.computed.resultType] ?? f.type) : f.type;
|
|
249
250
|
fields.set(f.key, { displayName: f.displayName, type });
|
|
251
|
+
for (const a of f.aliases ?? [])
|
|
252
|
+
aliases.set(a, f.key);
|
|
250
253
|
}
|
|
251
|
-
index.set(c.key, { displayName: c.displayName, kind: c.kind, fields });
|
|
254
|
+
index.set(c.key, { displayName: c.displayName, kind: c.kind, fields, aliases });
|
|
252
255
|
}
|
|
253
256
|
return index;
|
|
254
257
|
}
|
|
@@ -278,7 +281,11 @@ export async function checkManifest(manifest, opts) {
|
|
|
278
281
|
warn(`${at}.collection`, `${who}: no collection "${b.collection}" in the schema — Mapled marks it outdated.`);
|
|
279
282
|
else if (b.field) {
|
|
280
283
|
const field = collection.fields.get(b.field);
|
|
281
|
-
|
|
284
|
+
const renamedTo = collection.aliases.get(b.field);
|
|
285
|
+
if (!field && renamedTo) {
|
|
286
|
+
warn(`${at}.field`, `${who}: "${b.field}" in "${collection.displayName}" was renamed to "${renamedTo}" — it still works through an alias, and Mapled marks the binding outdated. Read "${renamedTo}", then remove the alias.`);
|
|
287
|
+
}
|
|
288
|
+
else if (!field) {
|
|
282
289
|
warn(`${at}.field`, `${who}: no field "${b.field}" in "${collection.displayName}" — Mapled marks it outdated.`);
|
|
283
290
|
}
|
|
284
291
|
else {
|
package/dist/pin.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare function readPin(dir: string): Promise<{
|
|
|
20
20
|
} | null>;
|
|
21
21
|
export declare function writePin(dir: string, pin: SchemaPin): Promise<string>;
|
|
22
22
|
export type Severity = "breaking" | "safe";
|
|
23
|
-
export type ChangeOp = "added" | "removed" | "changed";
|
|
23
|
+
export type ChangeOp = "added" | "removed" | "changed" | "renamed";
|
|
24
24
|
export type SchemaChange = {
|
|
25
25
|
scope: "collection" | "field" | "subfield";
|
|
26
26
|
collection: string;
|
|
@@ -33,7 +33,10 @@ export type SchemaChange = {
|
|
|
33
33
|
};
|
|
34
34
|
/** What changed between two schemas, judged for a site that reads the
|
|
35
35
|
content: anything that can make a read fail or a value disappear is
|
|
36
|
-
breaking; additions and renames of labels are safe.
|
|
36
|
+
breaking; additions and renames of labels are safe. A renamed key
|
|
37
|
+
(§33.5) is safe while the old one is still an alias of the field —
|
|
38
|
+
the site keeps reading it — and the rest of the field is compared
|
|
39
|
+
under its new key; once the alias is removed it is a removed field. */
|
|
37
40
|
export declare function diffSchema(before: SchemaCollection[], after: SchemaCollection[]): SchemaChange[];
|
|
38
41
|
/** The diff as terminal lines: one heading per collection, one line per
|
|
39
42
|
change, the severity in the last column. */
|
package/dist/pin.js
CHANGED
|
@@ -44,6 +44,11 @@ function pinField(f) {
|
|
|
44
44
|
out.validation = f.validation;
|
|
45
45
|
if (f.defaultValue !== undefined && f.defaultValue !== null)
|
|
46
46
|
out.defaultValue = f.defaultValue;
|
|
47
|
+
// the formula is part of the schema hash: a pin without it would never match the live schema
|
|
48
|
+
if (f.type === "computed" && f.computed)
|
|
49
|
+
out.computed = { expression: f.computed.expression, resultType: f.computed.resultType };
|
|
50
|
+
if (f.aliases && f.aliases.length > 0)
|
|
51
|
+
out.aliases = [...f.aliases];
|
|
47
52
|
return out;
|
|
48
53
|
}
|
|
49
54
|
/** The schema reduced to what the pin keeps — the live answer of
|
|
@@ -143,7 +148,10 @@ function sameJson(a, b) {
|
|
|
143
148
|
}
|
|
144
149
|
/** What changed between two schemas, judged for a site that reads the
|
|
145
150
|
content: anything that can make a read fail or a value disappear is
|
|
146
|
-
breaking; additions and renames of labels are safe.
|
|
151
|
+
breaking; additions and renames of labels are safe. A renamed key
|
|
152
|
+
(§33.5) is safe while the old one is still an alias of the field —
|
|
153
|
+
the site keeps reading it — and the rest of the field is compared
|
|
154
|
+
under its new key; once the alias is removed it is a removed field. */
|
|
147
155
|
export function diffSchema(before, after) {
|
|
148
156
|
const changes = [];
|
|
149
157
|
const names = new Map();
|
|
@@ -171,13 +179,21 @@ export function diffSchema(before, after) {
|
|
|
171
179
|
col(`name ${q(b.displayName)} → ${q(a.displayName)}`, "safe");
|
|
172
180
|
const afterFields = new Map(a.fields.map((f) => [f.key, f]));
|
|
173
181
|
const beforeFields = new Map(b.fields.map((f) => [f.key, f]));
|
|
182
|
+
const renamedTo = new Set();
|
|
174
183
|
for (const bf of b.fields) {
|
|
175
|
-
|
|
184
|
+
let af = afterFields.get(bf.key);
|
|
176
185
|
if (!af) {
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
const heir = a.fields.find((f) => !beforeFields.has(f.key) && !renamedTo.has(f.key) && f.aliases?.includes(bf.key));
|
|
187
|
+
if (!heir) {
|
|
188
|
+
changes.push({ ...base, scope: "field", field: bf.key, op: "removed", detail: `removed — ${describeType(bf, names)}`, severity: "breaking" });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
renamedTo.add(heir.key);
|
|
192
|
+
changes.push({ ...base, scope: "field", field: bf.key, op: "renamed", detail: `renamed to ${q(heir.key)} — ${q(bf.key)} still works as an alias`, severity: "safe" });
|
|
193
|
+
af = heir;
|
|
179
194
|
}
|
|
180
|
-
const
|
|
195
|
+
const under = af.key;
|
|
196
|
+
const push = (op, detail, severity) => changes.push({ ...base, scope: "field", field: under, op, detail, severity });
|
|
181
197
|
diffCommon(bf, af, names, push);
|
|
182
198
|
if (bf.type === "relation" && af.type === "relation" && bf.relation && af.relation) {
|
|
183
199
|
if (bf.relation.target !== af.relation.target) {
|
|
@@ -209,7 +225,7 @@ export function diffSchema(before, after) {
|
|
|
209
225
|
const beforeSubs = new Map(bf.group.fields.map((s) => [s.key, s]));
|
|
210
226
|
for (const bs of bf.group.fields) {
|
|
211
227
|
const as = afterSubs.get(bs.key);
|
|
212
|
-
const sub = `${
|
|
228
|
+
const sub = `${under}.${bs.key}`;
|
|
213
229
|
if (!as) {
|
|
214
230
|
changes.push({ ...base, scope: "subfield", field: sub, op: "removed", detail: `removed — ${describeType(bs, names)}`, severity: "breaking" });
|
|
215
231
|
continue;
|
|
@@ -222,7 +238,7 @@ export function diffSchema(before, after) {
|
|
|
222
238
|
changes.push({
|
|
223
239
|
...base,
|
|
224
240
|
scope: "subfield",
|
|
225
|
-
field: `${
|
|
241
|
+
field: `${under}.${as.key}`,
|
|
226
242
|
op: "added",
|
|
227
243
|
detail: `added — ${describeType(as, names)}, ${as.required ? "required" : "optional"}`,
|
|
228
244
|
severity: "safe",
|
|
@@ -235,7 +251,7 @@ export function diffSchema(before, after) {
|
|
|
235
251
|
push("changed", "default value changed", "safe");
|
|
236
252
|
}
|
|
237
253
|
for (const af of a.fields) {
|
|
238
|
-
if (beforeFields.has(af.key))
|
|
254
|
+
if (beforeFields.has(af.key) || renamedTo.has(af.key))
|
|
239
255
|
continue;
|
|
240
256
|
changes.push({
|
|
241
257
|
...base,
|
|
@@ -261,7 +277,7 @@ export function diffSchema(before, after) {
|
|
|
261
277
|
}
|
|
262
278
|
return changes;
|
|
263
279
|
}
|
|
264
|
-
const GLYPH = { added: "+", removed: "-", changed: "~" };
|
|
280
|
+
const GLYPH = { added: "+", removed: "-", changed: "~", renamed: ">" };
|
|
265
281
|
/** The diff as terminal lines: one heading per collection, one line per
|
|
266
282
|
change, the severity in the last column. */
|
|
267
283
|
export function formatChanges(changes, paint = (_, t) => t) {
|
package/dist/schema.d.ts
CHANGED
|
@@ -37,6 +37,10 @@ export type SchemaField = {
|
|
|
37
37
|
expression: string;
|
|
38
38
|
resultType: string;
|
|
39
39
|
} | null;
|
|
40
|
+
/** Previous keys (§33.5): the field was renamed, and Mapled serves and
|
|
41
|
+
accepts it under these too until they are removed. Not part of the
|
|
42
|
+
schema hash — a rename changes the key, which is. */
|
|
43
|
+
aliases?: string[];
|
|
40
44
|
};
|
|
41
45
|
export type SchemaCollection = {
|
|
42
46
|
key: string;
|
package/dist/types.js
CHANGED
|
@@ -115,14 +115,26 @@ function fieldLine(key, displayName, required, rendered) {
|
|
|
115
115
|
function renderSubField(sub) {
|
|
116
116
|
return fieldLine(sub.key, sub.displayName, sub.required, scalarType(sub.type, sub.options));
|
|
117
117
|
}
|
|
118
|
+
/** The field's line, followed by one per previous key (§33.5): a
|
|
119
|
+
renamed field is still delivered under them, typed as the field, and
|
|
120
|
+
the editor strikes them through until the site stops reading them. */
|
|
118
121
|
function renderField(field, owner, names, extras) {
|
|
122
|
+
const { required, rendered } = renderedField(field, owner, names, extras);
|
|
123
|
+
const lines = [fieldLine(field.key, field.displayName, required, rendered)];
|
|
124
|
+
for (const alias of field.aliases ?? []) {
|
|
125
|
+
lines.push(` /** @deprecated Renamed to "${comment(field.key)}" — still delivered under this key until the alias is removed in Mapled. */\n` +
|
|
126
|
+
` ${prop(alias)}?: ${rendered.type} | null;`);
|
|
127
|
+
}
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
function renderedField(field, owner, names, extras) {
|
|
119
131
|
if (field.type === "relation" && field.relation) {
|
|
120
132
|
const target = names.get(field.relation.target) ?? field.relation.target;
|
|
121
133
|
const many = field.relation.cardinality === "many";
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
note: `relation to ${comment(target)}${many ? " (many)" : ""}
|
|
125
|
-
}
|
|
134
|
+
return {
|
|
135
|
+
required: field.required,
|
|
136
|
+
rendered: { type: many ? "MapledRecordId[]" : "MapledRecordId", note: `relation to ${comment(target)}${many ? " (many)" : ""}` },
|
|
137
|
+
};
|
|
126
138
|
}
|
|
127
139
|
if (field.type === "group" && field.group) {
|
|
128
140
|
const itemName = `${owner}${pascal(wordsOf(field.key))}${field.group.repeatable ? "Item" : ""}`;
|
|
@@ -136,15 +148,12 @@ function renderField(field, owner, names, extras) {
|
|
|
136
148
|
const note = field.group.repeatable
|
|
137
149
|
? `group${field.group.maxItems ? `, up to ${field.group.maxItems} items` : ""}`
|
|
138
150
|
: "group";
|
|
139
|
-
return
|
|
140
|
-
type: field.group.repeatable ? `${itemName}[]` : itemName,
|
|
141
|
-
note,
|
|
142
|
-
});
|
|
151
|
+
return { required: field.required, rendered: { type: field.group.repeatable ? `${itemName}[]` : itemName, note } };
|
|
143
152
|
}
|
|
144
153
|
if (field.type === "computed" && field.computed) {
|
|
145
|
-
return
|
|
154
|
+
return { required: false, rendered: computedType(field.computed.resultType) };
|
|
146
155
|
}
|
|
147
|
-
return
|
|
156
|
+
return { required: field.required, rendered: scalarType(field.type, field.options) };
|
|
148
157
|
}
|
|
149
158
|
/** Unique type names: the singular PascalCase of the key; the raw
|
|
150
159
|
PascalCase when two keys meet there; a numeric suffix as a last resort. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mapled/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Mapled CLI — sign in, link a project, generate TypeScript types for its content and check the site's integration.",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Mapled CLI — sign in, link a project, generate TypeScript types for its content, create forms and check the site's integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://mapled.io",
|
|
7
7
|
"keywords": [
|