dropsh 0.6.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 +223 -4
- package/dist/src/commands/connections.d.ts +37 -0
- package/dist/src/commands/connections.js +42 -0
- package/dist/src/commands/create.d.ts +6 -1
- package/dist/src/commands/create.js +1 -2
- package/dist/src/commands/update.d.ts +13 -1
- package/dist/src/commands/update.js +13 -5
- package/dist/src/core/auth/session-store.d.ts +2 -1
- package/dist/src/core/auth/session-store.js +4 -4
- package/dist/src/core/config.d.ts +2 -0
- package/dist/src/core/config.js +22 -3
- package/dist/src/core/connections.d.ts +58 -0
- package/dist/src/core/connections.js +115 -0
- package/dist/src/core/params/from-parameters.d.ts +12 -0
- package/dist/src/core/params/from-parameters.js +209 -0
- package/dist/src/core/params/help.d.ts +46 -0
- package/dist/src/core/params/help.js +148 -0
- package/dist/src/core/params/parse-args.d.ts +18 -0
- package/dist/src/core/params/parse-args.js +75 -0
- package/dist/src/core/params/schema-fields.d.ts +20 -0
- package/dist/src/core/params/schema-fields.js +65 -0
- package/dist/src/core/paths.d.ts +8 -0
- package/dist/src/core/paths.js +14 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +214 -57
- package/dist/src/plugin-api.d.ts +5 -0
- package/dist/src/plugin-api.js +2 -0
- package/package.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user-global dropsh state directory. Single source of that path: the
|
|
3
|
+
* per-host auth store and the connections directory both hang off it, so the
|
|
4
|
+
* two cannot drift onto different roots.
|
|
5
|
+
*/
|
|
6
|
+
export declare function defaultStateDir(): string;
|
|
7
|
+
/** Default location of the named-connection files, one `<id>.js` per connection. */
|
|
8
|
+
export declare function defaultConnectionsDir(): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* The user-global dropsh state directory. Single source of that path: the
|
|
5
|
+
* per-host auth store and the connections directory both hang off it, so the
|
|
6
|
+
* two cannot drift onto different roots.
|
|
7
|
+
*/
|
|
8
|
+
export function defaultStateDir() {
|
|
9
|
+
return join(homedir(), ".config", "dropsh");
|
|
10
|
+
}
|
|
11
|
+
/** Default location of the named-connection files, one `<id>.js` per connection. */
|
|
12
|
+
export function defaultConnectionsDir() {
|
|
13
|
+
return join(defaultStateDir(), "connections");
|
|
14
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -39,4 +39,16 @@ export interface AuthStatusDeps {
|
|
|
39
39
|
}
|
|
40
40
|
export declare function authStatus(deps: AuthStatusDeps): Promise<AuthStatusInfo>;
|
|
41
41
|
export declare function buildProgram(opts?: ProgramOptions): Command;
|
|
42
|
+
/**
|
|
43
|
+
* The config selectors as they appear in raw argv, for the pre-parse plugin
|
|
44
|
+
* bootstrap — which runs before commander sees any option. It feeds the same
|
|
45
|
+
* resolver the commands use, so the early and the late resolution cannot answer
|
|
46
|
+
* differently and load plugins from a different connection than the command
|
|
47
|
+
* being bootstrapped.
|
|
48
|
+
*/
|
|
49
|
+
export declare function earlyConfigArgs(argv: string[]): {
|
|
50
|
+
config?: string;
|
|
51
|
+
connection?: string;
|
|
52
|
+
connectionsDir?: string;
|
|
53
|
+
};
|
|
42
54
|
export declare function main(): Promise<void>;
|