dropsh 0.5.8 → 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.
@@ -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
+ }
@@ -1,7 +1,8 @@
1
1
  import type { Command } from "commander";
2
2
  import type { AuthAdapter, AuthProvider } from "./auth/types.js";
3
3
  import type { AnyRenderer } from "./cli/render.js";
4
- import type { HttpClient } from "./http.js";
4
+ import type { HttpClient, HttpRequest } from "./http.js";
5
+ import type { DropSHOperation } from "./jsonapi/client.js";
5
6
  import type { Operation } from "./schema/to-jsonschema.js";
6
7
  export interface PluginContext {
7
8
  http: HttpClient;
@@ -21,6 +22,11 @@ export interface PluginContext {
21
22
  warn?: (message: string) => void;
22
23
  }
23
24
  export type SchemaOperation = Operation;
25
+ export interface RequestContext extends PluginContext {
26
+ operation: DropSHOperation;
27
+ entityType?: string;
28
+ bundle?: string;
29
+ }
24
30
  /**
25
31
  * Import-free descriptor naming a plugin package as a string. Same shape used
26
32
  * by the top-level `plugins[]` config; a plugin returns these in `dependencies`
@@ -50,6 +56,7 @@ export interface DropSHPlugin {
50
56
  */
51
57
  extendSchema?(entityType: string, bundle: string, baseSchema: unknown, ctx: PluginContext): Promise<unknown>;
52
58
  extendOperationSchema?(entityType: string, bundle: string, operation: SchemaOperation, operationSchema: unknown, ctx: PluginContext): Promise<unknown>;
59
+ alterRequest?(req: HttpRequest, ctx: RequestContext): Promise<HttpRequest>;
53
60
  registerCommands?(program: Command): void;
54
61
  renderers?: AnyRenderer[];
55
62
  }
@@ -0,0 +1,4 @@
1
+ import type { HttpRequest } from "./http.js";
2
+ import type { DropSHOperation } from "./jsonapi/client.js";
3
+ import type { DropSHPlugin, PluginContext } from "./plugin.js";
4
+ export declare function composeRequestHooks(plugins: DropSHPlugin[], ctx: PluginContext): (req: HttpRequest, operation: DropSHOperation, entityType?: string, bundle?: string) => Promise<HttpRequest>;
@@ -0,0 +1,26 @@
1
+ import { PluginError } from "../errors.js";
2
+ export function composeRequestHooks(plugins, ctx) {
3
+ return async (req, operation, entityType, bundle) => {
4
+ const requestContext = {
5
+ ...ctx,
6
+ operation,
7
+ ...(entityType !== undefined ? { entityType } : {}),
8
+ ...(bundle !== undefined ? { bundle } : {}),
9
+ };
10
+ let current = req;
11
+ for (const plugin of plugins) {
12
+ if (!plugin.alterRequest)
13
+ continue;
14
+ try {
15
+ const next = await plugin.alterRequest(current, requestContext);
16
+ if (next === undefined)
17
+ throw new Error("alterRequest must return an HttpRequest");
18
+ current = next;
19
+ }
20
+ catch (cause) {
21
+ throw new PluginError(plugin.id, "alterRequest", cause);
22
+ }
23
+ }
24
+ return current;
25
+ };
26
+ }
@@ -17,4 +17,7 @@ export declare class HttpError extends CliError {
17
17
  readonly body: unknown;
18
18
  constructor(status: number, message: string, body?: unknown);
19
19
  }
20
+ export declare class PluginError extends CliError {
21
+ constructor(pluginId: string, hook: string, cause: unknown);
22
+ }
20
23
  export declare function exitCodeFor(err: unknown): number;
@@ -36,6 +36,16 @@ export class HttpError extends CliError {
36
36
  this.body = body;
37
37
  }
38
38
  }
39
+ export class PluginError extends CliError {
40
+ constructor(pluginId, hook, cause) {
41
+ super("E_PLUGIN", `plugin '${pluginId}' failed in ${hook}`, {
42
+ pluginId,
43
+ hook,
44
+ cause: String(cause),
45
+ });
46
+ this.name = "PluginError";
47
+ }
48
+ }
39
49
  export function exitCodeFor(err) {
40
50
  if (err instanceof ConfigError)
41
51
  return 2;
@@ -45,5 +55,7 @@ export function exitCodeFor(err) {
45
55
  return 4;
46
56
  if (err instanceof HttpError)
47
57
  return 5;
58
+ if (err instanceof PluginError)
59
+ return 6;
48
60
  return 1;
49
61
  }