dropsh 0.6.0 → 0.6.1

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 CHANGED
@@ -295,17 +295,170 @@ exit codes.
295
295
  ```bash
296
296
  dropsh read <entity_type>/<bundle>/<uuid> [--include=<field>,…]
297
297
  dropsh search <entity_type> [--bundle=<b>] [--filter=key:value]… [--limit=N] [--include=<field>,…]
298
- dropsh create <entity_type> --bundle=<b> --data=<json|@file> [--dry-run] [--no-validate]
299
- dropsh update <entity_type>/<bundle>/<uuid> --data=<json|@file> [--dry-run] [--no-validate]
298
+ dropsh create <entity_type> --bundle=<b> (--data=<json|@file> | <field parameters>) [--dry-run] [--no-validate]
299
+ dropsh update <entity_type>/<bundle>/<uuid> (--data=<json|@file> | <field parameters>) [--dry-run] [--no-validate]
300
300
  dropsh delete <entity_type>/<bundle>/<uuid> [--dry-run]
301
301
  dropsh upload-file --target=<entity_type>/<bundle>/<uuid>/<field> --file=<path> [--dry-run]
302
302
  dropsh schema [--refresh]
303
- dropsh schema <entity_type>/<bundle> [--for=create|update] [--refresh]
303
+ dropsh schema <entity_type>/<bundle> [--for=create|update] [--refresh] [--fields]
304
304
  ```
305
305
 
306
306
  `create` and `update` validate payloads against the current schema by default.
307
307
  Pass `--no-validate` when you intentionally want to skip local validation.
308
308
 
309
+ ### Field parameters on `create` / `update`
310
+
311
+ Instead of handing `create`/`update` a finished JSON:API document via `--data`,
312
+ you can name the fields directly. The CLI reads the bundle's operation schema and
313
+ builds the document itself, so it knows which name is an attribute and which is a
314
+ relationship:
315
+
316
+ | Form | Meaning |
317
+ |---|---|
318
+ | `--<field> <value>` | set a field |
319
+ | `--<field>=<value>` | same; required when the value itself starts with `--` |
320
+ | `--<field>.<sub> <value>` | set a sub-property, e.g. `--body.value "Text"` |
321
+ | `--<relationship> <uuid>` | reference by UUID; repeat for a multi-valued field |
322
+ | `--json <field>=<json>` | raw JSON value, for arrays of objects |
323
+
324
+ Any number of fields goes into a single invocation; the `--<field>=<value>` pair
325
+ form and the spaced form are interchangeable and produce identical output.
326
+
327
+ **`--fields` lists the bundle's actual fields.** `--help` documents the forms;
328
+ `--fields` resolves the bundle's schema and prints every settable parameter,
329
+ then exits without sending anything:
330
+
331
+ ```bash
332
+ dropsh schema node/article_test --fields # the introspection route
333
+ dropsh create node --bundle=article_test --fields # same listing, on the command
334
+ ```
335
+
336
+ ```
337
+ node--article_test — field parameters
338
+
339
+ Attributes:
340
+ --drupal_internal__nid <value> integer ID
341
+ --drupal_internal__vid <value> integer Revision ID
342
+ --langcode.value <value> string Language code
343
+ --langcode.language <value> Language object
344
+ --revision_timestamp <value> number Revision create time
345
+ --revision_log <value> string Revision log message
346
+ --status [true|false] boolean Published
347
+ --title <value> string required Title
348
+ --created <value> number Authored on
349
+ --changed <value> number Changed
350
+ --promote [true|false] boolean Promoted to front page
351
+ --sticky [true|false] boolean Sticky at top of lists
352
+ --default_langcode [true|false] boolean Default translation
353
+ --revision_default [true|false] boolean Default revision
354
+ --revision_translation_affected [true|false] boolean Revision translation affected
355
+ --path.alias <value> string Path alias
356
+ --path.pid <value> integer Path id
357
+ --path.langcode <value> string Language Code
358
+ --body.value <value> string Text
359
+ --body.format <value> string Text format
360
+ --body.summary <value> string Summary
361
+ --field_test_text <value> string Test Text
362
+
363
+ Relationships (take a UUID):
364
+ --node_type <uuid> node_type--node_type Content type
365
+ --revision_uid <uuid> user--user Revision user
366
+ --uid <uuid> user--user Authored by
367
+ --field_image <uuid> file--file Image
368
+ ```
369
+
370
+ The listing is the schema's, not a curated subset: Drupal's base fields
371
+ (`--drupal_internal__nid`, `--langcode.*`, `--path.*`, `--node_type`,
372
+ `--revision_uid`, …) are settable parameters and appear alongside the
373
+ bundle's own fields.
374
+
375
+ Each row carries the schema type, whether the field is required, and the field's
376
+ human label. Object attributes appear as their dotted leaves (`--body.value`,
377
+ never `--body`), array attributes as their `--json` form, and a relationship that
378
+ allows several target types is spelled `<type>:<uuid>`. The same listing is reachable three ways, all derived from the schema alone with
379
+ no extra request: `dropsh schema <entity>/<bundle> --fields` (honouring `--for`
380
+ and `--refresh`), and `--fields` on `create` and `update`.
381
+
382
+ Add an explicit `--format json` to get the same listing as JSON — `parameter`,
383
+ `path`, `type`, `label`, `required`, `bare_flag` per attribute, and `targets`,
384
+ `multiple`, `requires_type_prefix` per relationship — for a caller that wants to
385
+ consume it rather than read it.
386
+
387
+ Rules worth knowing:
388
+
389
+ - **Field names come from the schema** (`dropsh schema <entity>/<bundle>`), so an
390
+ unknown name is **rejected** with exit code `4` instead of being ignored. The
391
+ error names the rejected parameter and lists the known attributes and
392
+ relationships.
393
+ - **Relationships take a bare UUID**; the resource type comes from the schema.
394
+ When a relationship allows several target types, pass `<type>:<uuid>`.
395
+ Repeating the flag accumulates values on a multi-valued relationship; a second
396
+ value on a single-valued one is an error.
397
+ - **Values are typed by the schema** — `boolean`, `integer`/`number`, else
398
+ string. A bare `--flag` means `true` only on a boolean field. Values are never
399
+ comma-split, so `--title "a, b"` survives intact.
400
+ - **`--data` and field parameters are mutually exclusive**; giving neither is an
401
+ error.
402
+ - **`--dry-run` prints the generated document** and sends nothing. Output keys are
403
+ emitted in schema declaration order, so two dry-runs of the same fields are
404
+ byte-identical and diffable.
405
+ - **`--no-validate` skips Ajv, not the schema.** In parameter mode the schema is
406
+ always loaded — attribute/relationship placement is impossible without it.
407
+ - A field whose name collides with a reserved option (`--bundle`, `--data`,
408
+ `--dry-run`, `--no-validate`, `--fields`, `--format`, `--auth-profile`,
409
+ `--config`, `--view-mode`, `--json`) is out of reach as a bare `--<field>`, but
410
+ `--json <field>=<json>` still sets it (write the value as JSON), and so does
411
+ `--data`. No Drupal base field name collides with that list in practice.
412
+ - Fields contributed by a plugin's `extendOperationSchema` hook are usable as
413
+ parameters like any native field.
414
+
415
+ Worked example:
416
+
417
+ ```bash
418
+ dropsh create node --bundle=article_test \
419
+ --title "Hello" --body.value "Text" \
420
+ --uid c8e0b5d2-9e66-4caf-ac43-337f3b3af62d --dry-run | jq
421
+ ```
422
+
423
+ ```json
424
+ {
425
+ "dry_run": true,
426
+ "method": "POST",
427
+ "path": "node/article_test",
428
+ "payload": {
429
+ "data": {
430
+ "type": "node--article_test",
431
+ "attributes": {
432
+ "title": "Hello",
433
+ "body": {
434
+ "value": "Text"
435
+ }
436
+ },
437
+ "relationships": {
438
+ "uid": {
439
+ "data": {
440
+ "type": "user--user",
441
+ "id": "c8e0b5d2-9e66-4caf-ac43-337f3b3af62d"
442
+ }
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ ```
449
+
450
+ `update` works the same way and puts the target UUID into `data.id`, sending a
451
+ PATCH with only the fields you named:
452
+
453
+ ```bash
454
+ dropsh update node/article_test/<uuid> --title "changed title"
455
+ ```
456
+
457
+ The builder is also available programmatically — `buildPayloadFromParameters`,
458
+ `indexSchemaFields` and `propertiesOf` are exported from `dropsh/plugin`, so a
459
+ plugin can turn (schema, parameters) into the same document without invoking the
460
+ CLI.
461
+
309
462
  ### `--include` on `read` / `search`
