dropsh 0.3.3 → 0.5.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
|
@@ -55,14 +55,18 @@ Optional modules unlock stricter schemas or builder-specific support:
|
|
|
55
55
|
| Feature | Drupal modules |
|
|
56
56
|
| --- | --- |
|
|
57
57
|
| OAuth2 login | `simple_oauth` |
|
|
58
|
-
| Authoritative schemas | `
|
|
58
|
+
| Authoritative schemas (Drupal 10.1+/11) | `jsonapi_schema` |
|
|
59
|
+
| Authoritative schemas (legacy; 500s on D11/PHP 8.4) | `schemata`, `schemata_json_schema` |
|
|
59
60
|
| Canvas schemas | `canvas`, `jsonapi_sdc` |
|
|
60
61
|
| Display Builder schemas | `display_builder`, `display_builder_entity_view`, `jsonapi_sdc` |
|
|
61
62
|
|
|
62
|
-
dropsh works without
|
|
63
|
-
back to a shallow schema inferred from JSON:API sample records.
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
dropsh works without an authoritative-schema module. In that case, `dropsh
|
|
64
|
+
schema` falls back to a shallow schema inferred from JSON:API sample records.
|
|
65
|
+
Installing `jsonapi_schema` lets `@dropsh/plugin-jsonapi-schema` return a more
|
|
66
|
+
precise schema with Drupal's required fields, formats, and constraints — so
|
|
67
|
+
`create`/`update` reject malformed payloads client-side. On **Drupal 11 / PHP
|
|
68
|
+
8.4** prefer `jsonapi_schema`: the older `schemata_json_schema` endpoint returns
|
|
69
|
+
HTTP 500 there.
|
|
66
70
|
|
|
67
71
|
### Installing the Drupal modules
|
|
68
72
|
|
|
@@ -77,16 +81,21 @@ drush en jsonapi
|
|
|
77
81
|
composer require drupal/simple_oauth
|
|
78
82
|
drush en simple_oauth
|
|
79
83
|
|
|
80
|
-
# Optional: authoritative schemas (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
# Optional: authoritative schemas (Drupal 10.1+/11, PHP 8.4) — recommended
|
|
85
|
+
composer require drupal/jsonapi_schema
|
|
86
|
+
drush en jsonapi_schema
|
|
87
|
+
|
|
88
|
+
# Legacy alternative — the schemata endpoint returns HTTP 500 on
|
|
89
|
+
# Drupal 11 / PHP 8.4; prefer jsonapi_schema there.
|
|
90
|
+
# composer require drupal/schemata
|
|
91
|
+
# drush en schemata schemata_json_schema
|
|
92
|
+
# drush role:perm:add "content_editor" "access schemata data models"
|
|
85
93
|
```
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
endpoint
|
|
95
|
+
`jsonapi_schema` serves its routes under the JSON:API prefix (`/jsonapi/*`), so
|
|
96
|
+
the role/consumer used by dropsh needs no extra permission beyond JSON:API
|
|
97
|
+
access. (The legacy `schemata` endpoint instead needs `access schemata data
|
|
98
|
+
models` on that role.)
|
|
90
99
|
|
|
91
100
|
By default JSON:API only accepts read operations. To create, update, or delete
|
|
92
101
|
entities through dropsh, set **Accept all JSON:API create, read, update, and
|
|
@@ -103,7 +112,7 @@ store.
|
|
|
103
112
|
```js
|
|
104
113
|
import { basicAuthPlugin } from "dropsh";
|
|
105
114
|
import { oauth2Plugin } from "@dropsh/plugin-oauth2";
|
|
106
|
-
import {
|
|
115
|
+
import { jsonapiSchemaPlugin } from "@dropsh/plugin-jsonapi-schema";
|
|
107
116
|
|
|
108
117
|
export default {
|
|
109
118
|
site: {
|
|
@@ -121,7 +130,7 @@ export default {
|
|
|
121
130
|
client_id: "my-client",
|
|
122
131
|
token_url: "https://my-drupal.example.com/oauth/token",
|
|
123
132
|
}),
|
|
124
|
-
|
|
133
|
+
jsonapiSchemaPlugin(),
|
|
125
134
|
],
|
|
126
135
|
};
|
|
127
136
|
```
|
package/dist/src/core/config.js
CHANGED
|
@@ -1,13 +1,76 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { resolve } from "node:path";
|
|
2
3
|
import { pathToFileURL } from "node:url";
|
|
3
4
|
import { ConfigError } from "../errors.js";
|
|
4
5
|
function isRecord(v) {
|
|
5
6
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* A plugin descriptor is plain config data that names a plugin package as a
|
|
10
|
+
* string (`plugin`). This lets a committed config stay import-free: the config
|
|
11
|
+
* carries only data, and dropsh resolves + constructs the plugin from its own
|
|
12
|
+
* install — so nothing has to resolve the plugin package from the config file's
|
|
13
|
+
* own `import` statements (which resolve relative to the config file, not the CLI).
|
|
14
|
+
*/
|
|
15
|
+
function isPluginDescriptor(e) {
|
|
16
|
+
return isRecord(e) && typeof e.plugin === "string";
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a named plugin package the way ESLint resolves plugins: by string
|
|
20
|
+
* name, relative to the config file first, then the cwd, then dropsh's own
|
|
21
|
+
* install (so a plugin installed alongside dropsh — a dropsh dep, or globally
|
|
22
|
+
* next to a global dropsh, or via `npx -p`— is found). The config never imports
|
|
23
|
+
* the package itself.
|
|
24
|
+
*/
|
|
25
|
+
async function loadNamedPlugin(entry, configPath) {
|
|
26
|
+
const name = entry.plugin;
|
|
27
|
+
const bases = [
|
|
28
|
+
pathToFileURL(configPath).href,
|
|
29
|
+
pathToFileURL(`${process.cwd()}/`).href,
|
|
30
|
+
import.meta.url,
|
|
31
|
+
];
|
|
32
|
+
let resolved;
|
|
33
|
+
for (const base of bases) {
|
|
34
|
+
try {
|
|
35
|
+
resolved = createRequire(base).resolve(name);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// try the next base
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (resolved === undefined)
|
|
43
|
+
throw new ConfigError(`cannot resolve plugin '${name}'. Install it next to dropsh (project dep, dropsh dep, global next to a global dropsh, or 'npx -p dropsh -p ${name}').`);
|
|
44
|
+
let mod;
|
|
45
|
+
try {
|
|
46
|
+
mod = (await import(pathToFileURL(resolved).href));
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw new ConfigError(`cannot load plugin '${name}'`, { cause: String(err) });
|
|
50
|
+
}
|
|
51
|
+
const exportName = typeof entry.export === "string" ? entry.export : "default";
|
|
52
|
+
const factory = mod[exportName];
|
|
53
|
+
if (typeof factory !== "function")
|
|
54
|
+
throw new ConfigError(`plugin '${name}' has no callable export '${exportName}'`);
|
|
55
|
+
return factory(entry.with ?? entry.options);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Expands `plugins[]` entries: named-package descriptors are resolved + built
|
|
59
|
+
* into real plugins; already-constructed plugins pass through unchanged
|
|
60
|
+
* (backward compat).
|
|
61
|
+
*/
|
|
62
|
+
async function resolvePlugins(raw, configPath) {
|
|
63
|
+
if (!Array.isArray(raw))
|
|
64
|
+
return [];
|
|
65
|
+
return Promise.all(raw.map((entry) => isPluginDescriptor(entry)
|
|
66
|
+
? loadNamedPlugin(entry, configPath)
|
|
67
|
+
: Promise.resolve(entry)));
|
|
68
|
+
}
|
|
7
69
|
export async function loadConfig(filePath) {
|
|
70
|
+
const configPath = resolve(filePath);
|
|
8
71
|
let mod;
|
|
9
72
|
try {
|
|
10
|
-
mod = await import(pathToFileURL(
|
|
73
|
+
mod = await import(pathToFileURL(configPath).href);
|
|
11
74
|
}
|
|
12
75
|
catch (err) {
|
|
13
76
|
throw new ConfigError(`Cannot load config file: ${filePath}`, { cause: String(err) });
|
|
@@ -21,7 +84,7 @@ export async function loadConfig(filePath) {
|
|
|
21
84
|
if (typeof site.base_url !== "string" || site.base_url.length === 0)
|
|
22
85
|
throw new ConfigError("site.base_url required");
|
|
23
86
|
const defaults = isRecord(raw.defaults) ? raw.defaults : {};
|
|
24
|
-
const plugins =
|
|
87
|
+
const plugins = await resolvePlugins(raw.plugins, configPath);
|
|
25
88
|
return {
|
|
26
89
|
site: {
|
|
27
90
|
base_url: site.base_url,
|
|
@@ -4,7 +4,13 @@ export async function fetchJsonSchema(deps) {
|
|
|
4
4
|
const { entity, bundle } = deps;
|
|
5
5
|
const target = { entity_type: entity, bundle };
|
|
6
6
|
const plugins = deps.plugins ?? [];
|
|
7
|
-
const ctx = {
|
|
7
|
+
const ctx = {
|
|
8
|
+
http: deps.http,
|
|
9
|
+
auth: deps.auth,
|
|
10
|
+
baseUrl: deps.baseUrl,
|
|
11
|
+
jsonapiPrefix: deps.jsonapiPrefix,
|
|
12
|
+
warn: deps.warn,
|
|
13
|
+
};
|
|
8
14
|
// Build heuristic base schema
|
|
9
15
|
let heuristicResult;
|
|
10
16
|
try {
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import draft6MetaSchema from "ajv/dist/refs/json-schema-draft-06.json" with { type: "json" };
|
|
3
3
|
import draft7MetaSchema from "ajv/dist/refs/json-schema-draft-07.json" with { type: "json" };
|
|
4
|
+
import addFormats from "ajv-formats";
|
|
4
5
|
import { ValidationError } from "../../errors.js";
|
|
5
6
|
export function validatePayload(schema, payload, target) {
|
|
6
7
|
const ajv = new Ajv({ allErrors: true, strict: false, validateSchema: false, logger: false });
|
|
8
|
+
// Enforce standard `format` keywords (uri, date-time, uuid, email, …) so a
|
|
9
|
+
// constraint-bearing schema (e.g. from the jsonapi-schema plugin) rejects a
|
|
10
|
+
// malformed value — such as an SCP-style git remote where a URI is required —
|
|
11
|
+
// client-side, before the request is sent. Schemas without `format` are
|
|
12
|
+
// unaffected.
|
|
13
|
+
addFormats(ajv);
|
|
7
14
|
// Schemata (Drupal module) may emit Draft-04 schemas that use `id` instead of `$id`.
|
|
8
15
|
// AJV v8 hard-throws on `id`; removing the keyword makes AJV silently ignore it.
|
|
9
16
|
ajv.removeKeyword("id");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dropsh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"ajv": "^8.18.0",
|
|
37
|
+
"ajv-formats": "^3.0.1",
|
|
37
38
|
"commander": "^12.1.0",
|
|
38
39
|
"drupal-jsonapi-params": "^3.0.1",
|
|
39
40
|
"open": "^11.0.0"
|