dropsh 0.5.8 → 0.6.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/dist/src/core/auth/types.d.ts +17 -4
- package/dist/src/core/jsonapi/client.d.ts +3 -1
- package/dist/src/core/jsonapi/client.js +31 -13
- package/dist/src/core/plugin.d.ts +8 -1
- package/dist/src/core/request-hooks.d.ts +4 -0
- package/dist/src/core/request-hooks.js +26 -0
- package/dist/src/errors.d.ts +3 -0
- package/dist/src/errors.js +12 -0
- package/dist/src/index.js +8 -0
- package/dist/src/plugin-api.d.ts +3 -3
- package/dist/src/plugin-api.js +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import type { HttpClient, HttpRequest } from "../http.js";
|
|
2
|
+
/**
|
|
3
|
+
* Outcome of a reactive credential renewal. On failure the renewal cause is
|
|
4
|
+
* carried back so the caller can surface *why* renewal failed instead of the
|
|
5
|
+
* bare, unexplained 401 that first triggered it.
|
|
6
|
+
*/
|
|
7
|
+
export type RenewOutcome = {
|
|
8
|
+
ok: true;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
};
|
|
2
13
|
export interface AuthAdapter {
|
|
3
14
|
apply(req: HttpRequest): Promise<HttpRequest>;
|
|
4
15
|
/**
|
|
5
16
|
* Force a credential refresh after the server rejected the current token (401),
|
|
6
|
-
* then persist it.
|
|
7
|
-
* once)
|
|
8
|
-
*
|
|
17
|
+
* then persist it. Resolves `{ ok: true }` when renewal succeeded (caller
|
|
18
|
+
* retries the request once) or `{ ok: false, cause }` when it could not renew,
|
|
19
|
+
* where `cause` is the error explaining the failure (the caller surfaces it in
|
|
20
|
+
* place of the original 401). Providers that cannot renew (e.g. basic auth)
|
|
21
|
+
* omit this method.
|
|
9
22
|
*/
|
|
10
|
-
renew?(): Promise<
|
|
23
|
+
renew?(): Promise<RenewOutcome>;
|
|
11
24
|
}
|
|
12
25
|
/** Provider-specific opaque session payload persisted to the state dir. */
|
|
13
26
|
export type AuthSession = Record<string, unknown>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { DrupalJsonApiParams } from "drupal-jsonapi-params";
|
|
2
2
|
import type { AuthAdapter } from "../auth/types.js";
|
|
3
|
-
import type { HttpClient } from "../http.js";
|
|
3
|
+
import type { HttpClient, HttpRequest } from "../http.js";
|
|
4
4
|
import { type Collection } from "./collection.js";
|
|
5
5
|
import { type Resource } from "./resource.js";
|
|
6
|
+
export type DropSHOperation = "read" | "search" | "create" | "update" | "delete" | "upload";
|
|
6
7
|
/** Attributes/relationships payload accepted by create/update/upsert. */
|
|
7
8
|
export interface ResourceWriteBody {
|
|
8
9
|
attributes?: Record<string, unknown>;
|
|
@@ -35,5 +36,6 @@ export interface JsonApiOptions {
|
|
|
35
36
|
prefix: string;
|
|
36
37
|
http: HttpClient;
|
|
37
38
|
auth: AuthAdapter;
|
|
39
|
+
alterRequest?: (req: HttpRequest, operation: DropSHOperation, entityType?: string, bundle?: string) => Promise<HttpRequest>;
|
|
38
40
|
}
|
|
39
41
|
export declare function createJsonApiClient(opts: JsonApiOptions): JsonApiClient;
|
|
@@ -12,27 +12,45 @@ const JSONAPI_HEADERS = {
|
|
|
12
12
|
Accept: "application/vnd.api+json",
|
|
13
13
|
"Content-Type": "application/vnd.api+json",
|
|
14
14
|
};
|
|
15
|
+
function targetFromPath(path) {
|
|
16
|
+
const [entityType, bundle] = path.split("/").filter(Boolean);
|
|
17
|
+
return {
|
|
18
|
+
...(entityType !== undefined ? { entityType } : {}),
|
|
19
|
+
...(bundle !== undefined ? { bundle } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function getOperation(path) {
|
|
23
|
+
const segments = path.split("/").filter(Boolean);
|
|
24
|
+
return segments.length === 0 || segments.length >= 3 ? "read" : "search";
|
|
25
|
+
}
|
|
15
26
|
export function createJsonApiClient(opts) {
|
|
16
|
-
async function send(method,
|
|
27
|
+
async function send(method, path, operation, body, extraHeaders = {}, queryString = "") {
|
|
17
28
|
const reqBase = {
|
|
18
29
|
method,
|
|
19
|
-
url,
|
|
30
|
+
url: joinUrl(opts.baseUrl, opts.prefix, path) + queryString,
|
|
20
31
|
headers: { ...JSONAPI_HEADERS, ...extraHeaders },
|
|
21
32
|
};
|
|
22
33
|
const req = body !== undefined ? { ...reqBase, body } : reqBase;
|
|
34
|
+
const authenticatedRequest = await opts.auth.apply(req);
|
|
35
|
+
const { entityType, bundle } = targetFromPath(path);
|
|
36
|
+
const alteredRequest = opts.alterRequest
|
|
37
|
+
? await opts.alterRequest(authenticatedRequest, operation, entityType, bundle)
|
|
38
|
+
: authenticatedRequest;
|
|
23
39
|
let res;
|
|
24
40
|
try {
|
|
25
|
-
res = await opts.http.send(
|
|
41
|
+
res = await opts.http.send(alteredRequest);
|
|
26
42
|
}
|
|
27
43
|
catch (err) {
|
|
28
44
|
// The server rejected the token (401). Ask the auth adapter to renew and,
|
|
29
|
-
// if it could, retry the request exactly once with a fresh token.
|
|
45
|
+
// if it could, retry the request exactly once with a fresh token. If it
|
|
46
|
+
// could not, surface the renewal cause (e.g. an unconfigured secret) so
|
|
47
|
+
// the caller sees *why* renewal failed, not just the original 401.
|
|
30
48
|
if (!(err instanceof HttpError) || err.status !== 401 || !opts.auth.renew)
|
|
31
49
|
throw err;
|
|
32
|
-
const
|
|
33
|
-
if (!
|
|
34
|
-
throw err;
|
|
35
|
-
res = await opts.http.send(await opts.auth.apply(
|
|
50
|
+
const outcome = await opts.auth.renew();
|
|
51
|
+
if (!outcome.ok)
|
|
52
|
+
throw outcome.cause ?? err;
|
|
53
|
+
res = await opts.http.send(await opts.auth.apply(alteredRequest));
|
|
36
54
|
}
|
|
37
55
|
if (res.status === 204 || res.body.length === 0)
|
|
38
56
|
return { ok: true };
|
|
@@ -41,19 +59,19 @@ export function createJsonApiClient(opts) {
|
|
|
41
59
|
const client = {
|
|
42
60
|
async get(path, params) {
|
|
43
61
|
const qs = params ? `?${params.getQueryString()}` : "";
|
|
44
|
-
return send("GET",
|
|
62
|
+
return send("GET", path, getOperation(path), undefined, {}, qs);
|
|
45
63
|
},
|
|
46
64
|
async post(path, body) {
|
|
47
|
-
return send("POST",
|
|
65
|
+
return send("POST", path, "create", JSON.stringify(body));
|
|
48
66
|
},
|
|
49
67
|
async patch(path, body) {
|
|
50
|
-
return send("PATCH",
|
|
68
|
+
return send("PATCH", path, "update", JSON.stringify(body));
|
|
51
69
|
},
|
|
52
70
|
async delete(path) {
|
|
53
|
-
return send("DELETE",
|
|
71
|
+
return send("DELETE", path, "delete");
|
|
54
72
|
},
|
|
55
73
|
async upload(path, filename, data) {
|
|
56
|
-
return send("POST",
|
|
74
|
+
return send("POST", path, "upload", data, {
|
|
57
75
|
"Content-Type": "application/octet-stream",
|
|
58
76
|
"Content-Disposition": `file; filename="${filename}"`,
|
|
59
77
|
});
|
|
@@ -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
|
+
}
|
package/dist/src/errors.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/errors.js
CHANGED
|
@@ -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
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createPrompt } from "./core/cli/prompt.js";
|
|
|
17
17
|
import { loadConfig } from "./core/config.js";
|
|
18
18
|
import { createHttpClient } from "./core/http.js";
|
|
19
19
|
import { createJsonApiClient } from "./core/jsonapi/client.js";
|
|
20
|
+
import { composeRequestHooks } from "./core/request-hooks.js";
|
|
20
21
|
import { fetchJsonSchema } from "./core/schema/jsonschema-source.js";
|
|
21
22
|
import { toOperationVariant } from "./core/schema/to-jsonschema.js";
|
|
22
23
|
import { validatePayload } from "./core/schema/validate.js";
|
|
@@ -119,11 +120,18 @@ async function defaultContext(configPath, profile) {
|
|
|
119
120
|
now: Date.now,
|
|
120
121
|
...(profile !== undefined ? { profile } : {}),
|
|
121
122
|
});
|
|
123
|
+
const pluginContext = {
|
|
124
|
+
http,
|
|
125
|
+
auth,
|
|
126
|
+
baseUrl: cfg.site.base_url,
|
|
127
|
+
jsonapiPrefix: cfg.site.jsonapi_prefix,
|
|
128
|
+
};
|
|
122
129
|
const client = createJsonApiClient({
|
|
123
130
|
baseUrl: cfg.site.base_url,
|
|
124
131
|
prefix: cfg.site.jsonapi_prefix,
|
|
125
132
|
http,
|
|
126
133
|
auth,
|
|
134
|
+
alterRequest: composeRequestHooks(cfg.plugins, pluginContext),
|
|
127
135
|
});
|
|
128
136
|
return {
|
|
129
137
|
client,
|
package/dist/src/plugin-api.d.ts
CHANGED
|
@@ -7,12 +7,12 @@ export type { Config, SiteConfig } from "./core/config.js";
|
|
|
7
7
|
export { loadConfig } from "./core/config.js";
|
|
8
8
|
export type { HttpClient, HttpRequest } from "./core/http.js";
|
|
9
9
|
export { createHttpClient } from "./core/http.js";
|
|
10
|
-
export type { JsonApiClient, JsonApiOptions, ResourceWriteBody, } from "./core/jsonapi/client.js";
|
|
10
|
+
export type { DropSHOperation, JsonApiClient, JsonApiOptions, ResourceWriteBody, } from "./core/jsonapi/client.js";
|
|
11
11
|
export { createJsonApiClient } from "./core/jsonapi/client.js";
|
|
12
12
|
export type { Collection, FilterOp } from "./core/jsonapi/collection.js";
|
|
13
13
|
export type { JsonApiResourceObject, Resource, } from "./core/jsonapi/resource.js";
|
|
14
14
|
export type { JsonApiDocument, JsonApiResource } from "./core/jsonapi/types.js";
|
|
15
|
-
export type { DropSHPlugin, PluginContext, PluginDescriptor, SchemaOperation, } from "./core/plugin.js";
|
|
15
|
+
export type { DropSHPlugin, PluginContext, PluginDescriptor, RequestContext, SchemaOperation, } from "./core/plugin.js";
|
|
16
16
|
export { composePlugins } from "./core/plugin.js";
|
|
17
|
-
export { AuthError, ConfigError, HttpError } from "./errors.js";
|
|
17
|
+
export { AuthError, ConfigError, HttpError, PluginError } from "./errors.js";
|
|
18
18
|
export type { CommandContext } from "./index.js";
|
package/dist/src/plugin-api.js
CHANGED
|
@@ -4,4 +4,4 @@ export { loadConfig } from "./core/config.js";
|
|
|
4
4
|
export { createHttpClient } from "./core/http.js";
|
|
5
5
|
export { createJsonApiClient } from "./core/jsonapi/client.js";
|
|
6
6
|
export { composePlugins } from "./core/plugin.js";
|
|
7
|
-
export { AuthError, ConfigError, HttpError } from "./errors.js";
|
|
7
|
+
export { AuthError, ConfigError, HttpError, PluginError } from "./errors.js";
|