310
463
 
311
464
  Pass `--include` to embed related resources in the response's JSON:API
@@ -2,7 +2,12 @@ import type { JsonApiClient } from "../core/jsonapi/client.js";
2
2
  export interface CreateArgs {
3
3
  entityType: string;
4
4
  bundle: string;
5
- dataArg: string;
5
+ /**
6
+ * The JSON:API document to send. Resolving it — from `--data` or from field
7
+ * parameters — happens before this command runs, so nothing here knows or
8
+ * cares which route produced it.
9
+ */
10
+ payload: unknown;
6
11
  dryRun?: boolean;
7
12
  noValidate?: boolean;
8
13
  }
@@ -1,6 +1,5 @@
1
- import { readDataArg } from "./_data.js";
2
1
  export async function runCreate(args, deps) {
3
- const payload = await readDataArg(args.dataArg);
2
+ const { payload } = args;
4
3
  const target = `${args.entityType}/${args.bundle}`;
5
4
  if (!args.noValidate && deps.validate) {
6
5
  await deps.validate(payload, target);
@@ -1,7 +1,12 @@
1
1
  import type { JsonApiClient } from "../core/jsonapi/client.js";
2
2
  export interface UpdateArgs {
3
3
  target: string;
4
- dataArg: string;
4
+ /**
5
+ * The JSON:API document to send. Resolving it — from `--data` or from field
6
+ * parameters — happens before this command runs, so nothing here knows or
7
+ * cares which route produced it.
8
+ */
9
+ payload: unknown;
5
10
  dryRun?: boolean;
6
11
  noValidate?: boolean;
7
12
  }
@@ -10,4 +15,11 @@ export interface UpdateDeps {
10
15
  emit: (v: unknown) => void | Promise<void>;
11
16
  validate?: (payload: unknown, target: string) => void | Promise<void>;
12
17
  }
18
+ /**
19
+ * Reject a malformed target. Exported so the CLI can run this guard *before* it
20
+ * builds a payload from field parameters: without it, a bad target would surface
21
+ * as the builder's "update requires the entity id" instead of this clearer
22
+ * message.
23
+ */
24
+ export declare function assertUpdateTarget(target: string): void;
13
25
  export declare function runUpdate(args: UpdateArgs, deps: UpdateDeps): Promise<void>;
@@ -1,11 +1,19 @@
1
1
  import { ValidationError } from "../errors.js";
2
- import { readDataArg } from "./_data.js";
3
2
  const TARGET_RE = /^[a-z0-9_]+\/[a-z0-9_]+\/[a-f0-9-]{8,}$/;
4
- export async function runUpdate(args, deps) {
5
- if (!TARGET_RE.test(args.target)) {
6
- throw new ValidationError(`target must be <entity_type>/<bundle>/<uuid>, got "${args.target}"`);
3
+ /**
4
+ * Reject a malformed target. Exported so the CLI can run this guard *before* it
5
+ * builds a payload from field parameters: without it, a bad target would surface
6
+ * as the builder's "update requires the entity id" instead of this clearer
7
+ * message.
8
+ */
9
+ export function assertUpdateTarget(target) {
10
+ if (!TARGET_RE.test(target)) {
11
+ throw new ValidationError(`target must be <entity_type>/<bundle>/<uuid>, got "${target}"`);
7
12
  }
8
- const payload = await readDataArg(args.dataArg);
13
+ }
14
+ export async function runUpdate(args, deps) {
15
+ assertUpdateTarget(args.target);
16
+ const { payload } = args;
9
17
  const [entity, bundle] = args.target.split("/", 3);
10
18
  const schemaTarget = `${entity}/${bundle}`;
11
19
  if (!args.noValidate && deps.validate) {
@@ -0,0 +1,12 @@
1
+ import type { RawParameter } from "./parse-args.js";
2
+ export interface BuildPayloadInput {
3
+ /** The resolved operation schema, plugin extensions already applied. */
4
+ schema: unknown;
5
+ parameters: RawParameter[];
6
+ operation: "create" | "update";
7
+ /** Required for `update`: becomes `data.id`. */
8
+ id?: string;
9
+ /** Fallback resource type when the schema carries no `type` const/enum. */
10
+ resourceType?: string;
11
+ }
12
+ export declare function buildPayloadFromParameters(input: BuildPayloadInput): unknown;
@@ -0,0 +1,209 @@
1
+ import { ValidationError } from "../../errors.js";
2
+ import { indexSchemaFields, propertiesOf, } from "./schema-fields.js";
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ function schemaTypeOf(node) {
7
+ if (!isRecord(node))
8
+ return undefined;
9
+ const t = node.type;
10
+ if (typeof t === "string")
11
+ return t;
12
+ if (Array.isArray(t))
13
+ return t.find((x) => typeof x === "string" && x !== "null");
14
+ return undefined;
15
+ }
16
+ /**
17
+ * Convert one command-line string into the value the leaf schema node asks for.
18
+ * A non-convertible value is rejected here rather than left for Ajv, because
19
+ * `--no-validate` must not turn a wrong type into a silently wrong payload.
20
+ */
21
+ function coerce(node, param, label) {
22
+ const type = schemaTypeOf(node);
23
+ if (!param.hasValue) {
24
+ if (type === "boolean")
25
+ return true;
26
+ throw new ValidationError(`missing value for --${label}${type !== undefined ? ` (expected ${type})` : ""}`);
27
+ }
28
+ if (type === "boolean") {
29
+ if (param.value === "true")
30
+ return true;
31
+ if (param.value === "false")
32
+ return false;
33
+ throw new ValidationError(`--${label} expects true or false, got "${param.value}"`);
34
+ }
35
+ if (type === "integer" || type === "number") {
36
+ const n = Number(param.value);
37
+ if (param.value.trim() === "" || !Number.isFinite(n)) {
38
+ throw new ValidationError(`--${label} expects ${type}, got "${param.value}"`);
39
+ }
40
+ if (type === "integer" && !Number.isInteger(n)) {
41
+ throw new ValidationError(`--${label} expects integer, got "${param.value}"`);
42
+ }
43
+ return n;
44
+ }
45
+ return param.value;
46
+ }
47
+ function parseJsonValue(param) {
48
+ try {
49
+ return JSON.parse(param.value);
50
+ }
51
+ catch (err) {
52
+ throw new ValidationError(`--json ${param.path} is not valid JSON: ${err.message}`);
53
+ }
54
+ }
55
+ /**
56
+ * Re-emit an object with its keys in the order its subschema declares them, so
57
+ * the same fields always serialise to the same bytes.
58
+ */
59
+ function ordered(node, value) {
60
+ if (Array.isArray(value)) {
61
+ const items = isRecord(node) ? node.items : undefined;
62
+ return value.map((entry) => ordered(items, entry));
63
+ }
64
+ if (!isRecord(value))
65
+ return value;
66
+ const props = propertiesOf(node);
67
+ const keys = Object.keys(value);
68
+ const declared = props ? Object.keys(props).filter((k) => keys.includes(k)) : [];
69
+ const extra = keys.filter((k) => !declared.includes(k)).sort();
70
+ const out = {};
71
+ for (const key of [...declared, ...extra]) {
72
+ out[key] = ordered(props?.[key], value[key]);
73
+ }
74
+ return out;
75
+ }
76
+ /**
77
+ * Walk a dotted path into a field's subschema, rejecting a segment the schema
78
+ * does not declare. Returns the leaf node the value must satisfy.
79
+ */
80
+ function resolveLeaf(field, segments) {
81
+ let node = field.node;
82
+ for (const segment of segments) {
83
+ const props = propertiesOf(node);
84
+ const next = props?.[segment];
85
+ if (next === undefined) {
86
+ throw new ValidationError(`unknown sub-property '${segment}' of '${field.name}'${props ? `; known: ${Object.keys(props).join(", ")}` : ""}`);
87
+ }
88
+ node = next;
89
+ }
90
+ return node;
91
+ }
92
+ function assign(target, segments, value, label) {
93
+ let cursor = target;
94
+ for (const segment of segments.slice(0, -1)) {
95
+ const existing = cursor[segment];
96
+ if (existing === undefined) {
97
+ const created = {};
98
+ cursor[segment] = created;
99
+ cursor = created;
100
+ }
101
+ else if (isRecord(existing)) {
102
+ cursor = existing;
103
+ }
104
+ else {
105
+ throw new ValidationError(`parameter '${label}' conflicts with an earlier value`);
106
+ }
107
+ }
108
+ const leaf = segments[segments.length - 1];
109
+ if (leaf in cursor) {
110
+ throw new ValidationError(`parameter '${label}' was given more than once`);
111
+ }
112
+ cursor[leaf] = value;
113
+ }
114
+ function unknownParameter(name, index) {
115
+ const attributes = [];
116
+ const relationships = [];
117
+ for (const field of index.fields.values()) {
118
+ (field.kind === "attribute" ? attributes : relationships).push(field.name);
119
+ }
120
+ throw new ValidationError(`unknown parameter '${name}'. Known attributes: ${attributes.join(", ") || "(none)"}. ` +
121
+ `Known relationships: ${relationships.join(", ") || "(none)"}.`);
122
+ }
123
+ /**
124
+ * A relationship parameter is a bare UUID whenever the schema allows exactly one
125
+ * target type, and `<type>:<uuid>` when it allows several. The UUID itself never
126
+ * contains a colon, so the split is unambiguous.
127
+ */
128
+ function resourceIdentifier(field, param) {
129
+ if (!param.hasValue) {
130
+ throw new ValidationError(`missing value for --${field.name} (expected a UUID)`);
131
+ }
132
+ const allowed = field.targetTypes ?? [];
133
+ const colon = param.value.lastIndexOf(":");
134
+ if (colon > 0) {
135
+ const type = param.value.slice(0, colon);
136
+ const id = param.value.slice(colon + 1);
137
+ if (allowed.length > 0 && !allowed.includes(type)) {
138
+ throw new ValidationError(`'${type}' is not an allowed target type for '${field.name}'; allowed: ${allowed.join(", ")}`);
139
+ }
140
+ return { type, id };
141
+ }
142
+ if (allowed.length === 0) {
143
+ throw new ValidationError(`relationship '${field.name}' declares no target type in the schema; pass <type>:<uuid>`);
144
+ }
145
+ if (allowed.length > 1) {
146
+ throw new ValidationError(`relationship '${field.name}' allows several target types (${allowed.join(", ")}); ` +
147
+ "pass <type>:<uuid>");
148
+ }
149
+ return { type: allowed[0], id: param.value };
150
+ }
151
+ export function buildPayloadFromParameters(input) {
152
+ const index = indexSchemaFields(input.schema);
153
+ const attributes = {};
154
+ const relationships = new Map();
155
+ for (const param of input.parameters) {
156
+ const segments = param.path.split(".");
157
+ const name = segments[0];
158
+ const field = index.fields.get(name);
159
+ if (!field)
160
+ unknownParameter(name, index);
161
+ if (field.kind === "relationship") {
162
+ if (segments.length > 1) {
163
+ throw new ValidationError(`relationship '${name}' takes a UUID, not a sub-path ('${param.path}')`);
164
+ }
165
+ const collected = relationships.get(name) ?? [];
166
+ if (collected.length > 0 && field.multiple !== true) {
167
+ throw new ValidationError(`relationship '${name}' accepts a single value; it was given more than once`);
168
+ }
169
+ collected.push(resourceIdentifier(field, param));
170
+ relationships.set(name, collected);
171
+ continue;
172
+ }
173
+ const tail = segments.slice(1);
174
+ const leaf = resolveLeaf(field, tail);
175
+ const value = param.form === "json"
176
+ ? ordered(leaf, parseJsonValue(param))
177
+ : coerce(leaf, param, param.path);
178
+ assign(attributes, segments, value, param.path);
179
+ }
180
+ const resourceType = index.resourceType ?? input.resourceType;
181
+ if (resourceType === undefined) {
182
+ throw new ValidationError("cannot determine the JSON:API resource type: the schema declares no type and no fallback was given");
183
+ }
184
+ const data = { type: resourceType };
185
+ if (input.operation === "update") {
186
+ if (input.id === undefined || input.id === "") {
187
+ throw new ValidationError("update requires the entity id for data.id");
188
+ }
189
+ data.id = input.id;
190
+ }
191
+ const attributeNode = propertiesOf(propertiesOf(input.schema)?.data)?.attributes;
192
+ if (Object.keys(attributes).length > 0) {
193
+ data.attributes = ordered(attributeNode, attributes);
194
+ }
195
+ if (relationships.size > 0) {
196
+ const out = {};
197
+ for (const name of index.order) {
198
+ const collected = relationships.get(name);
199
+ if (!collected)
200
+ continue;
201
+ const field = index.fields.get(name);
202
+ out[name] = {
203
+ data: field.multiple === true ? collected : collected[0],
204
+ };
205
+ }
206
+ data.relationships = out;
207
+ }
208
+ return { data };
209
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The `--help` block for the write commands. It documents the *forms* only and
3
+ * points at `--fields` for the bundle's actual field names: Commander renders
4
+ * help synchronously, so it cannot resolve a schema, while `--fields` runs in
5
+ * the action and goes through the normal fetch-and-cache pipeline.
6
+ */
7
+ export declare const FIELD_PARAMETER_HELP = "\nField parameters (instead of --data):\n --<field> <value> set a field, e.g. --title \"Hello\"\n --<field>=<value> same; also the form for many fields in one call\n --<field>.<sub> <value> set a sub-property, e.g. --body.value \"Text\"\n --<relationship> <uuid> reference by UUID; repeat for a multi-valued field\n --json <field>=<json> raw JSON value, for arrays of objects\n\nAdd --fields to list this bundle's actual field parameters and exit without\nsending anything. An unknown field name is rejected, never ignored.\n\nA value starting with -- needs the --<field>=<value> form. A field whose name\ncollides with a reserved option (--bundle, --data, --dry-run, --no-validate,\n--fields, --format, --auth-profile, --config, --view-mode, --json) is reachable\nthrough --json <field>=<json> or --data, never as a bare --<field>.\n\nExample:\n dropsh create node --bundle article --fields\n dropsh create node --bundle article --title \"Hello\" --body.value \"Text\" --dry-run";
8
+ export interface AttributeDescription {
9
+ /** The complete command-line spelling, value placeholder included. */
10
+ parameter: string;
11
+ /** Dotted path into `data.attributes`. */
12
+ path: string;
13
+ type?: string;
14
+ /** The field's human label, from the schema's `title`. */
15
+ label?: string;
16
+ /** Present only when the schema requires the field. */
17
+ required?: true;
18
+ /** Present only when the field can be given as a bare `--flag`. */
19
+ bare_flag?: true;
20
+ }
21
+ export interface RelationshipDescription {
22
+ parameter: string;
23
+ path: string;
24
+ targets: string[];
25
+ multiple: boolean;
26
+ /** The relationship's human label, from the schema's `title`. */
27
+ label?: string;
28
+ /** Present only when several target types make `<type>:<uuid>` mandatory. */
29
+ requires_type_prefix?: true;
30
+ }
31
+ export interface FieldsDescription {
32
+ type?: string;
33
+ attributes: AttributeDescription[];
34
+ relationships: RelationshipDescription[];
35
+ }
36
+ /**
37
+ * Turn a resolved operation schema into the machine-readable field listing that
38
+ * `--fields` prints: every settable parameter, in schema declaration order.
39
+ */
40
+ export declare function describeFields(schema: unknown): FieldsDescription;
41
+ /**
42
+ * Render the field listing as an aligned, human-readable table — what `--fields`
43
+ * prints by default. `--format json` yields `describeFields` verbatim instead,
44
+ * for a caller that wants to consume it.
45
+ */
46
+ export declare function renderFieldsTable(described: FieldsDescription): string;
@@ -0,0 +1,148 @@
1
+ import { indexSchemaFields, propertiesOf } from "./schema-fields.js";
2
+ /**
3
+ * The `--help` block for the write commands. It documents the *forms* only and
4
+ * points at `--fields` for the bundle's actual field names: Commander renders
5
+ * help synchronously, so it cannot resolve a schema, while `--fields` runs in
6
+ * the action and goes through the normal fetch-and-cache pipeline.
7
+ */
8
+ export const FIELD_PARAMETER_HELP = `
9
+ Field parameters (instead of --data):
10
+ --<field> <value> set a field, e.g. --title "Hello"
11
+ --<field>=<value> same; also the form for many fields in one call
12
+ --<field>.<sub> <value> set a sub-property, e.g. --body.value "Text"
13
+ --<relationship> <uuid> reference by UUID; repeat for a multi-valued field
14
+ --json <field>=<json> raw JSON value, for arrays of objects
15
+
16
+ Add --fields to list this bundle's actual field parameters and exit without
17
+ sending anything. An unknown field name is rejected, never ignored.
18
+
19
+ A value starting with -- needs the --<field>=<value> form. A field whose name
20
+ collides with a reserved option (--bundle, --data, --dry-run, --no-validate,
21
+ --fields, --format, --auth-profile, --config, --view-mode, --json) is reachable
22
+ through --json <field>=<json> or --data, never as a bare --<field>.
23
+
24
+ Example:
25
+ dropsh create node --bundle article --fields
26
+ dropsh create node --bundle article --title "Hello" --body.value "Text" --dry-run`;
27
+ function isRecord(value) {
28
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29
+ }
30
+ function schemaTypeOf(node) {
31
+ if (!isRecord(node))
32
+ return undefined;
33
+ const t = node.type;
34
+ if (typeof t === "string")
35
+ return t;
36
+ if (Array.isArray(t))
37
+ return t.find((x) => typeof x === "string" && x !== "null");
38
+ return undefined;
39
+ }
40
+ function labelOf(node) {
41
+ if (!isRecord(node))
42
+ return undefined;
43
+ return typeof node.title === "string" ? node.title : undefined;
44
+ }
45
+ /** The command-line spelling, including how the value is written. */
46
+ function spell(path, type) {
47
+ if (type === "array")
48
+ return `--json ${path}=<json>`;
49
+ if (type === "boolean")
50
+ return `--${path} [true|false]`;
51
+ return `--${path} <value>`;
52
+ }
53
+ function describeAttribute(path, node, required) {
54
+ const type = schemaTypeOf(node);
55
+ const label = labelOf(node);
56
+ return {
57
+ parameter: spell(path, type),
58
+ path,
59
+ ...(type !== undefined ? { type } : {}),
60
+ ...(label !== undefined ? { label } : {}),
61
+ ...(required ? { required: true } : {}),
62
+ ...(type === "boolean" ? { bare_flag: true } : {}),
63
+ };
64
+ }
65
+ /** An object attribute is settable only through its leaves, so recurse into them. */
66
+ function describeAttributeTree(path, node, required) {
67
+ const props = schemaTypeOf(node) === "array" ? undefined : propertiesOf(node);
68
+ if (!props || Object.keys(props).length === 0) {
69
+ return [describeAttribute(path, node, required)];
70
+ }
71
+ const req = isRecord(node) && Array.isArray(node.required) ? node.required : [];
72
+ return Object.entries(props).flatMap(([sub, subNode]) => describeAttributeTree(`${path}.${sub}`, subNode, required && req.includes(sub)));
73
+ }
74
+ function describeRelationship(field) {
75
+ const targets = field.targetTypes ?? [];
76
+ const ambiguous = targets.length !== 1;
77
+ const label = labelOf(field.node);
78
+ return {
79
+ parameter: `--${field.name} ${ambiguous ? "<type>:<uuid>" : "<uuid>"}`,
80
+ path: field.name,
81
+ targets,
82
+ multiple: field.multiple === true,
83
+ ...(label !== undefined ? { label } : {}),
84
+ ...(ambiguous ? { requires_type_prefix: true } : {}),
85
+ };
86
+ }
87
+ /**
88
+ * Turn a resolved operation schema into the machine-readable field listing that
89
+ * `--fields` prints: every settable parameter, in schema declaration order.
90
+ */
91
+ export function describeFields(schema) {
92
+ const index = indexSchemaFields(schema);
93
+ const attributeNode = propertiesOf(propertiesOf(schema)?.data)?.attributes;
94
+ const requiredNames = isRecord(attributeNode) && Array.isArray(attributeNode.required) ? attributeNode.required : [];
95
+ const attributes = [];
96
+ const relationships = [];
97
+ for (const name of index.order) {
98
+ const field = index.fields.get(name);
99
+ if (!field)
100
+ continue;
101
+ if (field.kind === "attribute") {
102
+ attributes.push(...describeAttributeTree(name, field.node, requiredNames.includes(name)));
103
+ }
104
+ else
105
+ relationships.push(describeRelationship(field));
106
+ }
107
+ return {
108
+ ...(index.resourceType !== undefined ? { type: index.resourceType } : {}),
109
+ attributes,
110
+ relationships,
111
+ };
112
+ }
113
+ /**
114
+ * Render the field listing as an aligned, human-readable table — what `--fields`
115
+ * prints by default. `--format json` yields `describeFields` verbatim instead,
116
+ * for a caller that wants to consume it.
117
+ */
118
+ export function renderFieldsTable(described) {
119
+ const { attributes, relationships } = described;
120
+ const head = `${described.type ?? "this bundle"} — field parameters`;
121
+ if (attributes.length === 0 && relationships.length === 0) {
122
+ return `${head}\n\nThe schema declares no field parameters for this bundle.`;
123
+ }
124
+ const rows = [];
125
+ for (const a of attributes) {
126
+ const notes = [a.type ?? "", a.required ? "required" : "", a.label ?? ""].filter((n) => n !== "");
127
+ rows.push([a.parameter, notes.join(" ")]);
128
+ }
129
+ const attributeRows = rows.length;
130
+ for (const r of relationships) {
131
+ const notes = [
132
+ r.targets.join(" | ") || "(no target type declared)",
133
+ r.multiple ? "repeatable" : "",
134
+ r.label ?? "",
135
+ ].filter((n) => n !== "");
136
+ rows.push([r.parameter, notes.join(" ")]);
137
+ }
138
+ const width = Math.max(...rows.map(([p]) => p.length)) + 2;
139
+ const line = ([p, notes]) => notes === "" ? ` ${p}` : ` ${p.padEnd(width)}${notes}`;
140
+ const out = [head];
141
+ if (attributeRows > 0) {
142
+ out.push("", "Attributes:", ...rows.slice(0, attributeRows).map(line));
143
+ }
144
+ if (relationships.length > 0) {
145
+ out.push("", "Relationships (take a UUID):", ...rows.slice(attributeRows).map(line));
146
+ }
147
+ return out.join("\n");
148
+ }
@@ -0,0 +1,18 @@
1
+ export interface RawParameter {
2
+ /** Dotted path into the payload, e.g. "title" or "body.value". */
3
+ path: string;
4
+ value: string;
5
+ /** Which surface syntax produced it; "json" means `value` is raw JSON. */
6
+ form: "flag" | "json";
7
+ /**
8
+ * False for a bare `--field` with no following value. Whether that is legal
9
+ * depends on the field's schema type, so the decision belongs to the builder.
10
+ */
11
+ hasValue: boolean;
12
+ }
13
+ /**
14
+ * Parse the token stream Commander passes through for unknown options into an
15
+ * ordered parameter list. Purely syntactic: no schema is consulted, so an
16
+ * unknown field name is not an error here.
17
+ */
18
+ export declare function parseFieldArgs(tokens: string[]): RawParameter[];
@@ -0,0 +1,75 @@
1
+ import { ValidationError } from "../../errors.js";
2
+ /**
3
+ * `--json` is the one collector: it takes `<field>=<json>` pairs rather than a
4
+ * value of its own, because a raw JSON value cannot be coerced from the leaf
5
+ * schema type. The collector name is matched before any other handling, so the
6
+ * pair's left-hand side is an arbitrary field path — including one that collides
7
+ * with a reserved option, or `json` itself. That makes `--json <field>=<json>` a
8
+ * second route to such a field besides `--data`; only the bare `--<field>` form
9
+ * is out of reach.
10
+ */
11
+ const COLLECTORS = new Set(["json"]);
12
+ function splitPair(pair, collector) {
13
+ const eq = pair.indexOf("=");
14
+ if (eq <= 0) {
15
+ throw new ValidationError(`--${collector} expects <field>=<value> pairs, got "${pair}"`);
16
+ }
17
+ return { path: pair.slice(0, eq), value: pair.slice(eq + 1) };
18
+ }
19
+ /**
20
+ * Parse the token stream Commander passes through for unknown options into an
21
+ * ordered parameter list. Purely syntactic: no schema is consulted, so an
22
+ * unknown field name is not an error here.
23
+ */
24
+ export function parseFieldArgs(tokens) {
25
+ const out = [];
26
+ let i = 0;
27
+ while (i < tokens.length) {
28
+ const token = tokens[i];
29
+ if (!token.startsWith("--")) {
30
+ throw new ValidationError(`unexpected argument "${token}"; field parameters are long options (--<field> <value>)`);
31
+ }
32
+ const body = token.slice(2);
33
+ if (body.length === 0) {
34
+ throw new ValidationError('"--" is not a field parameter');
35
+ }
36
+ const eq = body.indexOf("=");
37
+ const name = eq === -1 ? body : body.slice(0, eq);
38
+ if (COLLECTORS.has(name)) {
39
+ const pairs = [];
40
+ if (eq !== -1) {
41
+ pairs.push(body.slice(eq + 1));
42
+ i += 1;
43
+ }
44
+ else {
45
+ i += 1;
46
+ while (i < tokens.length && !tokens[i].startsWith("--")) {
47
+ pairs.push(tokens[i]);
48
+ i += 1;
49
+ }
50
+ }
51
+ if (pairs.length === 0) {
52
+ throw new ValidationError(`--${name} expects at least one <field>=<value> pair`);
53
+ }
54
+ for (const pair of pairs) {
55
+ const { path, value } = splitPair(pair, name);
56
+ out.push({ path, value, form: "json", hasValue: true });
57
+ }
58
+ continue;
59
+ }
60
+ if (eq !== -1) {
61
+ out.push({ path: name, value: body.slice(eq + 1), form: "flag", hasValue: true });
62
+ i += 1;
63
+ continue;
64
+ }
65
+ const next = tokens[i + 1];
66
+ if (next !== undefined && !next.startsWith("--")) {
67
+ out.push({ path: name, value: next, form: "flag", hasValue: true });
68
+ i += 2;
69
+ continue;
70
+ }
71
+ out.push({ path: name, value: "", form: "flag", hasValue: false });
72
+ i += 1;
73
+ }
74
+ return out;
75
+ }
@@ -0,0 +1,20 @@
1
+ export interface FieldDescriptor {
2
+ name: string;
3
+ kind: "attribute" | "relationship";
4
+ /** The field's own subschema, as declared under attributes/relationships. */
5
+ node: unknown;
6
+ /** Relationship only: the resource types its `data.type` accepts. */
7
+ targetTypes?: string[];
8
+ /** Relationship only: true when `data` is an array. */
9
+ multiple?: boolean;
10
+ }
11
+ export interface SchemaFieldIndex {
12
+ /** From `data.properties.type.const`, else the first `enum` entry. */
13
+ resourceType?: string;
14
+ fields: Map<string, FieldDescriptor>;
15
+ /** Schema declaration order: attributes first, then relationships. */
16
+ order: string[];
17
+ }
18
+ /** The `properties` map of a schema node, or undefined when it has none. */
19
+ export declare function propertiesOf(node: unknown): Record<string, unknown> | undefined;
20
+ export declare function indexSchemaFields(schema: unknown): SchemaFieldIndex;
@@ -0,0 +1,65 @@
1
+ function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ /** The `properties` map of a schema node, or undefined when it has none. */
5
+ export function propertiesOf(node) {
6
+ if (!isRecord(node))
7
+ return undefined;
8
+ const props = node.properties;
9
+ return isRecord(props) ? props : undefined;
10
+ }
11
+ function resourceTypeOf(dataNode) {
12
+ const typeNode = propertiesOf(dataNode)?.type;
13
+ if (!isRecord(typeNode))
14
+ return undefined;
15
+ if (typeof typeNode.const === "string")
16
+ return typeNode.const;
17
+ if (Array.isArray(typeNode.enum) && typeof typeNode.enum[0] === "string") {
18
+ return typeNode.enum[0];
19
+ }
20
+ return undefined;
21
+ }
22
+ /**
23
+ * A relationship declares its payload under `data`, either as a single resource
24
+ * identifier object or as an array of them. Both carry the allowed resource
25
+ * types in their `type` node's `enum`/`const`, which is what lets a bare UUID on
26
+ * the command line become a complete identifier object.
27
+ */
28
+ function describeRelationship(name, node) {
29
+ const dataNode = propertiesOf(node)?.data;
30
+ const multiple = isRecord(dataNode) && dataNode.type === "array";
31
+ const itemNode = multiple ? dataNode.items : dataNode;
32
+ const typeNode = propertiesOf(itemNode)?.type;
33
+ let targetTypes;
34
+ if (isRecord(typeNode)) {
35
+ if (Array.isArray(typeNode.enum)) {
36
+ targetTypes = typeNode.enum.filter((t) => typeof t === "string");
37
+ }
38
+ else if (typeof typeNode.const === "string") {
39
+ targetTypes = [typeNode.const];
40
+ }
41
+ }
42
+ return {
43
+ name,
44
+ kind: "relationship",
45
+ node,
46
+ multiple,
47
+ ...(targetTypes !== undefined ? { targetTypes } : {}),
48
+ };
49
+ }
50
+ export function indexSchemaFields(schema) {
51
+ const dataNode = propertiesOf(schema)?.data;
52
+ const dataProps = propertiesOf(dataNode);
53
+ const fields = new Map();
54
+ const order = [];
55
+ for (const [name, node] of Object.entries(propertiesOf(dataProps?.attributes) ?? {})) {
56
+ fields.set(name, { name, kind: "attribute", node });
57
+ order.push(name);
58
+ }
59
+ for (const [name, node] of Object.entries(propertiesOf(dataProps?.relationships) ?? {})) {
60
+ fields.set(name, describeRelationship(name, node));
61
+ order.push(name);
62
+ }
63
+ const resourceType = resourceTypeOf(dataNode);
64
+ return { fields, order, ...(resourceType !== undefined ? { resourceType } : {}) };
65
+ }
package/dist/src/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from "node:util";
3
3
  import { Command } from "commander";
4
+ import { readDataArg } from "./commands/_data.js";
4
5
  import { runAuthLogin, runAuthLogout, runAuthStatus, runAuthUse } from "./commands/auth.js";
5
6
  import { runCreate } from "./commands/create.js";
6
7
  import { runDelete } from "./commands/delete.js";
7
8
  import { runRead } from "./commands/read.js";
8
9
  import { applyOperationSchemaPlugins, operationHookPluginIds, runSchema, SCHEMA_PIPELINE_VERSION, schemaCacheMetadataMatches, siteCacheRoot, } from "./commands/schema.js";
9
10
  import { runSearch } from "./commands/search.js";
10
- import { runUpdate } from "./commands/update.js";
11
+ import { assertUpdateTarget, runUpdate, } from "./commands/update.js";
11
12
  import { runUploadFile } from "./commands/upload-file.js";
12
13
  import { collectProviders, defaultProvider, providerById, sessionlessProvider, } from "./core/auth/registry.js";
13
14
  import { readProfiles, writeProfile } from "./core/auth/session-store.js";
@@ -17,11 +18,14 @@ import { createPrompt } from "./core/cli/prompt.js";
17
18
  import { loadConfig } from "./core/config.js";
18
19
  import { createHttpClient } from "./core/http.js";
19
20
  import { createJsonApiClient } from "./core/jsonapi/client.js";
21
+ import { buildPayloadFromParameters } from "./core/params/from-parameters.js";
22
+ import { describeFields, FIELD_PARAMETER_HELP, renderFieldsTable } from "./core/params/help.js";
23
+ import { parseFieldArgs } from "./core/params/parse-args.js";
20
24
  import { composeRequestHooks } from "./core/request-hooks.js";
21
25
  import { fetchJsonSchema } from "./core/schema/jsonschema-source.js";
22
26
  import { toOperationVariant } from "./core/schema/to-jsonschema.js";
23
27
  import { validatePayload } from "./core/schema/validate.js";
24
- import { AuthError, ConfigError, exitCodeFor } from "./errors.js";
28
+ import { AuthError, ConfigError, exitCodeFor, ValidationError } from "./errors.js";
25
29
  function resolveConfigPath(override) {
26
30
  return override ?? process.env.DROPSH_CONFIG ?? "dropsh.config.js";
27
31
  }
@@ -201,6 +205,73 @@ export function buildProgram(opts = {}) {
201
205
  throw new ConfigError(`Unknown format '${f}'. Available: ${["json", ...renderers.map((r) => r.id)].join(", ")}`);
202
206
  }
203
207
  }
208
+ /**
209
+ * Print a resolved schema as its field listing: a table for a human, or the
210
+ * same data verbatim on an explicit `--format json`. Shared by `create`,
211
+ * `update` and `schema`, so the three cannot drift.
212
+ */
213
+ function emitFieldListing(schema, rctx) {
214
+ const described = describeFields(schema);
215
+ if (program.getOptionValueSource("format") === "default") {
216
+ stdout(`${renderFieldsTable(described)}\n`);
217
+ }
218
+ else {
219
+ output.emit(described, rctx);
220
+ }
221
+ }
222
+ /**
223
+ * The write-command wiring `create` and `update` share: decide `--data` vs
224
+ * field parameters, fetch the operation schema at most once, build the payload
225
+ * from the parameters, and hand back the args/deps both `runCreate` and
226
+ * `runUpdate` accept. The two commands differ only in how they identify their
227
+ * target, which is why that part stays in each action.
228
+ */
229
+ async function writeWiring(input) {
230
+ const { ctx, options } = input;
231
+ // --fields is a discovery request, not a write: resolve the schema through
232
+ // the normal fetch-and-cache pipeline, print the parameters, send nothing.
233
+ // It reads as a table because a human asked what the fields are; an explicit
234
+ // --format json hands back the same data verbatim for a caller to consume.
235
+ if (options.fields === true) {
236
+ emitFieldListing(await loadOrFetchSchema(ctx, input.schemaTarget, input.operation), input.rctx);
237
+ return "listed";
238
+ }
239
+ // Exactly one route produces the document, and it is settled here so the
240
+ // command itself only ever receives a finished payload.
241
+ const fields = parseFieldArgs(input.tokens);
242
+ if (options.data !== undefined && fields.length > 0) {
243
+ throw new ValidationError("--data and field parameters are mutually exclusive; use one or the other");
244
+ }
245
+ if (options.data === undefined && fields.length === 0) {
246
+ throw new ValidationError("provide either --data or field parameters (--<field> <value>)");
247
+ }
248
+ let cached;
249
+ const schema = async () => (cached ??= await loadOrFetchSchema(ctx, input.schemaTarget, input.operation));
250
+ let payload;
251
+ if (options.data !== undefined) {
252
+ payload = await readDataArg(options.data);
253
+ }
254
+ else {
255
+ input.guard?.();
256
+ payload = buildPayloadFromParameters({
257
+ schema: await schema(),
258
+ parameters: fields,
259
+ operation: input.operation,
260
+ ...(input.id !== undefined ? { id: input.id } : {}),
261
+ resourceType: input.resourceType,
262
+ });
263
+ }
264
+ const args = { payload };
265
+ if (options.dryRun !== undefined)
266
+ args.dryRun = options.dryRun;
267
+ if (options.validate === false)
268
+ args.noValidate = true;
269
+ const deps = { client: ctx.client, emit: (v) => output.emit(v, input.rctx) };
270
+ if (!args.noValidate) {
271
+ deps.validate = async (payload, t) => validatePayload(await schema(), payload, t);
272
+ }
273
+ return { args, deps };
274
+ }
204
275
  function assertJsonOnly(command) {
205
276
  const f = currentFormat();
206
277
  if (f !== "json") {
@@ -347,54 +418,61 @@ Example:
347
418
  .command("create <entity_type>")
348
419
  .description("Create an entity of given type/bundle")
349
420
  .requiredOption("--bundle <bundle>")
350
- .requiredOption("--data <json>", "inline JSON or @path")
421
+ .option("--data <json>", "inline JSON or @path")
351
422
  .option("--dry-run")
352
423
  .option("--no-validate", "skip client-side schema validation")
353
- .action((entityType, o) => {
354
- const args = { entityType, bundle: o.bundle, dataArg: o.data };
355
- if (o.dryRun !== undefined)
356
- args.dryRun = o.dryRun;
357
- if (o.validate === false)
358
- args.noValidate = true;
424
+ .option("--fields", "list this bundle's field parameters and exit")
425
+ .allowUnknownOption()
426
+ .addHelpText("after", FIELD_PARAMETER_HELP)
427
+ .action((entityType, o, cmd) => {
359
428
  return run(async (ctx) => {
360
- const rctx = { command: "create", entityType, bundle: o.bundle };
361
- const deps = { client: ctx.client, emit: (v) => output.emit(v, rctx) };
362
- if (!args.noValidate) {
363
- deps.validate = async (payload, target) => {
364
- const schema = await loadOrFetchSchema(ctx, target, "create");
365
- validatePayload(schema, payload, target);
366
- };
367
- }
368
- await runCreate(args, deps);
429
+ const wiring = await writeWiring({
430
+ ctx,
431
+ operation: "create",
432
+ schemaTarget: `${entityType}/${o.bundle}`,
433
+ resourceType: `${entityType}--${o.bundle}`,
434
+ tokens: cmd.args.slice(1),
435
+ rctx: { command: "create", entityType, bundle: o.bundle },
436
+ options: o,
437
+ });
438
+ if (wiring === "listed")
439
+ return;
440
+ await runCreate({ entityType, bundle: o.bundle, ...wiring.args }, wiring.deps);
369
441
  }, assertRenderable);
370
442
  });
371
443
  program
372
444
  .command("update <target>")
373
445
  .description("Update an existing entity")
374
- .requiredOption("--data <json>", "inline JSON or @path")
446
+ .option("--data <json>", "inline JSON or @path")
375
447
  .option("--dry-run")
376
448
  .option("--no-validate", "skip client-side schema validation")
377
- .action((target, o) => {
378
- const args = { target, dataArg: o.data };
379
- if (o.dryRun !== undefined)
380
- args.dryRun = o.dryRun;
381
- if (o.validate === false)
382
- args.noValidate = true;
449
+ .option("--fields", "list this bundle's field parameters and exit")
450
+ .allowUnknownOption()
451
+ .addHelpText("after", FIELD_PARAMETER_HELP)
452
+ .action((target, o, cmd) => {
383
453
  return run(async (ctx) => {
384
- const [entityType, bundle] = target.split("/");
454
+ const [entityType, bundle, id] = target.split("/");
385
455
  const rctx = { command: "update", target };
386
456
  if (entityType !== undefined)
387
457
  rctx.entityType = entityType;
388
458
  if (bundle !== undefined)
389
459
  rctx.bundle = bundle;
390
- const deps = { client: ctx.client, emit: (v) => output.emit(v, rctx) };
391
- if (!args.noValidate) {
392
- deps.validate = async (payload, t) => {
393
- const schema = await loadOrFetchSchema(ctx, t, "update");
394
- validatePayload(schema, payload, t);
395
- };
396
- }
397
- await runUpdate(args, deps);
460
+ const wiring = await writeWiring({
461
+ ctx,
462
+ operation: "update",
463
+ schemaTarget: `${entityType}/${bundle}`,
464
+ resourceType: `${entityType}--${bundle}`,
465
+ tokens: cmd.args.slice(1),
466
+ rctx,
467
+ options: o,
468
+ ...(id !== undefined ? { id } : {}),
469
+ // Guard the target before building, so a malformed one is reported by
470
+ // its own message rather than as a missing data.id.
471
+ guard: () => assertUpdateTarget(target),
472
+ });
473
+ if (wiring === "listed")
474
+ return;
475
+ await runUpdate({ target, ...wiring.args }, wiring.deps);
398
476
  }, assertRenderable);
399
477
  });
400
478
  program
@@ -426,21 +504,29 @@ Example:
426
504
  .description("Catalog (no target) or JSON Schema for <entity>/<bundle>")
427
505
  .option("--for <op>", "create|update", "create")
428
506
  .option("--refresh", "bypass cache for this call")
507
+ .option("--fields", "list the target's field parameters instead of its schema")
429
508
  .action((target, o) => {
430
509
  const operation = o.for === "update" ? "update" : "create";
431
510
  const schemaArgs = target !== undefined
432
511
  ? { target, operation, refresh: Boolean(o.refresh) }
433
512
  : { operation, refresh: Boolean(o.refresh) };
434
- return run((ctx) => runSchema(schemaArgs, {
435
- http: ctx.http,
436
- auth: ctx.auth,
437
- baseUrl: ctx.baseUrl,
438
- jsonapiPrefix: ctx.jsonapiPrefix,
439
- cwd: ctx.cwd,
440
- emit: output.emit,
441
- warn: (m) => stderr(`${m}\n`),
442
- plugins: ctx.plugins,
443
- }), () => assertJsonOnly("schema"));
513
+ return run((ctx) => {
514
+ if (o.fields === true && target === undefined) {
515
+ throw new ValidationError("--fields needs a target: dropsh schema <entity_type>/<bundle> --fields");
516
+ }
517
+ // --fields is the same schema, viewed as its parameters: swapping the
518
+ // emitter inherits runSchema's caching and --refresh untouched.
519
+ return runSchema(schemaArgs, {
520
+ http: ctx.http,
521
+ auth: ctx.auth,
522
+ baseUrl: ctx.baseUrl,
523
+ jsonapiPrefix: ctx.jsonapiPrefix,
524
+ cwd: ctx.cwd,
525
+ emit: o.fields === true ? (schema) => emitFieldListing(schema) : output.emit,
526
+ warn: (m) => stderr(`${m}\n`),
527
+ plugins: ctx.plugins,
528
+ });
529
+ }, () => assertJsonOnly("schema"));
444
530
  });
445
531
  const auth = program.command("auth").description("Manage authentication");
446
532
  auth
@@ -12,6 +12,11 @@ export { createJsonApiClient } from "./core/jsonapi/client.js";
12
12
  export type { Collection, FilterOp } from "./core/jsonapi/collection.js";
13
13
  export type { JsonApiResourceObject, Resource, } from "./core/jsonapi/resource.js";
14
14
  export type { JsonApiDocument, JsonApiResource } from "./core/jsonapi/types.js";
15
+ export type { BuildPayloadInput } from "./core/params/from-parameters.js";
16
+ export { buildPayloadFromParameters } from "./core/params/from-parameters.js";
17
+ export type { RawParameter } from "./core/params/parse-args.js";
18
+ export type { FieldDescriptor, SchemaFieldIndex } from "./core/params/schema-fields.js";
19
+ export { indexSchemaFields, propertiesOf } from "./core/params/schema-fields.js";
15
20
  export type { DropSHPlugin, PluginContext, PluginDescriptor, RequestContext, SchemaOperation, } from "./core/plugin.js";
16
21
  export { composePlugins } from "./core/plugin.js";
17
22
  export { AuthError, ConfigError, HttpError, PluginError } from "./errors.js";
@@ -3,5 +3,7 @@ export { indexIncluded, isInteractive } from "./core/cli/render.js";
3
3
  export { loadConfig } from "./core/config.js";
4
4
  export { createHttpClient } from "./core/http.js";
5
5
  export { createJsonApiClient } from "./core/jsonapi/client.js";
6
+ export { buildPayloadFromParameters } from "./core/params/from-parameters.js";
7
+ export { indexSchemaFields, propertiesOf } from "./core/params/schema-fields.js";
6
8
  export { composePlugins } from "./core/plugin.js";
7
9
  export { AuthError, ConfigError, HttpError, PluginError } from "./errors.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dropsh",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Entity-agnostic helper CLI for Drupal 11 JSON:API, used by the dropsh Claude skill for editorial publishing workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -66,8 +66,8 @@
66
66
  "test:watch": "vitest",
67
67
  "test:integration": "pnpm run build && pnpm run build:packages && vitest run -c vitest.config.integration.ts",
68
68
  "typecheck": "pnpm run build:packages && tsc --noEmit && pnpm -r run typecheck",
69
- "lint": "biome check src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
70
- "lint:fix": "biome check --write src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/schema/ tests/integrations/*.ts",
69
+ "lint": "biome check src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/params/ tests/integrations/schema/ tests/integrations/*.ts",
70
+ "lint:fix": "biome check --write src/ packages/ plugins/ tests/integrations/helpers/ tests/integrations/params/ tests/integrations/schema/ tests/integrations/*.ts",
71
71
  "drupal:up": "bash tests/integrations/bin/drupal-up.sh",
72
72
  "drupal:down": "bash tests/integrations/bin/drupal-down.sh",
73
73
  "init-worktree": "node scripts/init-worktree.mjs",