@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/src/update.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
/**
|
|
3
|
+
* Updates a content item from a local JSON file.
|
|
4
|
+
*/
|
|
5
|
+
import { authenticatedFetch, getEntityUrl } from './client.js';
|
|
6
|
+
import {
|
|
7
|
+
displayEntitySummary,
|
|
8
|
+
fetchEntity,
|
|
9
|
+
getLabelField,
|
|
10
|
+
stringifyComponentInputs,
|
|
11
|
+
} from './entity.js';
|
|
12
|
+
import { readContent, saveContent } from './utils.js';
|
|
13
|
+
import {
|
|
14
|
+
formatValidationErrors,
|
|
15
|
+
validateContentComponents,
|
|
16
|
+
} from './validate.js';
|
|
17
|
+
|
|
18
|
+
export async function updateContent(filePath) {
|
|
19
|
+
if (!filePath) {
|
|
20
|
+
console.error('Usage: canvas-jsonapi update <file-path>');
|
|
21
|
+
console.error('Example: canvas-jsonapi update content/page/abc-123.json');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Read local file
|
|
27
|
+
const data = readContent(filePath);
|
|
28
|
+
|
|
29
|
+
// Extract type and UUID from the data itself (more reliable)
|
|
30
|
+
const type = data.data?.type || data.type;
|
|
31
|
+
const uuid = data.data?.id || data.id;
|
|
32
|
+
|
|
33
|
+
if (!type || !uuid) {
|
|
34
|
+
throw new Error('File must contain data.type and data.id fields');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate component inputs before sending to server
|
|
38
|
+
const validation = validateContentComponents(data);
|
|
39
|
+
if (!validation.valid) {
|
|
40
|
+
console.error(formatValidationErrors(validation.errors));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
if (validation.warnings?.length > 0) {
|
|
44
|
+
for (const warning of validation.warnings) {
|
|
45
|
+
console.warn(`Warning: ${warning}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Prepare update body - ensure proper JSON:API format
|
|
50
|
+
const attributes = data.data?.attributes || data.attributes;
|
|
51
|
+
|
|
52
|
+
// Strip read-only fields that the API won't accept on PATCH
|
|
53
|
+
const readOnlyFields = [
|
|
54
|
+
'changed',
|
|
55
|
+
'created',
|
|
56
|
+
'revision_created',
|
|
57
|
+
'default_langcode',
|
|
58
|
+
'revision_translation_affected',
|
|
59
|
+
'metatags',
|
|
60
|
+
'path',
|
|
61
|
+
];
|
|
62
|
+
const writableAttributes = { ...attributes };
|
|
63
|
+
for (const field of readOnlyFields) {
|
|
64
|
+
delete writableAttributes[field];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const updateBody = {
|
|
68
|
+
data: {
|
|
69
|
+
type: type,
|
|
70
|
+
id: uuid,
|
|
71
|
+
attributes: stringifyComponentInputs(writableAttributes),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Include writable relationships (e.g., brands, categories, banner_image)
|
|
76
|
+
const relationships = data.data?.relationships || data.relationships;
|
|
77
|
+
if (relationships) {
|
|
78
|
+
const readOnlyRelationships = [
|
|
79
|
+
'node_type',
|
|
80
|
+
'revision_uid',
|
|
81
|
+
'uid',
|
|
82
|
+
'vid',
|
|
83
|
+
'revision_user',
|
|
84
|
+
'parent',
|
|
85
|
+
'owner',
|
|
86
|
+
];
|
|
87
|
+
const writableRelationships = {};
|
|
88
|
+
for (const [key, value] of Object.entries(relationships)) {
|
|
89
|
+
if (!readOnlyRelationships.includes(key) && value?.data !== undefined) {
|
|
90
|
+
writableRelationships[key] = { data: value.data };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (Object.keys(writableRelationships).length > 0) {
|
|
94
|
+
updateBody.data.relationships = writableRelationships;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Send update request using the correct endpoint from API root
|
|
99
|
+
const url = await getEntityUrl(type, uuid);
|
|
100
|
+
const response = await authenticatedFetch(url, {
|
|
101
|
+
method: 'PATCH',
|
|
102
|
+
headers: { 'Content-Type': 'application/vnd.api+json' },
|
|
103
|
+
body: JSON.stringify(updateBody),
|
|
104
|
+
});
|
|
105
|
+
const contentType = response.headers.get('content-type') || '';
|
|
106
|
+
|
|
107
|
+
// Handle non-JSON responses
|
|
108
|
+
if (
|
|
109
|
+
!contentType.includes('application/vnd.api+json') &&
|
|
110
|
+
!contentType.includes('application/json')
|
|
111
|
+
) {
|
|
112
|
+
const text = await response.text();
|
|
113
|
+
console.error(`HTTP ${response.status}: ${response.statusText}`);
|
|
114
|
+
console.error(text.slice(0, 500));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = await response.json();
|
|
119
|
+
|
|
120
|
+
// Check for errors in response
|
|
121
|
+
if (!response.ok || result.errors) {
|
|
122
|
+
console.error(`Error updating ${type}/${uuid}:`);
|
|
123
|
+
console.error(JSON.stringify(result, null, 2));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fetch the full entity to get complete data
|
|
128
|
+
const fullEntity = await fetchEntity(type, uuid);
|
|
129
|
+
|
|
130
|
+
// Save the updated entity with label
|
|
131
|
+
const labelField = getLabelField(type);
|
|
132
|
+
const label = fullEntity.data?.attributes?.[labelField] || '(untitled)';
|
|
133
|
+
const savedPath = saveContent(type, uuid, fullEntity, label);
|
|
134
|
+
|
|
135
|
+
displayEntitySummary('Updated', type, uuid, fullEntity, savedPath);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(`Error updating from ${filePath}:`, error.message);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
/**
|
|
3
|
+
* Uploads a document file and creates a media entity.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { basename } from 'path';
|
|
7
|
+
|
|
8
|
+
import { authenticatedFetch, getApiBaseUrl } from './client.js';
|
|
9
|
+
import { displayEntitySummary, fetchEntity } from './entity.js';
|
|
10
|
+
import { saveContent } from './utils.js';
|
|
11
|
+
|
|
12
|
+
export async function uploadDocument(filePath, name) {
|
|
13
|
+
if (!filePath) {
|
|
14
|
+
console.error('Usage: canvas-jsonapi upload-document <file-path> [name]');
|
|
15
|
+
console.error(
|
|
16
|
+
'Example: canvas-jsonapi upload-document fonts/AbsaraSans.woff2 "AbsaraSans Regular"',
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const filename = basename(filePath);
|
|
23
|
+
const fileData = readFileSync(filePath);
|
|
24
|
+
const mediaName = name || filename;
|
|
25
|
+
|
|
26
|
+
console.log(`Uploading: ${filename}`);
|
|
27
|
+
|
|
28
|
+
// Step 1: Upload the file binary
|
|
29
|
+
const fileUploadUrl = `${getApiBaseUrl()}/media/document/media_file`;
|
|
30
|
+
const fileResponse = await authenticatedFetch(fileUploadUrl, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/octet-stream',
|
|
34
|
+
'Content-Disposition': `file; filename="${filename}"`,
|
|
35
|
+
Accept: 'application/vnd.api+json',
|
|
36
|
+
},
|
|
37
|
+
body: fileData,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!fileResponse.ok) {
|
|
41
|
+
const text = await fileResponse.text();
|
|
42
|
+
console.error(`File upload failed: ${fileResponse.status}`);
|
|
43
|
+
console.error(text.slice(0, 1000));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fileResult = await fileResponse.json();
|
|
48
|
+
const fileId = fileResult.data?.id;
|
|
49
|
+
const fileUrl = fileResult.data?.attributes?.uri?.url;
|
|
50
|
+
|
|
51
|
+
if (!fileId) {
|
|
52
|
+
throw new Error('No file ID returned from upload');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Step 2: Create media entity
|
|
56
|
+
const mediaUrl = `${getApiBaseUrl()}/media/document`;
|
|
57
|
+
const mediaBody = {
|
|
58
|
+
data: {
|
|
59
|
+
type: 'media--document',
|
|
60
|
+
attributes: {
|
|
61
|
+
name: mediaName,
|
|
62
|
+
status: true,
|
|
63
|
+
},
|
|
64
|
+
relationships: {
|
|
65
|
+
media_file: {
|
|
66
|
+
data: {
|
|
67
|
+
type: 'file--file',
|
|
68
|
+
id: fileId,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const mediaResponse = await authenticatedFetch(mediaUrl, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/vnd.api+json',
|
|
79
|
+
Accept: 'application/vnd.api+json',
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify(mediaBody),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!mediaResponse.ok) {
|
|
85
|
+
const text = await mediaResponse.text();
|
|
86
|
+
console.error(`Media creation failed: ${mediaResponse.status}`);
|
|
87
|
+
console.error(text.slice(0, 1000));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mediaResult = await mediaResponse.json();
|
|
92
|
+
const mediaUuid = mediaResult.data?.id;
|
|
93
|
+
|
|
94
|
+
if (!mediaUuid) {
|
|
95
|
+
throw new Error('No media UUID returned from API');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fetch the full media entity with file includes
|
|
99
|
+
const fullEntity = await fetchEntity('media--document', mediaUuid);
|
|
100
|
+
|
|
101
|
+
// Save locally with filename as label
|
|
102
|
+
const savedPath = saveContent(
|
|
103
|
+
'media--document',
|
|
104
|
+
mediaUuid,
|
|
105
|
+
fullEntity,
|
|
106
|
+
mediaName,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
displayEntitySummary(
|
|
110
|
+
'Uploaded',
|
|
111
|
+
'media--document',
|
|
112
|
+
mediaUuid,
|
|
113
|
+
fullEntity,
|
|
114
|
+
savedPath,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Return the file URL for programmatic use
|
|
118
|
+
const entityFileUrl =
|
|
119
|
+
fullEntity?.included?.[0]?.attributes?.uri?.url || fileUrl;
|
|
120
|
+
return { uuid: mediaUuid, fileUrl: entityFileUrl };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(`Error uploading ${filePath}:`, error.message);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
/**
|
|
3
|
+
* Uploads an image file and creates a media entity.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { basename } from 'path';
|
|
7
|
+
|
|
8
|
+
import { authenticatedFetch, getApiBaseUrl } from './client.js';
|
|
9
|
+
import { displayEntitySummary, fetchEntity } from './entity.js';
|
|
10
|
+
import { saveContent } from './utils.js';
|
|
11
|
+
|
|
12
|
+
export async function uploadImage(filePath, altText) {
|
|
13
|
+
if (!filePath) {
|
|
14
|
+
console.error('Usage: canvas-jsonapi upload-image <file-path> [alt-text]');
|
|
15
|
+
console.error(
|
|
16
|
+
'Example: canvas-jsonapi upload-image images/photo.jpg "Photo description"',
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const filename = basename(filePath);
|
|
23
|
+
const fileData = readFileSync(filePath);
|
|
24
|
+
const alt = altText || filename;
|
|
25
|
+
|
|
26
|
+
console.log(`Uploading: ${filename}`);
|
|
27
|
+
|
|
28
|
+
// Step 1: Upload the file binary
|
|
29
|
+
const fileUploadUrl = `${getApiBaseUrl()}/media/image/media_image`;
|
|
30
|
+
const fileResponse = await authenticatedFetch(fileUploadUrl, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/octet-stream',
|
|
34
|
+
'Content-Disposition': `file; filename="${filename}"`,
|
|
35
|
+
Accept: 'application/vnd.api+json',
|
|
36
|
+
},
|
|
37
|
+
body: fileData,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!fileResponse.ok) {
|
|
41
|
+
const text = await fileResponse.text();
|
|
42
|
+
console.error(`File upload failed: ${fileResponse.status}`);
|
|
43
|
+
console.error(text.slice(0, 1000));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fileResult = await fileResponse.json();
|
|
48
|
+
const fileId = fileResult.data?.id;
|
|
49
|
+
|
|
50
|
+
if (!fileId) {
|
|
51
|
+
throw new Error('No file ID returned from upload');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Step 2: Create media entity
|
|
55
|
+
const mediaUrl = `${getApiBaseUrl()}/media/image`;
|
|
56
|
+
const mediaBody = {
|
|
57
|
+
data: {
|
|
58
|
+
type: 'media--image',
|
|
59
|
+
attributes: {
|
|
60
|
+
name: filename,
|
|
61
|
+
status: true,
|
|
62
|
+
},
|
|
63
|
+
relationships: {
|
|
64
|
+
media_image: {
|
|
65
|
+
data: {
|
|
66
|
+
type: 'file--file',
|
|
67
|
+
id: fileId,
|
|
68
|
+
meta: {
|
|
69
|
+
alt: alt,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const mediaResponse = await authenticatedFetch(mediaUrl, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/vnd.api+json',
|
|
81
|
+
Accept: 'application/vnd.api+json',
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(mediaBody),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!mediaResponse.ok) {
|
|
87
|
+
const text = await mediaResponse.text();
|
|
88
|
+
console.error(`Media creation failed: ${mediaResponse.status}`);
|
|
89
|
+
console.error(text.slice(0, 1000));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const mediaResult = await mediaResponse.json();
|
|
94
|
+
const mediaUuid = mediaResult.data?.id;
|
|
95
|
+
|
|
96
|
+
if (!mediaUuid) {
|
|
97
|
+
throw new Error('No media UUID returned from API');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fetch the full media entity with file includes
|
|
101
|
+
const fullEntity = await fetchEntity('media--image', mediaUuid);
|
|
102
|
+
|
|
103
|
+
// Save locally with filename as label
|
|
104
|
+
const savedPath = saveContent(
|
|
105
|
+
'media--image',
|
|
106
|
+
mediaUuid,
|
|
107
|
+
fullEntity,
|
|
108
|
+
filename,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
displayEntitySummary(
|
|
112
|
+
'Uploaded',
|
|
113
|
+
'media--image',
|
|
114
|
+
mediaUuid,
|
|
115
|
+
fullEntity,
|
|
116
|
+
savedPath,
|
|
117
|
+
);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`Error uploading ${filePath}:`, error.message);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
/**
|
|
3
|
+
* Uploads a video file and creates a media entity.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { basename } from 'path';
|
|
7
|
+
|
|
8
|
+
import { authenticatedFetch, getApiBaseUrl } from './client.js';
|
|
9
|
+
import { displayEntitySummary, fetchEntity } from './entity.js';
|
|
10
|
+
import { saveContent } from './utils.js';
|
|
11
|
+
|
|
12
|
+
export async function uploadVideo(filePath, name) {
|
|
13
|
+
if (!filePath) {
|
|
14
|
+
console.error('Usage: canvas-jsonapi upload-video <file-path> [name]');
|
|
15
|
+
console.error(
|
|
16
|
+
'Example: canvas-jsonapi upload-video videos/hero.mp4 "Hero background video"',
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const filename = basename(filePath);
|
|
23
|
+
const fileData = readFileSync(filePath);
|
|
24
|
+
const mediaName = name || filename;
|
|
25
|
+
|
|
26
|
+
console.log(
|
|
27
|
+
`Uploading: ${filename} (${(fileData.length / 1024 / 1024).toFixed(1)} MB)`,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Step 1: Upload the file binary
|
|
31
|
+
const fileUploadUrl = `${getApiBaseUrl()}/media/video/media_video_file`;
|
|
32
|
+
const fileResponse = await authenticatedFetch(fileUploadUrl, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/octet-stream',
|
|
36
|
+
'Content-Disposition': `file; filename="${filename}"`,
|
|
37
|
+
Accept: 'application/vnd.api+json',
|
|
38
|
+
},
|
|
39
|
+
body: fileData,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!fileResponse.ok) {
|
|
43
|
+
const text = await fileResponse.text();
|
|
44
|
+
console.error(`File upload failed: ${fileResponse.status}`);
|
|
45
|
+
console.error(text.slice(0, 1000));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fileResult = await fileResponse.json();
|
|
50
|
+
const fileId = fileResult.data?.id;
|
|
51
|
+
const fileUrl = fileResult.data?.attributes?.uri?.url;
|
|
52
|
+
|
|
53
|
+
if (!fileId) {
|
|
54
|
+
throw new Error('No file ID returned from upload');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Step 2: Create media entity
|
|
58
|
+
const mediaUrl = `${getApiBaseUrl()}/media/video`;
|
|
59
|
+
const mediaBody = {
|
|
60
|
+
data: {
|
|
61
|
+
type: 'media--video',
|
|
62
|
+
attributes: {
|
|
63
|
+
name: mediaName,
|
|
64
|
+
status: true,
|
|
65
|
+
},
|
|
66
|
+
relationships: {
|
|
67
|
+
media_video_file: {
|
|
68
|
+
data: {
|
|
69
|
+
type: 'file--file',
|
|
70
|
+
id: fileId,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const mediaResponse = await authenticatedFetch(mediaUrl, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/vnd.api+json',
|
|
81
|
+
Accept: 'application/vnd.api+json',
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(mediaBody),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!mediaResponse.ok) {
|
|
87
|
+
const text = await mediaResponse.text();
|
|
88
|
+
console.error(`Media creation failed: ${mediaResponse.status}`);
|
|
89
|
+
console.error(text.slice(0, 1000));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const mediaResult = await mediaResponse.json();
|
|
94
|
+
const mediaUuid = mediaResult.data?.id;
|
|
95
|
+
|
|
96
|
+
if (!mediaUuid) {
|
|
97
|
+
throw new Error('No media UUID returned from API');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Fetch the full media entity with file includes
|
|
101
|
+
const fullEntity = await fetchEntity('media--video', mediaUuid);
|
|
102
|
+
|
|
103
|
+
// Save locally
|
|
104
|
+
const savedPath = saveContent(
|
|
105
|
+
'media--video',
|
|
106
|
+
mediaUuid,
|
|
107
|
+
fullEntity,
|
|
108
|
+
mediaName,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
displayEntitySummary(
|
|
112
|
+
'Uploaded',
|
|
113
|
+
'media--video',
|
|
114
|
+
mediaUuid,
|
|
115
|
+
fullEntity,
|
|
116
|
+
savedPath,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Return the file URL for programmatic use
|
|
120
|
+
const entityFileUrl =
|
|
121
|
+
fullEntity?.included?.[0]?.attributes?.uri?.url || fileUrl;
|
|
122
|
+
return { uuid: mediaUuid, fileUrl: entityFileUrl };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`Error uploading ${filePath}:`, error.message);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
/**
|
|
3
|
+
* Common utility functions for content management scripts.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'fs';
|
|
13
|
+
import { dirname, join } from 'path';
|
|
14
|
+
|
|
15
|
+
export const CONTENT_DIR = join(process.cwd(), 'content');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Converts a string to a safe filename slug
|
|
19
|
+
* @param {string} str - String to slugify
|
|
20
|
+
* @returns {string} Safe filename string
|
|
21
|
+
*/
|
|
22
|
+
export function slugify(str) {
|
|
23
|
+
return str
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.normalize('NFD')
|
|
26
|
+
.replace(/[\u0300-\u036f]/g, '') // Remove diacritics
|
|
27
|
+
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
|
28
|
+
.replace(/^-+|-+$/g, '') // Trim leading/trailing hyphens
|
|
29
|
+
.slice(0, 50); // Limit length
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensures a directory exists, creating it recursively if needed
|
|
34
|
+
* @param {string} dirPath - Directory path to ensure
|
|
35
|
+
*/
|
|
36
|
+
export function ensureDir(dirPath) {
|
|
37
|
+
if (!existsSync(dirPath)) {
|
|
38
|
+
mkdirSync(dirPath, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Gets the file path for a content item
|
|
44
|
+
* @param {string} type - Entity type (e.g., "page--landing")
|
|
45
|
+
* @param {string} uuid - Content UUID
|
|
46
|
+
* @param {string} [label] - Optional label to append to filename
|
|
47
|
+
* @returns {string} Full file path
|
|
48
|
+
*/
|
|
49
|
+
export function getContentPath(type, uuid, label) {
|
|
50
|
+
const filename = label ? `${uuid}.${slugify(label)}.json` : `${uuid}.json`;
|
|
51
|
+
return join(CONTENT_DIR, type, filename);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Gets the directory path for a content type
|
|
56
|
+
* @param {string} type - Entity type (e.g., "page--landing")
|
|
57
|
+
* @returns {string} Directory path
|
|
58
|
+
*/
|
|
59
|
+
export function getTypeDir(type) {
|
|
60
|
+
return join(CONTENT_DIR, type);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Finds a content file by UUID (ignoring the label part of filename)
|
|
65
|
+
* @param {string} type - Entity type
|
|
66
|
+
* @param {string} uuid - Content UUID
|
|
67
|
+
* @returns {string|null} Full file path or null if not found
|
|
68
|
+
*/
|
|
69
|
+
export function findContentByUuid(type, uuid) {
|
|
70
|
+
const typeDir = getTypeDir(type);
|
|
71
|
+
if (!existsSync(typeDir)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const files = readdirSync(typeDir);
|
|
75
|
+
const match = files.find(
|
|
76
|
+
(f) => f.startsWith(`${uuid}.`) && f.endsWith('.json'),
|
|
77
|
+
);
|
|
78
|
+
return match ? join(typeDir, match) : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Saves content data to a local JSON file
|
|
83
|
+
* @param {string} type - Entity type
|
|
84
|
+
* @param {string} uuid - Content UUID
|
|
85
|
+
* @param {object} data - Content data to save
|
|
86
|
+
* @param {string} [label] - Optional label to append to filename
|
|
87
|
+
* @returns {string} Path to saved file
|
|
88
|
+
*/
|
|
89
|
+
export function saveContent(type, uuid, data, label) {
|
|
90
|
+
// Remove old file if it exists with a different name
|
|
91
|
+
const existingFile = findContentByUuid(type, uuid);
|
|
92
|
+
const newFilePath = getContentPath(type, uuid, label);
|
|
93
|
+
|
|
94
|
+
if (existingFile && existingFile !== newFilePath) {
|
|
95
|
+
try {
|
|
96
|
+
unlinkSync(existingFile);
|
|
97
|
+
} catch {
|
|
98
|
+
// Ignore errors removing old file
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
ensureDir(dirname(newFilePath));
|
|
103
|
+
writeFileSync(newFilePath, JSON.stringify(data, null, 2));
|
|
104
|
+
return newFilePath;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reads content data from a local JSON file
|
|
109
|
+
* @param {string} filePath - Path to JSON file
|
|
110
|
+
* @returns {object} Parsed content data
|
|
111
|
+
*/
|
|
112
|
+
export function readContent(filePath) {
|
|
113
|
+
if (!existsSync(filePath)) {
|
|
114
|
+
throw new Error(`File not found: ${filePath}`);
|
|
115
|
+
}
|
|
116
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
117
|
+
return JSON.parse(raw);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extracts type and UUID from a file path
|
|
122
|
+
* @param {string} filePath - Path to content file
|
|
123
|
+
* @returns {{type: string, uuid: string}} Extracted type and UUID
|
|
124
|
+
*/
|
|
125
|
+
export function parseContentPath(filePath) {
|
|
126
|
+
// Match pattern: content/<type>/<uuid>.<label>.json or content/<type>/<uuid>.json
|
|
127
|
+
const match = filePath.match(
|
|
128
|
+
/content\/([^/]+)\/([a-f0-9-]+)(?:\.[^/]+)?\.json$/i,
|
|
129
|
+
);
|
|
130
|
+
if (!match) {
|
|
131
|
+
throw new Error(`Invalid content file path: ${filePath}`);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
type: match[1],
|
|
135
|
+
uuid: match[2],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generates a temporary filename for new content
|
|
141
|
+
* @param {string} type - Entity type
|
|
142
|
+
* @returns {string} Temporary file path with placeholder UUID
|
|
143
|
+
*/
|
|
144
|
+
export function getTempContentPath(type) {
|
|
145
|
+
const timestamp = Date.now();
|
|
146
|
+
return getContentPath(type, `new-${timestamp}`);
|
|
147
|
+
}
|