@mapled/cli 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js +16 -4
- package/dist/pin.d.ts +5 -2
- package/dist/pin.js +36 -9
- package/dist/schema.d.ts +10 -0
- package/dist/schema.js +2 -0
- package/dist/types.js +34 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,7 +84,9 @@ Articles (articles)
|
|
|
84
84
|
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
85
|
```
|
|
86
86
|
|
|
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.
|
|
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 — 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.
|
|
88
|
+
|
|
89
|
+
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
90
|
|
|
89
91
|
## Scan the code into a manifest
|
|
90
92
|
|
|
@@ -153,7 +155,7 @@ Mapled doctor — Dolphin Landing
|
|
|
153
155
|
✓ MAPLED.md MAPLED.md matches the schema (3f9a1c2b4d5e) and manifest v3
|
|
154
156
|
✓ Environment MAPLED_KEY and MAPLED_WEBHOOK_SECRET in .env.local
|
|
155
157
|
✓ Secrets in git No env files or Mapled secrets are tracked.
|
|
156
|
-
✓ @mapled/next 0.
|
|
158
|
+
✓ @mapled/next 0.7.0 (current)
|
|
157
159
|
✓ mapled CLI 0.2.0 (current)
|
|
158
160
|
✓ Revalidation route app/api/mapled/revalidate/route.ts
|
|
159
161
|
⚠ Preview route app/api/mapled/preview/route.ts is missing — mount createPreviewHandler from "@mapled/next/server" so Preview from Mapled works.
|
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
|
@@ -84,6 +84,7 @@ export const FIELD_TYPE_LABEL = {
|
|
|
84
84
|
group: "group",
|
|
85
85
|
json: "JSON",
|
|
86
86
|
location: "location",
|
|
87
|
+
computed: "computed",
|
|
87
88
|
};
|
|
88
89
|
export function typeLabel(type) {
|
|
89
90
|
return FIELD_TYPE_LABEL[type] ?? type;
|
|
@@ -241,9 +242,16 @@ export function indexSchema(schema) {
|
|
|
241
242
|
const index = new Map();
|
|
242
243
|
for (const c of schema.collections) {
|
|
243
244
|
const fields = new Map();
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
const aliases = new Map();
|
|
246
|
+
// a computed field (§14.9) binds as a field of its result type
|
|
247
|
+
const COMPUTED_AS = { number: "number", text: "short_text", boolean: "boolean", date: "date", datetime: "datetime" };
|
|
248
|
+
for (const f of c.fields) {
|
|
249
|
+
const type = f.type === "computed" && f.computed ? (COMPUTED_AS[f.computed.resultType] ?? f.type) : f.type;
|
|
250
|
+
fields.set(f.key, { displayName: f.displayName, type });
|
|
251
|
+
for (const a of f.aliases ?? [])
|
|
252
|
+
aliases.set(a, f.key);
|
|
253
|
+
}
|
|
254
|
+
index.set(c.key, { displayName: c.displayName, kind: c.kind, fields, aliases });
|
|
247
255
|
}
|
|
248
256
|
return index;
|
|
249
257
|
}
|
|
@@ -273,7 +281,11 @@ export async function checkManifest(manifest, opts) {
|
|
|
273
281
|
warn(`${at}.collection`, `${who}: no collection "${b.collection}" in the schema — Mapled marks it outdated.`);
|
|
274
282
|
else if (b.field) {
|
|
275
283
|
const field = collection.fields.get(b.field);
|
|
276
|
-
|
|
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) {
|
|
277
289
|
warn(`${at}.field`, `${who}: no field "${b.field}" in "${collection.displayName}" — Mapled marks it outdated.`);
|
|
278
290
|
}
|
|
279
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
|
|
@@ -104,6 +109,9 @@ function describeType(f, names) {
|
|
|
104
109
|
if (f.type === "group" && "group" in f && f.group) {
|
|
105
110
|
return `group of ${f.group.fields.length} field${f.group.fields.length === 1 ? "" : "s"}${f.group.repeatable ? ", repeatable" : ""}`;
|
|
106
111
|
}
|
|
112
|
+
if (f.type === "computed" && "computed" in f && f.computed) {
|
|
113
|
+
return `computed (${f.computed.resultType === "datetime" ? "date and time" : f.computed.resultType})`;
|
|
114
|
+
}
|
|
107
115
|
return typeLabel(f.type);
|
|
108
116
|
}
|
|
109
117
|
function diffCommon(before, after, names, push) {
|
|
@@ -140,7 +148,10 @@ function sameJson(a, b) {
|
|
|
140
148
|
}
|
|
141
149
|
/** What changed between two schemas, judged for a site that reads the
|
|
142
150
|
content: anything that can make a read fail or a value disappear is
|
|
143
|
-
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. */
|
|
144
155
|
export function diffSchema(before, after) {
|
|
145
156
|
const changes = [];
|
|
146
157
|
const names = new Map();
|
|
@@ -168,13 +179,21 @@ export function diffSchema(before, after) {
|
|
|
168
179
|
col(`name ${q(b.displayName)} → ${q(a.displayName)}`, "safe");
|
|
169
180
|
const afterFields = new Map(a.fields.map((f) => [f.key, f]));
|
|
170
181
|
const beforeFields = new Map(b.fields.map((f) => [f.key, f]));
|
|
182
|
+
const renamedTo = new Set();
|
|
171
183
|
for (const bf of b.fields) {
|
|
172
|
-
|
|
184
|
+
let af = afterFields.get(bf.key);
|
|
173
185
|
if (!af) {
|
|
174
|
-
|
|
175
|
-
|
|
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;
|
|
176
194
|
}
|
|
177
|
-
const
|
|
195
|
+
const under = af.key;
|
|
196
|
+
const push = (op, detail, severity) => changes.push({ ...base, scope: "field", field: under, op, detail, severity });
|
|
178
197
|
diffCommon(bf, af, names, push);
|
|
179
198
|
if (bf.type === "relation" && af.type === "relation" && bf.relation && af.relation) {
|
|
180
199
|
if (bf.relation.target !== af.relation.target) {
|
|
@@ -186,6 +205,14 @@ export function diffSchema(before, after) {
|
|
|
186
205
|
push("changed", `on delete ${bf.relation.onDelete ?? "default"} → ${af.relation.onDelete ?? "default"}`, "safe");
|
|
187
206
|
}
|
|
188
207
|
}
|
|
208
|
+
if (bf.type === "computed" && af.type === "computed" && bf.computed && af.computed) {
|
|
209
|
+
if (bf.computed.resultType !== af.computed.resultType) {
|
|
210
|
+
push("changed", `${describeType(bf, names)} → ${describeType(af, names)}`, "breaking");
|
|
211
|
+
}
|
|
212
|
+
else if (bf.computed.expression !== af.computed.expression) {
|
|
213
|
+
push("changed", "formula changed", "safe");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
189
216
|
if (bf.type === "group" && af.type === "group" && bf.group && af.group) {
|
|
190
217
|
if (bf.group.repeatable !== af.group.repeatable) {
|
|
191
218
|
push("changed", bf.group.repeatable ? "repeatable → single item" : "single item → repeatable", "breaking");
|
|
@@ -198,7 +225,7 @@ export function diffSchema(before, after) {
|
|
|
198
225
|
const beforeSubs = new Map(bf.group.fields.map((s) => [s.key, s]));
|
|
199
226
|
for (const bs of bf.group.fields) {
|
|
200
227
|
const as = afterSubs.get(bs.key);
|
|
201
|
-
const sub = `${
|
|
228
|
+
const sub = `${under}.${bs.key}`;
|
|
202
229
|
if (!as) {
|
|
203
230
|
changes.push({ ...base, scope: "subfield", field: sub, op: "removed", detail: `removed — ${describeType(bs, names)}`, severity: "breaking" });
|
|
204
231
|
continue;
|
|
@@ -211,7 +238,7 @@ export function diffSchema(before, after) {
|
|
|
211
238
|
changes.push({
|
|
212
239
|
...base,
|
|
213
240
|
scope: "subfield",
|
|
214
|
-
field: `${
|
|
241
|
+
field: `${under}.${as.key}`,
|
|
215
242
|
op: "added",
|
|
216
243
|
detail: `added — ${describeType(as, names)}, ${as.required ? "required" : "optional"}`,
|
|
217
244
|
severity: "safe",
|
|
@@ -224,7 +251,7 @@ export function diffSchema(before, after) {
|
|
|
224
251
|
push("changed", "default value changed", "safe");
|
|
225
252
|
}
|
|
226
253
|
for (const af of a.fields) {
|
|
227
|
-
if (beforeFields.has(af.key))
|
|
254
|
+
if (beforeFields.has(af.key) || renamedTo.has(af.key))
|
|
228
255
|
continue;
|
|
229
256
|
changes.push({
|
|
230
257
|
...base,
|
|
@@ -250,7 +277,7 @@ export function diffSchema(before, after) {
|
|
|
250
277
|
}
|
|
251
278
|
return changes;
|
|
252
279
|
}
|
|
253
|
-
const GLYPH = { added: "+", removed: "-", changed: "~" };
|
|
280
|
+
const GLYPH = { added: "+", removed: "-", changed: "~", renamed: ">" };
|
|
254
281
|
/** The diff as terminal lines: one heading per collection, one line per
|
|
255
282
|
change, the severity in the last column. */
|
|
256
283
|
export function formatChanges(changes, paint = (_, t) => t) {
|
package/dist/schema.d.ts
CHANGED
|
@@ -31,6 +31,16 @@ export type SchemaField = {
|
|
|
31
31
|
/** Rules and default (§33) — kept in the schema pin, not read by the generator. */
|
|
32
32
|
validation?: unknown;
|
|
33
33
|
defaultValue?: unknown;
|
|
34
|
+
/** Computed fields (§14.9): the formula and what it produces — the
|
|
35
|
+
site reads the value as a field of the result type. */
|
|
36
|
+
computed?: {
|
|
37
|
+
expression: string;
|
|
38
|
+
resultType: string;
|
|
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[];
|
|
34
44
|
};
|
|
35
45
|
export type SchemaCollection = {
|
|
36
46
|
key: string;
|
package/dist/schema.js
CHANGED
|
@@ -29,6 +29,8 @@ export function schemaHash(schema) {
|
|
|
29
29
|
})),
|
|
30
30
|
}
|
|
31
31
|
: null,
|
|
32
|
+
// present only for computed fields, so schemas without formulas hash as before
|
|
33
|
+
...(f.type === "computed" && f.computed ? { computed: { expression: f.computed.expression, resultType: f.computed.resultType } } : {}),
|
|
32
34
|
})),
|
|
33
35
|
}));
|
|
34
36
|
return createHash("sha256").update(JSON.stringify(projection)).digest("hex").slice(0, 12);
|
package/dist/types.js
CHANGED
|
@@ -94,6 +94,19 @@ function scalarType(type, options) {
|
|
|
94
94
|
return { type: "unknown", note: type };
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
+
/** The field type a computed field is delivered as (§14.9). */
|
|
98
|
+
const COMPUTED_AS = {
|
|
99
|
+
number: "number",
|
|
100
|
+
text: "short_text",
|
|
101
|
+
boolean: "boolean",
|
|
102
|
+
date: "date",
|
|
103
|
+
datetime: "datetime",
|
|
104
|
+
};
|
|
105
|
+
/** What the site receives for a computed field: its result type, read-only. */
|
|
106
|
+
function computedType(resultType) {
|
|
107
|
+
const base = scalarType(COMPUTED_AS[resultType] ?? "", null);
|
|
108
|
+
return { type: base.type, note: `computed, ${base.note}` };
|
|
109
|
+
}
|
|
97
110
|
function fieldLine(key, displayName, required, rendered) {
|
|
98
111
|
const doc = ` /** ${comment(displayName)} — ${rendered.note} */`;
|
|
99
112
|
const line = required ? ` ${prop(key)}: ${rendered.type};` : ` ${prop(key)}?: ${rendered.type} | null;`;
|
|
@@ -102,14 +115,26 @@ function fieldLine(key, displayName, required, rendered) {
|
|
|
102
115
|
function renderSubField(sub) {
|
|
103
116
|
return fieldLine(sub.key, sub.displayName, sub.required, scalarType(sub.type, sub.options));
|
|
104
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. */
|
|
105
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) {
|
|
106
131
|
if (field.type === "relation" && field.relation) {
|
|
107
132
|
const target = names.get(field.relation.target) ?? field.relation.target;
|
|
108
133
|
const many = field.relation.cardinality === "many";
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
note: `relation to ${comment(target)}${many ? " (many)" : ""}
|
|
112
|
-
}
|
|
134
|
+
return {
|
|
135
|
+
required: field.required,
|
|
136
|
+
rendered: { type: many ? "MapledRecordId[]" : "MapledRecordId", note: `relation to ${comment(target)}${many ? " (many)" : ""}` },
|
|
137
|
+
};
|
|
113
138
|
}
|
|
114
139
|
if (field.type === "group" && field.group) {
|
|
115
140
|
const itemName = `${owner}${pascal(wordsOf(field.key))}${field.group.repeatable ? "Item" : ""}`;
|
|
@@ -123,12 +148,12 @@ function renderField(field, owner, names, extras) {
|
|
|
123
148
|
const note = field.group.repeatable
|
|
124
149
|
? `group${field.group.maxItems ? `, up to ${field.group.maxItems} items` : ""}`
|
|
125
150
|
: "group";
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
151
|
+
return { required: field.required, rendered: { type: field.group.repeatable ? `${itemName}[]` : itemName, note } };
|
|
152
|
+
}
|
|
153
|
+
if (field.type === "computed" && field.computed) {
|
|
154
|
+
return { required: false, rendered: computedType(field.computed.resultType) };
|
|
130
155
|
}
|
|
131
|
-
return
|
|
156
|
+
return { required: field.required, rendered: scalarType(field.type, field.options) };
|
|
132
157
|
}
|
|
133
158
|
/** Unique type names: the singular PascalCase of the key; the raw
|
|
134
159
|
PascalCase when two keys meet there; a numeric suffix as a last resort. */
|
package/package.json
CHANGED