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
package/README.md
CHANGED
|
@@ -171,7 +171,8 @@ Each descriptor's `with` is passed to the plugin factory (legacy alias:
|
|
|
171
171
|
descriptor names `dropsh/plugin` with `export: "basicAuthPlugin"`.
|
|
172
172
|
|
|
173
173
|
Use `dropsh.config.example.js` as a fuller starting point. Override the config
|
|
174
|
-
path with `--config <path>` or `DROPSH_CONFIG
|
|
174
|
+
path with `--config <path>` or `DROPSH_CONFIG`, or address a config file by name
|
|
175
|
+
with `--connection <id>` — see [Named connections](#named-connections--one-config-file-per-site).
|
|
175
176
|
|
|
176
177
|
<details>
|
|
177
178
|
<summary>Workspace variant — importing factories directly</summary>
|
|
@@ -204,6 +205,66 @@ to the config file, so it only works when the packages are installed there.
|
|
|
204
205
|
|
|
205
206
|
</details>
|
|
206
207
|
|
|
208
|
+
### Named connections — one config file per site
|
|
209
|
+
|
|
210
|
+
A *named connection* is a config file addressed by id instead of by path. A
|
|
211
|
+
connection is a file: `<connections-dir>/<id>.js`, carrying exactly the shape
|
|
212
|
+
`dropsh.config.js` has (`site`, `defaults`, `plugins`), and its basename is its
|
|
213
|
+
id. There is no registry, index or map — adding a connection is adding a file.
|
|
214
|
+
Only `.js` files are connections.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
dropsh --connection staging read node/article/<uuid>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**The connections directory** defaults to `~/.config/dropsh/connections/`,
|
|
221
|
+
beside the per-host auth store. It is overridable, and the flag wins over the
|
|
222
|
+
environment variable:
|
|
223
|
+
|
|
224
|
+
| # | Selector | Kind |
|
|
225
|
+
|---|----------|------|
|
|
226
|
+
| 1 | `--connections-dir <path>` | flag |
|
|
227
|
+
| 2 | `$DROPSH_CONNECTIONS_DIR` | environment |
|
|
228
|
+
| 3 | `~/.config/dropsh/connections` | default |
|
|
229
|
+
|
|
230
|
+
**Which config file a run uses**, highest first:
|
|
231
|
+
|
|
232
|
+
| # | Selector | Kind |
|
|
233
|
+
|---|----------|------|
|
|
234
|
+
| 1 | `--config <path>` | names a **file** |
|
|
235
|
+
| 2 | `--connection <id>` | names an **entry** |
|
|
236
|
+
| 3 | `$DROPSH_CONFIG` | names a **file** |
|
|
237
|
+
| 4 | `$DROPSH_CONNECTION` | names an **entry** |
|
|
238
|
+
| 5 | `./dropsh.config.js` | the cwd default |
|
|
239
|
+
|
|
240
|
+
`--config` and `--connection` given together is not an error: `--config` names a
|
|
241
|
+
file and wins.
|
|
242
|
+
|
|
243
|
+
An unknown id is a hard error — non-zero exit, naming the id, the directory
|
|
244
|
+
searched and the available ids. Nothing is prompted and nothing is created, so
|
|
245
|
+
an unattended run fails rather than hangs:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
Unknown connection 'stagng'. Available in /home/me/.config/dropsh/connections: production, staging.
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**List them** with `dropsh connections list`: one row per connection with its
|
|
252
|
+
`site.base_url`, a `*` on the one the current invocation would resolve to, and
|
|
253
|
+
the resolved path stated instead when that is a path rather than an id. A
|
|
254
|
+
connection whose file cannot be read is listed with its error in place of the
|
|
255
|
+
URL and the listing still exits 0. Listing reads `site` only, so a connection
|
|
256
|
+
naming an uninstalled plugin package cannot abort the listing of the others.
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
dropsh connections list
|
|
260
|
+
dropsh connections list --format json
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Auth is unchanged by connections: the session store keys on the host of
|
|
264
|
+
`site.base_url`, so two connections against different hosts keep their tokens
|
|
265
|
+
apart and two against the same host share them. `--auth-profile` keeps selecting
|
|
266
|
+
the identity *within* whichever connection is in force.
|
|
267
|
+
|
|
207
268
|
## Authentication
|
|
208
269
|
|
|
209
270
|
Authentication is configured through plugins. Each plugin that contributes an
|
|
@@ -295,17 +356,175 @@ exit codes.
|
|
|
295
356
|
```bash
|
|
296
357
|
dropsh read <entity_type>/<bundle>/<uuid> [--include=<field>,…]
|
|
297
358
|
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]
|
|
359
|
+
dropsh create <entity_type> --bundle=<b> (--data=<json|@file> | <field parameters>) [--dry-run] [--no-validate]
|
|
360
|
+
dropsh update <entity_type>/<bundle>/<uuid> (--data=<json|@file> | <field parameters>) [--dry-run] [--no-validate]
|
|
300
361
|
dropsh delete <entity_type>/<bundle>/<uuid> [--dry-run]
|
|
301
362
|
dropsh upload-file --target=<entity_type>/<bundle>/<uuid>/<field> --file=<path> [--dry-run]
|
|
302
363
|
dropsh schema [--refresh]
|
|
303
|
-
dropsh schema <entity_type>/<bundle> [--for=create|update] [--refresh]
|
|
364
|
+
dropsh schema <entity_type>/<bundle> [--for=create|update] [--refresh] [--fields]
|
|
365
|
+
dropsh connections list
|
|
304
366
|
```
|
|
305
367
|
|
|
368
|
+
Every command accepts the global `--config <path>`, `--connection <id>` and
|
|
369
|
+
`--connections-dir <path>` selectors described under
|
|
370
|
+
[Named connections](#named-connections--one-config-file-per-site).
|
|
371
|
+
|
|
306
372
|
`create` and `update` validate payloads against the current schema by default.
|
|
307
373
|
Pass `--no-validate` when you intentionally want to skip local validation.
|
|
308
374
|
|
|
375
|
+
### Field parameters on `create` / `update`
|
|
376
|
+
|
|
377
|
+
Instead of handing `create`/`update` a finished JSON:API document via `--data`,
|
|
378
|
+
you can name the fields directly. The CLI reads the bundle's operation schema and
|
|
379
|
+
builds the document itself, so it knows which name is an attribute and which is a
|
|
380
|
+
relationship:
|
|
381
|
+
|
|
382
|
+
| Form | Meaning |
|
|
383
|
+
|---|---|
|
|
384
|
+
| `--<field> <value>` | set a field |
|
|
385
|
+
| `--<field>=<value>` | same; required when the value itself starts with `--` |
|
|
386
|
+
| `--<field>.<sub> <value>` | set a sub-property, e.g. `--body.value "Text"` |
|
|
387
|
+
| `--<relationship> <uuid>` | reference by UUID; repeat for a multi-valued field |
|
|
388
|
+
| `--json <field>=<json>` | raw JSON value, for arrays of objects |
|
|
389
|
+
|
|
390
|
+
Any number of fields goes into a single invocation; the `--<field>=<value>` pair
|
|
391
|
+
form and the spaced form are interchangeable and produce identical output.
|
|
392
|
+
|
|
393
|
+
**`--fields` lists the bundle's actual fields.** `--help` documents the forms;
|
|
394
|
+
`--fields` resolves the bundle's schema and prints every settable parameter,
|
|
395
|
+
then exits without sending anything:
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
dropsh schema node/article_test --fields # the introspection route
|
|
399
|
+
dropsh create node --bundle=article_test --fields # same listing, on the command
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
node--article_test — field parameters
|
|
404
|
+
|
|
405
|
+
Attributes:
|
|
406
|
+
--drupal_internal__nid <value> integer ID
|
|
407
|
+
--drupal_internal__vid <value> integer Revision ID
|
|
408
|
+
--langcode.value <value> string Language code
|
|
409
|
+
--langcode.language <value> Language object
|
|
410
|
+
--revision_timestamp <value> number Revision create time
|
|
411
|
+
--revision_log <value> string Revision log message
|
|
412
|
+
--status [true|false] boolean Published
|
|
413
|
+
--title <value> string required Title
|
|
414
|
+
--created <value> number Authored on
|
|
415
|
+
--changed <value> number Changed
|
|
416
|
+
--promote [true|false] boolean Promoted to front page
|
|
417
|
+
--sticky [true|false] boolean Sticky at top of lists
|
|
418
|
+
--default_langcode [true|false] boolean Default translation
|
|
419
|
+
--revision_default [true|false] boolean Default revision
|
|
420
|
+
--revision_translation_affected [true|false] boolean Revision translation affected
|
|
421
|
+
--path.alias <value> string Path alias
|
|
422
|
+
--path.pid <value> integer Path id
|
|
423
|
+
--path.langcode <value> string Language Code
|
|
424
|
+
--body.value <value> string Text
|
|
425
|
+
--body.format <value> string Text format
|
|
426
|
+
--body.summary <value> string Summary
|
|
427
|
+
--field_test_text <value> string Test Text
|
|
428
|
+
|
|
429
|
+
Relationships (take a UUID):
|
|
430
|
+
--node_type <uuid> node_type--node_type Content type
|
|
431
|
+
--revision_uid <uuid> user--user Revision user
|
|
432
|
+
--uid <uuid> user--user Authored by
|
|
433
|
+
--field_image <uuid> file--file Image
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
The listing is the schema's, not a curated subset: Drupal's base fields
|
|
437
|
+
(`--drupal_internal__nid`, `--langcode.*`, `--path.*`, `--node_type`,
|
|
438
|
+
`--revision_uid`, …) are settable parameters and appear alongside the
|
|
439
|
+
bundle's own fields.
|
|
440
|
+
|
|
441
|
+
Each row carries the schema type, whether the field is required, and the field's
|
|
442
|
+
human label. Object attributes appear as their dotted leaves (`--body.value`,
|
|
443
|
+
never `--body`), array attributes as their `--json` form, and a relationship that
|
|
444
|
+
allows several target types is spelled `<type>:<uuid>`. The same listing is reachable three ways, all derived from the schema alone with
|
|
445
|
+
no extra request: `dropsh schema <entity>/<bundle> --fields` (honouring `--for`
|
|
446
|
+
and `--refresh`), and `--fields` on `create` and `update`.
|
|
447
|
+
|
|
448
|
+
Add an explicit `--format json` to get the same listing as JSON — `parameter`,
|
|
449
|
+
`path`, `type`, `label`, `required`, `bare_flag` per attribute, and `targets`,
|
|
450
|
+
`multiple`, `requires_type_prefix` per relationship — for a caller that wants to
|
|
451
|
+
consume it rather than read it.
|
|
452
|
+
|
|
453
|
+
Rules worth knowing:
|
|
454
|
+
|
|
455
|
+
- **Field names come from the schema** (`dropsh schema <entity>/<bundle>`), so an
|
|
456
|
+
unknown name is **rejected** with exit code `4` instead of being ignored. The
|
|
457
|
+
error names the rejected parameter and lists the known attributes and
|
|
458
|
+
relationships.
|
|
459
|
+
- **Relationships take a bare UUID**; the resource type comes from the schema.
|
|
460
|
+
When a relationship allows several target types, pass `<type>:<uuid>`.
|
|
461
|
+
Repeating the flag accumulates values on a multi-valued relationship; a second
|
|
462
|
+
value on a single-valued one is an error.
|
|
463
|
+
- **Values are typed by the schema** — `boolean`, `integer`/`number`, else
|
|
464
|
+
string. A bare `--flag` means `true` only on a boolean field. Values are never
|
|
465
|
+
comma-split, so `--title "a, b"` survives intact.
|
|
466
|
+
- **`--data` and field parameters are mutually exclusive**; giving neither is an
|
|
467
|
+
error.
|
|
468
|
+
- **`--dry-run` prints the generated document** and sends nothing. Output keys are
|
|
469
|
+
emitted in schema declaration order, so two dry-runs of the same fields are
|
|
470
|
+
byte-identical and diffable.
|
|
471
|
+
- **`--no-validate` skips Ajv, not the schema.** In parameter mode the schema is
|
|
472
|
+
always loaded — attribute/relationship placement is impossible without it.
|
|
473
|
+
- A field whose name collides with a reserved option (`--bundle`, `--data`,
|
|
474
|
+
`--dry-run`, `--no-validate`, `--fields`, `--format`, `--auth-profile`,
|
|
475
|
+
`--config`, `--view-mode`, `--json`) is out of reach as a bare `--<field>`, but
|
|
476
|
+
`--json <field>=<json>` still sets it (write the value as JSON), and so does
|
|
477
|
+
`--data`. No Drupal base field name collides with that list in practice.
|
|
478
|
+
- Fields contributed by a plugin's `extendOperationSchema` hook are usable as
|
|
479
|
+
parameters like any native field.
|
|
480
|
+
|
|
481
|
+
Worked example:
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
dropsh create node --bundle=article_test \
|
|
485
|
+
--title "Hello" --body.value "Text" \
|
|
486
|
+
--uid c8e0b5d2-9e66-4caf-ac43-337f3b3af62d --dry-run | jq
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
```json
|
|
490
|
+
{
|
|
491
|
+
"dry_run": true,
|
|
492
|
+
"method": "POST",
|
|
493
|
+
"path": "node/article_test",
|
|
494
|
+
"payload": {
|
|
495
|
+
"data": {
|
|
496
|
+
"type": "node--article_test",
|
|
497
|
+
"attributes": {
|
|
498
|
+
"title": "Hello",
|
|
499
|
+
"body": {
|
|
500
|
+
"value": "Text"
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
"relationships": {
|
|
504
|
+
"uid": {
|
|
505
|
+
"data": {
|
|
506
|
+
"type": "user--user",
|
|
507
|
+
"id": "c8e0b5d2-9e66-4caf-ac43-337f3b3af62d"
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
`update` works the same way and puts the target UUID into `data.id`, sending a
|
|
517
|
+
PATCH with only the fields you named:
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
dropsh update node/article_test/<uuid> --title "changed title"
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
The builder is also available programmatically — `buildPayloadFromParameters`,
|
|
524
|
+
`indexSchemaFields` and `propertiesOf` are exported from `dropsh/plugin`, so a
|
|
525
|
+
plugin can turn (schema, parameters) into the same document without invoking the
|
|
526
|
+
CLI.
|
|
527
|
+
|
|
309
528
|
### `--include` on `read` / `search`
|
|
310
529
|
|
|
311
530
|
Pass `--include` to embed related resources in the response's JSON:API
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ConfigSource, ConnectionEntry } from "../core/connections.js";
|
|
2
|
+
export interface ConnectionsListArgs {
|
|
3
|
+
/** The resolved connections directory. */
|
|
4
|
+
dir: string;
|
|
5
|
+
/** What this invocation would resolve to — the row it marks, when it names an id. */
|
|
6
|
+
resolved: ConfigSource;
|
|
7
|
+
}
|
|
8
|
+
export interface ConnectionsListDeps {
|
|
9
|
+
list: (dir: string) => Promise<ConnectionEntry[]>;
|
|
10
|
+
emit: (v: unknown) => void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/** One listed connection, as it is emitted. */
|
|
13
|
+
export interface ConnectionsListRow {
|
|
14
|
+
id: string;
|
|
15
|
+
base_url?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
/** True on the single connection this invocation would resolve to. */
|
|
18
|
+
current: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface ConnectionsListResult {
|
|
21
|
+
directory: string;
|
|
22
|
+
resolved: ConfigSource;
|
|
23
|
+
connections: ConnectionsListRow[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* List the connections in the resolved directory. A broken connection is a row
|
|
27
|
+
* carrying its error, not an abort: the listing succeeded and reports exactly
|
|
28
|
+
* what is on disk. When the invocation resolves to a path rather than an id —
|
|
29
|
+
* `--config`, `$DROPSH_CONFIG`, or the cwd default — no row is marked and the
|
|
30
|
+
* resolved path stands on its own in `resolved`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runConnectionsList(args: ConnectionsListArgs, deps: ConnectionsListDeps): Promise<ConnectionsListResult>;
|
|
33
|
+
/**
|
|
34
|
+
* The listing as a human reads it: one row per connection, a `*` on the one in
|
|
35
|
+
* force, and the resolved path stated below when no id is in force.
|
|
36
|
+
*/
|
|
37
|
+
export declare function renderConnectionsTable(result: ConnectionsListResult): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List the connections in the resolved directory. A broken connection is a row
|
|
3
|
+
* carrying its error, not an abort: the listing succeeded and reports exactly
|
|
4
|
+
* what is on disk. When the invocation resolves to a path rather than an id —
|
|
5
|
+
* `--config`, `$DROPSH_CONFIG`, or the cwd default — no row is marked and the
|
|
6
|
+
* resolved path stands on its own in `resolved`.
|
|
7
|
+
*/
|
|
8
|
+
export async function runConnectionsList(args, deps) {
|
|
9
|
+
const entries = await deps.list(args.dir);
|
|
10
|
+
const result = {
|
|
11
|
+
directory: args.dir,
|
|
12
|
+
resolved: args.resolved,
|
|
13
|
+
connections: entries.map((e) => ({
|
|
14
|
+
id: e.id,
|
|
15
|
+
...(e.baseUrl !== undefined ? { base_url: e.baseUrl } : {}),
|
|
16
|
+
...(e.error !== undefined ? { error: e.error } : {}),
|
|
17
|
+
current: e.id === args.resolved.id,
|
|
18
|
+
})),
|
|
19
|
+
};
|
|
20
|
+
await deps.emit(result);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The listing as a human reads it: one row per connection, a `*` on the one in
|
|
25
|
+
* force, and the resolved path stated below when no id is in force.
|
|
26
|
+
*/
|
|
27
|
+
export function renderConnectionsTable(result) {
|
|
28
|
+
const lines = [`Connections in ${result.directory}`];
|
|
29
|
+
if (result.connections.length === 0) {
|
|
30
|
+
lines.push(" (none)");
|
|
31
|
+
}
|
|
32
|
+
const width = Math.max(0, ...result.connections.map((c) => c.id.length));
|
|
33
|
+
for (const c of result.connections) {
|
|
34
|
+
const marker = c.current ? "*" : " ";
|
|
35
|
+
const value = c.base_url ?? `! ${c.error ?? "unreadable"}`;
|
|
36
|
+
lines.push(`${marker} ${c.id.padEnd(width)} ${value}`);
|
|
37
|
+
}
|
|
38
|
+
if (result.resolved.id === undefined) {
|
|
39
|
+
lines.push(`Resolved config: ${result.resolved.path} (${result.resolved.source})`);
|
|
40
|
+
}
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { defaultStateDir } from "../paths.js";
|
|
1
2
|
import type { AuthSession } from "./types.js";
|
|
3
|
+
export { defaultStateDir };
|
|
2
4
|
/** One stored auth identity: the provider that owns it plus its opaque session. */
|
|
3
5
|
export interface ProfileRecord {
|
|
4
6
|
provider: string;
|
|
@@ -17,7 +19,6 @@ export interface SessionRecord {
|
|
|
17
19
|
activeProvider: string;
|
|
18
20
|
session: AuthSession;
|
|
19
21
|
}
|
|
20
|
-
export declare function defaultStateDir(): string;
|
|
21
22
|
/**
|
|
22
23
|
* Read the whole profiles file, migrating a legacy `{ activeProvider, session }`
|
|
23
24
|
* record in memory to the v2 shape. No write-time migration is needed: the next
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { ConfigError } from "../../errors.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { defaultStateDir } from "../paths.js";
|
|
5
|
+
// Re-exported so every existing importer of the auth store keeps working while
|
|
6
|
+
// the path itself lives in one place (`src/core/paths.ts`).
|
|
7
|
+
export { defaultStateDir };
|
|
8
8
|
function hostnameFromUrl(baseUrl) {
|
|
9
9
|
return new URL(baseUrl).hostname;
|
|
10
10
|
}
|
|
@@ -11,4 +11,6 @@ export interface Config {
|
|
|
11
11
|
};
|
|
12
12
|
plugins: DropSHPlugin[];
|
|
13
13
|
}
|
|
14
|
+
/** The `site` section of a config file, read without resolving its plugins. */
|
|
15
|
+
export declare function loadConfigSite(filePath: string): Promise<SiteConfig>;
|
|
14
16
|
export declare function loadConfig(filePath: string): Promise<Config>;
|
package/dist/src/core/config.js
CHANGED
|
@@ -152,7 +152,14 @@ async function resolvePlugins(raw, configPath) {
|
|
|
152
152
|
await expand(entry, baseBases, undefined);
|
|
153
153
|
return out;
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Import a config module and validate its `site` section — everything a config
|
|
157
|
+
* source has to be valid for, short of resolving `plugins[]`. Both `loadConfig`
|
|
158
|
+
* and the connection listing go through here, so the two cannot diverge on what
|
|
159
|
+
* a valid `site` is, and the listing can read many files without constructing
|
|
160
|
+
* any plugin.
|
|
161
|
+
*/
|
|
162
|
+
async function importConfig(filePath) {
|
|
156
163
|
const configPath = resolve(filePath);
|
|
157
164
|
let mod;
|
|
158
165
|
try {
|
|
@@ -169,13 +176,25 @@ export async function loadConfig(filePath) {
|
|
|
169
176
|
throw new ConfigError("site section missing");
|
|
170
177
|
if (typeof site.base_url !== "string" || site.base_url.length === 0)
|
|
171
178
|
throw new ConfigError("site.base_url required");
|
|
172
|
-
const defaults = isRecord(raw.defaults) ? raw.defaults : {};
|
|
173
|
-
const plugins = await resolvePlugins(raw.plugins, configPath);
|
|
174
179
|
return {
|
|
180
|
+
configPath,
|
|
181
|
+
raw,
|
|
175
182
|
site: {
|
|
176
183
|
base_url: site.base_url,
|
|
177
184
|
jsonapi_prefix: typeof site.jsonapi_prefix === "string" ? site.jsonapi_prefix : "/jsonapi",
|
|
178
185
|
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** The `site` section of a config file, read without resolving its plugins. */
|
|
189
|
+
export async function loadConfigSite(filePath) {
|
|
190
|
+
return (await importConfig(filePath)).site;
|
|
191
|
+
}
|
|
192
|
+
export async function loadConfig(filePath) {
|
|
193
|
+
const { configPath, raw, site } = await importConfig(filePath);
|
|
194
|
+
const defaults = isRecord(raw.defaults) ? raw.defaults : {};
|
|
195
|
+
const plugins = await resolvePlugins(raw.plugins, configPath);
|
|
196
|
+
return {
|
|
197
|
+
site,
|
|
179
198
|
defaults: {
|
|
180
199
|
dry_run: defaults.dry_run === true,
|
|
181
200
|
timeout_ms: typeof defaults.timeout_ms === "number" ? defaults.timeout_ms : 30000,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The name of the config file dropsh falls back to in the current directory. */
|
|
2
|
+
export declare const CWD_CONFIG_FILE = "dropsh.config.js";
|
|
3
|
+
/** Which selector produced the resolved config path. */
|
|
4
|
+
export type ConfigSourceKind = "flag-config" | "flag-connection" | "env-config" | "env-connection" | "cwd";
|
|
5
|
+
/** The chosen config file plus the provenance of that choice. */
|
|
6
|
+
export interface ConfigSource {
|
|
7
|
+
path: string;
|
|
8
|
+
source: ConfigSourceKind;
|
|
9
|
+
/** The connection id, when the winning selector named an entry rather than a file. */
|
|
10
|
+
id?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ResolveConnectionsDirInput {
|
|
13
|
+
/** `--connections-dir <path>`. */
|
|
14
|
+
flag?: string | undefined;
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The connections directory: `--connections-dir` > `$DROPSH_CONNECTIONS_DIR` >
|
|
19
|
+
* `~/.config/dropsh/connections`. Resolved independently of which config
|
|
20
|
+
* selector wins, because `connections list` reports the directory either way.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveConnectionsDir(input?: ResolveConnectionsDirInput): string;
|
|
23
|
+
export interface ResolveConfigSourceInput {
|
|
24
|
+
/** `--config <path>`. */
|
|
25
|
+
config?: string | undefined;
|
|
26
|
+
/** `--connection <id>`. */
|
|
27
|
+
connection?: string | undefined;
|
|
28
|
+
connectionsDir?: string;
|
|
29
|
+
env?: NodeJS.ProcessEnv;
|
|
30
|
+
cwd?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the config file for this invocation, highest selector first:
|
|
34
|
+
* `--config` > `--connection` > `$DROPSH_CONFIG` > `$DROPSH_CONNECTION` >
|
|
35
|
+
* `./dropsh.config.js`. `--config` and `--connection` together is not an error:
|
|
36
|
+
* `--config` names a *file* and that is its stated meaning, so it wins silently.
|
|
37
|
+
*
|
|
38
|
+
* Returns the path *and* its provenance, so every caller — the command context,
|
|
39
|
+
* the auth wiring, the pre-parse plugin bootstrap and `connections list` — reads
|
|
40
|
+
* the same answer instead of re-deriving the chain.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveConfigSource(input?: ResolveConfigSourceInput): ConfigSource;
|
|
43
|
+
/** One row of `connections list`: a connection's id and either its base URL or its error. */
|
|
44
|
+
export interface ConnectionEntry {
|
|
45
|
+
id: string;
|
|
46
|
+
baseUrl?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Every `.js` file in `dir`, by basename, sorted, with its `site.base_url`. A
|
|
51
|
+
* file that cannot be imported, or that declares no usable `site`, is a row
|
|
52
|
+
* carrying its error rather than an abort: the listing reports exactly what is
|
|
53
|
+
* on disk. Plugins are never constructed, so one connection naming an
|
|
54
|
+
* uninstalled package cannot take the others down with it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function listConnections(dir: string): Promise<ConnectionEntry[]>;
|
|
57
|
+
/** Sorted ids of the `.js` files in `dir`; `[]` when the directory is absent. */
|
|
58
|
+
export declare function listConnectionIds(dir: string): Promise<string[]>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { ConfigError } from "../errors.js";
|
|
5
|
+
import { loadConfigSite } from "./config.js";
|
|
6
|
+
import { defaultConnectionsDir } from "./paths.js";
|
|
7
|
+
/** The name of the config file dropsh falls back to in the current directory. */
|
|
8
|
+
export const CWD_CONFIG_FILE = "dropsh.config.js";
|
|
9
|
+
/**
|
|
10
|
+
* The connections directory: `--connections-dir` > `$DROPSH_CONNECTIONS_DIR` >
|
|
11
|
+
* `~/.config/dropsh/connections`. Resolved independently of which config
|
|
12
|
+
* selector wins, because `connections list` reports the directory either way.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveConnectionsDir(input = {}) {
|
|
15
|
+
const env = input.env ?? process.env;
|
|
16
|
+
return input.flag ?? env.DROPSH_CONNECTIONS_DIR ?? defaultConnectionsDir();
|
|
17
|
+
}
|
|
18
|
+
function connectionPath(dir, id) {
|
|
19
|
+
return join(dir, `${id}.js`);
|
|
20
|
+
}
|
|
21
|
+
function toIds(entries) {
|
|
22
|
+
return entries
|
|
23
|
+
.filter((name) => name.endsWith(".js"))
|
|
24
|
+
.map((name) => basename(name, ".js"))
|
|
25
|
+
.sort();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Sorted ids of the `.js` files in `dir`; `[]` when the directory is absent.
|
|
29
|
+
* Synchronous because it is only reached on the unknown-id error path, and the
|
|
30
|
+
* resolver itself has to stay synchronous for the commander option wiring.
|
|
31
|
+
*/
|
|
32
|
+
function listConnectionIdsSync(dir) {
|
|
33
|
+
try {
|
|
34
|
+
return toIds(readdirSync(dir));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the config file for this invocation, highest selector first:
|
|
42
|
+
* `--config` > `--connection` > `$DROPSH_CONFIG` > `$DROPSH_CONNECTION` >
|
|
43
|
+
* `./dropsh.config.js`. `--config` and `--connection` together is not an error:
|
|
44
|
+
* `--config` names a *file* and that is its stated meaning, so it wins silently.
|
|
45
|
+
*
|
|
46
|
+
* Returns the path *and* its provenance, so every caller — the command context,
|
|
47
|
+
* the auth wiring, the pre-parse plugin bootstrap and `connections list` — reads
|
|
48
|
+
* the same answer instead of re-deriving the chain.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveConfigSource(input = {}) {
|
|
51
|
+
const env = input.env ?? process.env;
|
|
52
|
+
const dir = input.connectionsDir ?? resolveConnectionsDir({ env });
|
|
53
|
+
const cwd = input.cwd ?? process.cwd();
|
|
54
|
+
if (input.config)
|
|
55
|
+
return { path: input.config, source: "flag-config" };
|
|
56
|
+
if (input.connection) {
|
|
57
|
+
return {
|
|
58
|
+
path: requireConnection(dir, input.connection),
|
|
59
|
+
source: "flag-connection",
|
|
60
|
+
id: input.connection,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (env.DROPSH_CONFIG)
|
|
64
|
+
return { path: env.DROPSH_CONFIG, source: "env-config" };
|
|
65
|
+
if (env.DROPSH_CONNECTION) {
|
|
66
|
+
const id = env.DROPSH_CONNECTION;
|
|
67
|
+
return { path: requireConnection(dir, id), source: "env-connection", id };
|
|
68
|
+
}
|
|
69
|
+
return { path: join(cwd, CWD_CONFIG_FILE), source: "cwd" };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The path of connection `id`, or a `ConfigError` naming the id, the directory
|
|
73
|
+
* searched and the available ids. Nothing prompts and nothing is created: an
|
|
74
|
+
* unattended dispatch must fail rather than hang.
|
|
75
|
+
*/
|
|
76
|
+
function requireConnection(dir, id) {
|
|
77
|
+
const path = connectionPath(dir, id);
|
|
78
|
+
if (existsSync(path))
|
|
79
|
+
return path;
|
|
80
|
+
const available = listConnectionIdsSync(dir);
|
|
81
|
+
const tail = available.length > 0
|
|
82
|
+
? `Available in ${dir}: ${available.join(", ")}.`
|
|
83
|
+
: `no connections found in ${dir}.`;
|
|
84
|
+
throw new ConfigError(`Unknown connection '${id}'. ${tail}`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Every `.js` file in `dir`, by basename, sorted, with its `site.base_url`. A
|
|
88
|
+
* file that cannot be imported, or that declares no usable `site`, is a row
|
|
89
|
+
* carrying its error rather than an abort: the listing reports exactly what is
|
|
90
|
+
* on disk. Plugins are never constructed, so one connection naming an
|
|
91
|
+
* uninstalled package cannot take the others down with it.
|
|
92
|
+
*/
|
|
93
|
+
export async function listConnections(dir) {
|
|
94
|
+
const ids = await listConnectionIds(dir);
|
|
95
|
+
return Promise.all(ids.map(async (id) => {
|
|
96
|
+
try {
|
|
97
|
+
const site = await loadConfigSite(connectionPath(dir, id));
|
|
98
|
+
return { id, baseUrl: site.base_url };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return { id, error: err instanceof Error ? err.message : String(err) };
|
|
102
|
+
}
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/** Sorted ids of the `.js` files in `dir`; `[]` when the directory is absent. */
|
|
106
|
+
export async function listConnectionIds(dir) {
|
|
107
|
+
let entries;
|
|
108
|
+
try {
|
|
109
|
+
entries = await readdir(dir);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
return toIds(entries);
|
|
115
|
+
}
|