@freelygive/canvas-jsonapi 0.0.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 +157 -0
- package/package.json +30 -0
- package/src/client.js +181 -0
- package/src/create.js +120 -0
- package/src/delete.js +68 -0
- package/src/entity.js +233 -0
- package/src/get.js +95 -0
- package/src/index.js +121 -0
- package/src/list.js +197 -0
- package/src/update.js +140 -0
- package/src/upload-document.js +125 -0
- package/src/upload-image.js +122 -0
- package/src/upload-video.js +127 -0
- package/src/utils.js +147 -0
- package/src/uuid.js +10 -0
- package/src/validate.js +298 -0
- package/src/whoami.js +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# @freelygive/canvas-jsonapi
|
|
2
|
+
|
|
3
|
+
CLI for managing content in Drupal Canvas / Acquia Source via JSON:API.
|
|
4
|
+
|
|
5
|
+
List, fetch, create, update, and delete pages, media, and other content
|
|
6
|
+
entities. Upload images, videos, and documents. Validate component inputs
|
|
7
|
+
against local `component.yml` definitions before sending to the server.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
Run directly with `npx` (no install required):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @freelygive/canvas-jsonapi <command> [args...]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or inside a DDEV environment:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
ddev npx @freelygive/canvas-jsonapi <command> [args...]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
### Component directory
|
|
26
|
+
|
|
27
|
+
Create a `canvas.config.json` in your project root (shared with
|
|
28
|
+
`@drupal-canvas/cli`):
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"componentDir": "./src/components"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This tells the CLI where to find `component.yml` files for input validation.
|
|
37
|
+
|
|
38
|
+
### API credentials
|
|
39
|
+
|
|
40
|
+
Create a `.env` file in your project root:
|
|
41
|
+
|
|
42
|
+
```env
|
|
43
|
+
CANVAS_SITE_URL=https://your-site.acquia.site
|
|
44
|
+
CANVAS_JSONAPI_PREFIX=api
|
|
45
|
+
CANVAS_CLIENT_ID=cli
|
|
46
|
+
CANVAS_CLIENT_SECRET=your-secret
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| Variable | Required | Default | Description |
|
|
50
|
+
| ----------------------- | -------- | --------- | ------------------------------------ |
|
|
51
|
+
| `CANVAS_SITE_URL` | Yes | | Base URL of Acquia Source site |
|
|
52
|
+
| `CANVAS_JSONAPI_PREFIX` | No | `jsonapi` | API path prefix |
|
|
53
|
+
| `CANVAS_CLIENT_ID` | No | | OAuth client ID |
|
|
54
|
+
| `CANVAS_CLIENT_SECRET` | No | | OAuth client secret |
|
|
55
|
+
| `CONTENT_OAUTH_SCOPE` | No | | OAuth scope override |
|
|
56
|
+
| `CONTENT_NO_AUTH` | No | | Set to `true` to skip authentication |
|
|
57
|
+
|
|
58
|
+
The component directory is resolved in this order:
|
|
59
|
+
|
|
60
|
+
1. `componentDir` in `canvas.config.json`
|
|
61
|
+
2. `CANVAS_COMPONENT_DIR` env var (legacy fallback)
|
|
62
|
+
3. `./src/components` (default)
|
|
63
|
+
|
|
64
|
+
## Commands
|
|
65
|
+
|
|
66
|
+
### List content
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx @freelygive/canvas-jsonapi list <type>
|
|
70
|
+
npx @freelygive/canvas-jsonapi list --types # Discover available types
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Get content
|
|
74
|
+
|
|
75
|
+
Fetch and save content items locally to `content/<type>/<uuid>.json`:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx @freelygive/canvas-jsonapi get <type> <uuid> [<uuid>...]
|
|
79
|
+
npx @freelygive/canvas-jsonapi get <type> <uuid> --include <relationships>
|
|
80
|
+
npx @freelygive/canvas-jsonapi get <file-path> # Refresh from existing file
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Create content
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx @freelygive/canvas-jsonapi create <file-path>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Update content
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx @freelygive/canvas-jsonapi update <file-path>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Delete content
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx @freelygive/canvas-jsonapi delete <type> <uuid> [<uuid>...]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Upload media
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx @freelygive/canvas-jsonapi upload-image <file-path> [alt-text]
|
|
105
|
+
npx @freelygive/canvas-jsonapi upload-video <file-path> [name]
|
|
106
|
+
npx @freelygive/canvas-jsonapi upload-document <file-path> [name]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Utilities
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx @freelygive/canvas-jsonapi uuid [count] # Generate random UUIDs
|
|
113
|
+
npx @freelygive/canvas-jsonapi whoami # Check OAuth status
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Local content storage
|
|
117
|
+
|
|
118
|
+
Content is saved to `content/` in the current working directory:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
content/
|
|
122
|
+
page/
|
|
123
|
+
<uuid>.<title-slug>.json
|
|
124
|
+
media--image/
|
|
125
|
+
<uuid>.<filename>.json
|
|
126
|
+
media--document/
|
|
127
|
+
<uuid>.<name>.json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Add `content/` to your `.gitignore`.
|
|
131
|
+
|
|
132
|
+
## Component validation
|
|
133
|
+
|
|
134
|
+
Before creating or updating pages, component inputs are validated against
|
|
135
|
+
`component.yml` files found via `canvas.config.json`. This catches type errors
|
|
136
|
+
and missing required props before the API returns a generic 503.
|
|
137
|
+
|
|
138
|
+
## AI assistant usage
|
|
139
|
+
|
|
140
|
+
When using this tool with an AI assistant (e.g., Claude Code), add the following
|
|
141
|
+
to your `CLAUDE.md` or equivalent instructions file:
|
|
142
|
+
|
|
143
|
+
```markdown
|
|
144
|
+
## Content Management
|
|
145
|
+
|
|
146
|
+
Use `npx @freelygive/canvas-jsonapi` (or `ddev npx @freelygive/canvas-jsonapi`
|
|
147
|
+
in DDEV) to manage content in Acquia Source.
|
|
148
|
+
|
|
149
|
+
Available commands: list, get, create, update, delete, upload-image,
|
|
150
|
+
upload-video, upload-document, uuid, whoami.
|
|
151
|
+
|
|
152
|
+
Run `npx @freelygive/canvas-jsonapi` with no arguments for full usage.
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
GPL-2.0-or-later
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@freelygive/canvas-jsonapi",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "CLI for managing content in Drupal Canvas / Acquia Source via JSON:API.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"canvas-jsonapi": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"dotenv": "^16.0.0",
|
|
18
|
+
"drupal-jsonapi-params": "^3.0.0",
|
|
19
|
+
"js-yaml": "^4.1.1"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"drupal",
|
|
23
|
+
"canvas",
|
|
24
|
+
"acquia",
|
|
25
|
+
"jsonapi",
|
|
26
|
+
"cms",
|
|
27
|
+
"content-management"
|
|
28
|
+
],
|
|
29
|
+
"license": "GPL-2.0-or-later"
|
|
30
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
/**
|
|
3
|
+
* API client utilities for JSON:API access.
|
|
4
|
+
* Uses environment variables from .env file.
|
|
5
|
+
*
|
|
6
|
+
* Required env vars:
|
|
7
|
+
* - CANVAS_SITE_URL: Base URL of Acquia Source site
|
|
8
|
+
*
|
|
9
|
+
* Optional env vars:
|
|
10
|
+
* - CANVAS_JSONAPI_PREFIX: API prefix (default: "jsonapi")
|
|
11
|
+
* - CANVAS_CLIENT_ID: OAuth client ID (if authentication required)
|
|
12
|
+
* - CANVAS_CLIENT_SECRET: OAuth client secret (if authentication required)
|
|
13
|
+
* - CONTENT_OAUTH_SCOPE: OAuth scope for content access
|
|
14
|
+
* - CONTENT_NO_AUTH: Set to "true" to disable authentication
|
|
15
|
+
*/
|
|
16
|
+
import { config } from 'dotenv';
|
|
17
|
+
|
|
18
|
+
// Load environment variables
|
|
19
|
+
config();
|
|
20
|
+
|
|
21
|
+
// Cache for OAuth token
|
|
22
|
+
let tokenCache = null;
|
|
23
|
+
|
|
24
|
+
// Cache for JSON:API endpoint mapping
|
|
25
|
+
let endpointMapCache = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the site base URL
|
|
29
|
+
* @returns {string} Site base URL
|
|
30
|
+
*/
|
|
31
|
+
export function getSiteUrl() {
|
|
32
|
+
const baseUrl = process.env.CANVAS_SITE_URL;
|
|
33
|
+
if (!baseUrl) {
|
|
34
|
+
throw new Error('Missing required environment variable: CANVAS_SITE_URL');
|
|
35
|
+
}
|
|
36
|
+
return baseUrl.replace(/\/$/, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the API base URL with prefix
|
|
41
|
+
* @returns {string} Full API base URL
|
|
42
|
+
*/
|
|
43
|
+
export function getApiBaseUrl() {
|
|
44
|
+
const apiPrefix = process.env.CANVAS_JSONAPI_PREFIX || 'jsonapi';
|
|
45
|
+
return `${getSiteUrl()}/${apiPrefix}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get OAuth access token using client credentials flow
|
|
50
|
+
* @returns {Promise<string|null>} Access token or null if auth disabled
|
|
51
|
+
*/
|
|
52
|
+
async function getAccessToken() {
|
|
53
|
+
const noAuth = process.env.CONTENT_NO_AUTH === 'true';
|
|
54
|
+
if (noAuth) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const clientId = process.env.CANVAS_CLIENT_ID;
|
|
59
|
+
const clientSecret = process.env.CANVAS_CLIENT_SECRET;
|
|
60
|
+
|
|
61
|
+
if (!clientId || !clientSecret) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check cache
|
|
66
|
+
if (tokenCache && tokenCache.expiresAt > Date.now()) {
|
|
67
|
+
return tokenCache.accessToken;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Request new token
|
|
71
|
+
const tokenUrl = `${getSiteUrl()}/oauth/token`;
|
|
72
|
+
const params = new URLSearchParams({
|
|
73
|
+
grant_type: 'client_credentials',
|
|
74
|
+
client_id: clientId,
|
|
75
|
+
client_secret: clientSecret,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const scope = process.env.CONTENT_OAUTH_SCOPE;
|
|
79
|
+
if (scope) {
|
|
80
|
+
params.append('scope', scope);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const response = await fetch(tokenUrl, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
86
|
+
body: params,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const text = await response.text();
|
|
91
|
+
throw new Error(`OAuth token request failed: ${response.status} ${text}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const data = await response.json();
|
|
95
|
+
|
|
96
|
+
// Cache token with 60 second buffer before expiry
|
|
97
|
+
tokenCache = {
|
|
98
|
+
accessToken: data.access_token,
|
|
99
|
+
expiresAt: Date.now() + (data.expires_in - 60) * 1000,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return tokenCache.accessToken;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Fetch with OAuth authentication
|
|
107
|
+
* @param {string} url - URL to fetch
|
|
108
|
+
* @param {RequestInit} [init] - Fetch options
|
|
109
|
+
* @returns {Promise<Response>} Fetch response
|
|
110
|
+
*/
|
|
111
|
+
export async function authenticatedFetch(url, init = {}) {
|
|
112
|
+
const token = await getAccessToken();
|
|
113
|
+
|
|
114
|
+
const headers = { ...init.headers };
|
|
115
|
+
if (token) {
|
|
116
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return fetch(url, { ...init, headers });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fetch and cache the JSON:API endpoint mapping from the API root.
|
|
124
|
+
* The mapping provides the correct URL for each entity type.
|
|
125
|
+
* @returns {Promise<Map<string, string>>} Map of type names to endpoint URLs
|
|
126
|
+
*/
|
|
127
|
+
export async function getEndpointMap() {
|
|
128
|
+
if (endpointMapCache) {
|
|
129
|
+
return endpointMapCache;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const response = await authenticatedFetch(getApiBaseUrl());
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(`Failed to fetch API root: ${response.status}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const data = await response.json();
|
|
138
|
+
const links = data.links || {};
|
|
139
|
+
|
|
140
|
+
// Build map from type name to endpoint URL
|
|
141
|
+
endpointMapCache = new Map();
|
|
142
|
+
for (const [typeName, linkInfo] of Object.entries(links)) {
|
|
143
|
+
if (typeName === 'self') continue;
|
|
144
|
+
const href = typeof linkInfo === 'string' ? linkInfo : linkInfo?.href;
|
|
145
|
+
if (href) {
|
|
146
|
+
endpointMapCache.set(typeName, href);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return endpointMapCache;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the API endpoint URL for an entity type.
|
|
155
|
+
* Fetches the endpoint mapping from the API root if not cached.
|
|
156
|
+
* @param {string} type - Entity type (e.g., "node--news_article", "taxonomy_term--categories")
|
|
157
|
+
* @returns {Promise<string>} The endpoint URL for the entity type
|
|
158
|
+
*/
|
|
159
|
+
export async function getEntityEndpoint(type) {
|
|
160
|
+
const map = await getEndpointMap();
|
|
161
|
+
const url = map.get(type);
|
|
162
|
+
|
|
163
|
+
if (!url) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Unknown entity type: ${type}. Use 'canvas-jsonapi list --types' to see available types.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return url;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the API URL for a specific entity by type and UUID.
|
|
174
|
+
* @param {string} type - Entity type (e.g., "node--news_article")
|
|
175
|
+
* @param {string} uuid - Entity UUID
|
|
176
|
+
* @returns {Promise<string>} The full URL for the entity
|
|
177
|
+
*/
|
|
178
|
+
export async function getEntityUrl(type, uuid) {
|
|
179
|
+
const endpoint = await getEntityEndpoint(type);
|
|
180
|
+
return `${endpoint}/${uuid}`;
|
|
181
|
+
}
|
package/src/create.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
/**
|
|
3
|
+
* Creates a new content item from a local JSON file.
|
|
4
|
+
*/
|
|
5
|
+
import { unlinkSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
import { authenticatedFetch, getEntityEndpoint } from './client.js';
|
|
8
|
+
import {
|
|
9
|
+
displayEntitySummary,
|
|
10
|
+
fetchEntity,
|
|
11
|
+
getLabelField,
|
|
12
|
+
stringifyComponentInputs,
|
|
13
|
+
} from './entity.js';
|
|
14
|
+
import { readContent, saveContent } from './utils.js';
|
|
15
|
+
import {
|
|
16
|
+
formatValidationErrors,
|
|
17
|
+
validateContentComponents,
|
|
18
|
+
} from './validate.js';
|
|
19
|
+
|
|
20
|
+
export async function createContent(filePath) {
|
|
21
|
+
if (!filePath) {
|
|
22
|
+
console.error('Usage: canvas-jsonapi create <file-path>');
|
|
23
|
+
console.error('Example: canvas-jsonapi create content/page/new-123.json');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Read local file
|
|
29
|
+
const data = readContent(filePath);
|
|
30
|
+
|
|
31
|
+
// Extract type from the data
|
|
32
|
+
const type = data.data?.type || data.type;
|
|
33
|
+
|
|
34
|
+
if (!type) {
|
|
35
|
+
throw new Error('File must contain data.type field');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Validate component inputs before sending to server
|
|
39
|
+
const validation = validateContentComponents(data);
|
|
40
|
+
if (!validation.valid) {
|
|
41
|
+
console.error(formatValidationErrors(validation.errors));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (validation.warnings?.length > 0) {
|
|
45
|
+
for (const warning of validation.warnings) {
|
|
46
|
+
console.warn(`Warning: ${warning}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Prepare create body - remove any existing id
|
|
51
|
+
const attributes = data.data?.attributes || data.attributes;
|
|
52
|
+
const createBody = {
|
|
53
|
+
data: {
|
|
54
|
+
type: type,
|
|
55
|
+
attributes: stringifyComponentInputs(attributes),
|
|
56
|
+
relationships: data.data?.relationships || data.relationships,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Remove id if present (API will generate one)
|
|
61
|
+
delete createBody.data.id;
|
|
62
|
+
|
|
63
|
+
// Send create request using the correct endpoint from API root
|
|
64
|
+
const url = await getEntityEndpoint(type);
|
|
65
|
+
const response = await authenticatedFetch(url, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': 'application/vnd.api+json' },
|
|
68
|
+
body: JSON.stringify(createBody),
|
|
69
|
+
});
|
|
70
|
+
const contentType = response.headers.get('content-type') || '';
|
|
71
|
+
|
|
72
|
+
// Handle non-JSON responses
|
|
73
|
+
if (
|
|
74
|
+
!contentType.includes('application/vnd.api+json') &&
|
|
75
|
+
!contentType.includes('application/json')
|
|
76
|
+
) {
|
|
77
|
+
const text = await response.text();
|
|
78
|
+
console.error(`HTTP ${response.status}: ${response.statusText}`);
|
|
79
|
+
console.error(text.slice(0, 500));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = await response.json();
|
|
84
|
+
|
|
85
|
+
// Check for errors in response
|
|
86
|
+
if (!response.ok || result.errors) {
|
|
87
|
+
console.error(`Error creating ${type}:`);
|
|
88
|
+
console.error(JSON.stringify(result, null, 2));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Extract the new UUID from the response
|
|
93
|
+
const newUuid = result.data?.id || result.id;
|
|
94
|
+
|
|
95
|
+
if (!newUuid) {
|
|
96
|
+
throw new Error('No UUID returned from API');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Remove the original temp file
|
|
100
|
+
try {
|
|
101
|
+
unlinkSync(filePath);
|
|
102
|
+
console.log(`Removed temporary file: ${filePath}`);
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore if file already gone
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Fetch the full entity
|
|
108
|
+
const fullEntity = await fetchEntity(type, newUuid);
|
|
109
|
+
|
|
110
|
+
// Save with the new UUID and label
|
|
111
|
+
const labelField = getLabelField(type);
|
|
112
|
+
const label = fullEntity.data?.attributes?.[labelField] || '(untitled)';
|
|
113
|
+
const newFilePath = saveContent(type, newUuid, fullEntity, label);
|
|
114
|
+
|
|
115
|
+
displayEntitySummary('Created', type, newUuid, fullEntity, newFilePath);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(`Error creating from ${filePath}:`, error.message);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/delete.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
/**
|
|
3
|
+
* Deletes content items from Acquia Source.
|
|
4
|
+
*/
|
|
5
|
+
import { unlinkSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
import { authenticatedFetch, getEntityUrl } from './client.js';
|
|
8
|
+
import { findContentByUuid } from './utils.js';
|
|
9
|
+
|
|
10
|
+
export async function deleteContent(args) {
|
|
11
|
+
if (!args || args.length < 2) {
|
|
12
|
+
console.error('Usage: canvas-jsonapi delete <type> <uuid> [<uuid>...]');
|
|
13
|
+
console.error('Example: canvas-jsonapi delete page abc-123-def');
|
|
14
|
+
console.error('Example: canvas-jsonapi delete media--image uuid1 uuid2');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const type = args[0];
|
|
19
|
+
const uuids = args.slice(1);
|
|
20
|
+
|
|
21
|
+
let hasError = false;
|
|
22
|
+
for (const uuid of uuids) {
|
|
23
|
+
try {
|
|
24
|
+
await deleteSingleContent(type, uuid);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`Error deleting ${type}/${uuid}:`, error.message);
|
|
27
|
+
hasError = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (hasError) {
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function deleteSingleContent(type, uuid) {
|
|
37
|
+
// Get the correct endpoint URL from the API root mapping
|
|
38
|
+
const url = await getEntityUrl(type, uuid);
|
|
39
|
+
|
|
40
|
+
const response = await authenticatedFetch(url, {
|
|
41
|
+
method: 'DELETE',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const contentType = response.headers.get('content-type') || '';
|
|
46
|
+
if (contentType.includes('json')) {
|
|
47
|
+
const result = await response.json();
|
|
48
|
+
throw new Error(JSON.stringify(result.errors || result, null, 2));
|
|
49
|
+
} else {
|
|
50
|
+
const text = await response.text();
|
|
51
|
+
throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Try to remove local file if it exists
|
|
56
|
+
const localFile = findContentByUuid(type, uuid);
|
|
57
|
+
if (localFile) {
|
|
58
|
+
try {
|
|
59
|
+
unlinkSync(localFile);
|
|
60
|
+
console.log(`Deleted: ${uuid}`);
|
|
61
|
+
console.log(` Removed local file: ${localFile}`);
|
|
62
|
+
} catch {
|
|
63
|
+
console.log(`Deleted: ${uuid}`);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
console.log(`Deleted: ${uuid}`);
|
|
67
|
+
}
|
|
68
|
+
}
|