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 | `schemata`, `schemata_json_schema` |
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 the Schemata modules. In that case, `dropsh schema` falls
63
- back to a shallow schema inferred from JSON:API sample records. Installing
64
- `schemata` and `schemata_json_schema` lets `@dropsh/plugin-schemata` return a
65
- more precise schema with Drupal's required fields and constraints.
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 (schemata_json_schema is a submodule
81
- # of the schemata project)
82
- composer require drupal/schemata
83
- drush en schemata schemata_json_schema
84
- drush role:perm:add "content_editor" "access schemata data models"
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
- Grant `access schemata data models` to the role used by dropsh. For OAuth2, that
88
- means the user role or client credential access context that reads the Schemata
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 { schemataPlugin } from "@dropsh/plugin-schemata";
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
- schemataPlugin(),
133
+ jsonapiSchemaPlugin(),
125
134
  ],
126
135
  };
127
136
  ```
@@ -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(resolve(filePath)).href);
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 = Array.isArray(raw.plugins) ? raw.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 = { http: deps.http, auth: deps.auth, baseUrl: deps.baseUrl };
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.3",
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"