@caweb/cli 1.3.2 → 1.3.4

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
@@ -2,16 +2,12 @@
2
2
  `caweb-cli` is a tool which rapidly sets up a local WordPress environment fully configured for the [CAWebPublishing Service](https://caweb.cdt.ca.gov/), allows for the creation of Gutenberg blocks with the CAWebPublishing template configurations, and much more. The cli will automatically generate the necessary [.wp-env.json](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#wp-env-json) file, to override or add additional configuration options use the [.wp-env.override.json](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/#wp-env-override-json) file.
3
3
 
4
4
  *`caweb-cli` is largely inspired by WordPress Packages major thanks to the whole WordPress team and community!*
5
- The following WordPress packages are used:
6
- [wp-env](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/)
7
- [create-block](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/)
8
5
 
9
6
  ## Prerequisites
10
7
  - Latest version of [Docker Desktop](https://www.docker.com/products/docker-desktop), which includes [Compose v2](https://docs.docker.com/compose/migrate/), is installed.
11
8
  - <strong>For Debian-Based Linux distributions:</strong> <code>docker-compose</code> may need to be installed with: <code>sudo apt-get install docker-compose</code>.
12
9
  - <strong>For Windows users:</strong> [WSL should be set to version 2 for Windows Docker Desktop compatibility](https://docs.docker.com/desktop/windows/wsl/).
13
10
  - git is installed.
14
- - php is installed.
15
11
 
16
12
  ## Additional Features
17
13
  - Downloads and configures the [CAWeb Theme](https://github.com/CAWebPublishing/CAWeb)
@@ -21,15 +17,13 @@ The following WordPress packages are used:
21
17
  - phpMyAdmin development site starts at http://localhost:8080
22
18
  - phpMyAdmin test site started at http://localhost:9090
23
19
  - Uses CAWebPublishing [External Project Template Configuration](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/packages-create-block-external-template/) when creating Gutenberg Blocks, see configurations [here](https://github.com/CAWebPublishing/cli/lib/template)
24
- - Allows for syncing of WordPress instance via Rest API and CLI, requires ssh.
25
- - Adds config.yml to both cli containers, alias are added for each ssh connection.
20
+ - Allows for syncing of WordPress instance via Rest API, to maintain ID's please ensure [CAWebPublishing Development Toolbox](https://github.com/CAWebPublishing/caweb-dev/) plugin is installed.
21
+ - Adds config.yml to both cli containers.
26
22
  <b>Example config.yml file</b>
27
23
  <pre>
28
24
  path: /var/www/html
29
25
  apache_modules:
30
26
  - mod_rewrite
31
- @staging:
32
- ssh: wpcli@staging.wp-cli.org
33
27
  </pre>
34
28
 
35
29
 
@@ -81,7 +75,7 @@ Options:
81
75
  </pre>
82
76
  ### `caweb run <container> [command...]`
83
77
  <pre>
84
- caweb run <container> [command...]
78
+ caweb run &lt;container&gt; [command...]
85
79
 
86
80
  Runs an arbitrary command in one of the underlying Docker containers. A double
87
81
  dash can be used to pass arguments to the container without parsing them. This
@@ -166,7 +160,7 @@ Options:
166
160
 
167
161
  ### `caweb create-block [options] <slug>`
168
162
  <pre>
169
- caweb create-block [options] <slug>
163
+ caweb create-block [options] &lt;slug&gt;
170
164
 
171
165
  Scaffold for WordPress plugin to register CA.gov Design System Block.
172
166
 
@@ -178,7 +172,7 @@ Options:
178
172
  </pre>
179
173
  ### `caweb update-block [options] <slug>`
180
174
  <pre>
181
- caweb update-block [options] <slug>
175
+ caweb update-block [options] &lt;slug&gt;
182
176
 
183
177
  Updates a CA.gov Design System Block.
184
178
 
@@ -188,3 +182,18 @@ Arguments:
188
182
  Options:
189
183
  -h, --help display help for command
190
184
  </pre>
185
+ ### `caweb sync <from> <to>`
186
+ In order for the sync process to work correctly, you must have a caweb.json file in the project root directory. For more information read [caweb.json](./docs/SYNC.MD) configuration.
187
+ <pre>
188
+ caweb sync &lt;from&gt; &lt;to&gt;
189
+
190
+ Sync changes from one destination to another.
191
+
192
+ Arguments:
193
+ from Target Site URL with current changes.
194
+ to Destination Site URL that should be synced.
195
+
196
+ Options:
197
+ -h, --help display help for command
198
+ -t,--tax [tax...] Taxonomy that should be synced. Omitting this option will sync the full site. (choices: "pages", "posts", "media", "menus")
199
+ </pre>
package/commands/sync.js CHANGED
@@ -14,26 +14,65 @@ import {
14
14
  getTaxonomies,
15
15
  createTaxonomies
16
16
  } from '../lib/index.js';
17
+ import { get } from 'http';
17
18
 
18
19
  const errors = [];
19
20
 
21
+ /**
22
+ * Return all parent items for a given set of ids.
23
+ *
24
+ * @async
25
+ * @param {Array} ids
26
+ * @param {*} request
27
+ * @param {string} [tax='pages']
28
+ * @returns {unknown}
29
+ */
30
+ async function getParentItems( objects, request, tax = 'pages' ){
31
+ let parentItemsObjects = [];
32
+ let objectParentIds = objects.map((obj) => { return obj.parent; });
33
+
34
+ while( objectParentIds.length > 0 ){
35
+
36
+ // if we have parent ids, we have to collect any parent items.
37
+ let parentItems = await getTaxonomies({
38
+ ...request,
39
+ orderby: 'parent',
40
+ embed: true,
41
+ include: objectParentIds
42
+ }, tax);
43
+
44
+ // if we have parent items, we have to add to the items array.
45
+ if( parentItems ){
46
+ parentItemsObjects = parentItemsObjects.concat( parentItems );
47
+ }
48
+
49
+ // if the parent items have parent ids, we have to save those for the next run.
50
+ objectParentIds = parentItems.map((obj) => { return obj.parent; }).filter((id) => { return id !== 0; })
51
+ }
52
+
53
+ return objects.concat( parentItemsObjects ).sort( (a,b) => a.parent - b.parent );
54
+ }
20
55
 
21
56
  /**
22
57
  * Sync Environments.
23
58
  *
59
+ * @see https://developer.wordpress.org/rest-api/reference/
60
+ *
24
61
  * @param {Object} options
25
62
  * @param {Object} options.spinner A CLI spinner which indicates progress.
26
63
  * @param {boolean} options.from Remote Site URL with current changes.
27
64
  * @param {boolean} options.to Destination Site URL that should be synced.
28
65
  * @param {boolean} options.debug True if debug mode is enabled.
29
66
  * @param {Array} options.tax Taxonomy that should be synced.
67
+ * @param {Array} options.include Include specific IDs only.
30
68
  */
31
69
  export default async function sync({
32
70
  spinner,
33
71
  debug,
34
72
  from,
35
73
  to,
36
- tax
74
+ tax,
75
+ include
37
76
  } ) {
38
77
 
39
78
  const localFile = path.join(appPath, 'caweb.json');
@@ -79,13 +118,11 @@ export default async function sync({
79
118
  }
80
119
  }
81
120
 
82
- let ssh = 'local' !== to ? to : `docker:${path.basename(workDirectoryPath)}-cli-1`;
83
121
 
84
122
  to = serviceConfig.sync[to];
85
123
 
86
124
  let toOptions = {
87
125
  url: to.url,
88
- ssh,
89
126
  headers: {
90
127
  Authorization: 'Basic ' + Buffer.from(`${to.user}:${to.pwd}`).toString('base64'),
91
128
  'content-type': 'multipart/form-data',
@@ -93,100 +130,165 @@ export default async function sync({
93
130
  }
94
131
  }
95
132
 
133
+ /**
134
+ * Sync Process
135
+ * If taxonomy is undefined then we don't sync that section.
136
+ *
137
+ * 1) Site Settings are always synced.
138
+ * 2) We always collect all the media.
139
+ * 3) We only collect pages if the taxonomy is undefined or it's set to pages.
140
+ * 4) We only collect posts if the taxonomy is undefined or it's set to posts.
141
+ * 5) We only collect menus if the taxonomy is undefined or it's set to menus.
142
+ * - We also collect menu items if menus are collected.
143
+ */
144
+ let settings = [];
145
+ let media = [];
96
146
  let pages = [];
97
147
  let posts = [];
98
- let attachedMedia = [];
99
- let featuredMedia = [];
100
148
  let menus = [];
101
149
  let menuNavItems = [];
102
150
 
103
- // get all settings
104
- // site settings are always synced
105
- spinner.text = `Getting Site Settings from ${from.url}`;
106
- let settings = await getTaxonomies({
107
- ...fromOptions,
108
- fields: [
109
- 'show_on_front',
110
- 'page_on_front'
111
- ],
112
- }, 'settings')
113
-
114
- /**
115
- * If taxonomy is undefined then we sync everything, otherwise we only sync what's being requested.
116
- * First we collect everything we need from the base, then we send the information to the target.
117
- * If media is requested then we need to download posts/pages to get any attached/featured media.
118
- */
119
- // collect pages
120
- if( undefined === tax || tax.includes('pages') || tax.includes('media')){
121
- // get all pages/posts
122
- spinner.text = `Gathering all pages from ${from.url}`;
123
- pages = await getTaxonomies(fromOptions);
124
- }
151
+ // Media Library.
152
+ spinner.text = `Collecting Media Library ${from.url}`;
153
+ let mediaLibrary = await getTaxonomies({
154
+ ...fromOptions,
155
+ fields: [
156
+ 'id',
157
+ 'source_url',
158
+ 'title',
159
+ 'caption',
160
+ 'alt_text',
161
+ 'date',
162
+ 'mime_type',
163
+ 'post',
164
+ 'media_details'
165
+ ],
166
+ include: tax.includes('media') && include ? include.join(',') : null
167
+ },
168
+ 'media'
169
+ );
125
170
 
126
- // collect posts
127
- if( undefined === tax || tax.includes('posts')|| tax.includes('media')){
128
- // get all pages/posts
129
- spinner.text = `Gathering all posts from ${from.url}`;
130
- posts = await getTaxonomies(fromOptions, 'posts');
171
+ // Site Settings.
172
+ if( undefined === tax || tax.includes('settings') ){
173
+ spinner.text = `Collecting Site Settings from ${from.url}`;
174
+ settings = await getTaxonomies({
175
+ ...fromOptions,
176
+ fields: [
177
+ 'show_on_front',
178
+ 'page_on_front',
179
+ 'posts_per_page'
180
+ ],
181
+ }, 'settings')
182
+
131
183
  }
132
184
 
133
- // collect media
134
- if( undefined === tax || tax.includes('media')){
135
-
136
- spinner.text = `Collecting all attached/featured images from ${from.url}`;
137
- /**
138
- * Media Library Handling
139
- * 1) attached media items by default are only possible on pages.
140
- * 2) featured media by default are only possible on posts.
141
- */
142
- const mediaFields = [
143
- 'id',
144
- 'source_url',
145
- 'title',
146
- 'caption',
147
- 'alt_text',
148
- 'date',
149
- 'mime_type'
150
- ];
151
- attachedMedia = await getTaxonomies({
185
+ // Pages.
186
+ if( undefined === tax || tax.includes('pages') ){
187
+ // get all pages/posts
188
+ spinner.text = `Collecting pages from ${from.url}`;
189
+ pages = await getTaxonomies({
152
190
  ...fromOptions,
153
- fields: mediaFields,
154
- parent: pages.map((p) => { return p.id })
191
+ orderby: 'parent',
192
+ embed: true,
193
+ include: tax && include ? include.join(',') : null
155
194
  },
156
- 'media'
195
+ 'pages'
157
196
  );
158
197
 
159
- featuredMedia = await getTaxonomies({
198
+ // we only do this if specific ids were requested.
199
+ if( include ){
200
+ // if we have parent ids, we have to collect any parent items.
201
+ pages = await getParentItems(
202
+ pages,
203
+ fromOptions,
204
+ 'pages'
205
+ )
206
+ }
207
+ }
208
+
209
+ // Posts.
210
+ if( undefined === tax || tax.includes('posts') ){
211
+ // get all pages/posts
212
+ spinner.text = `Collecting all posts from ${from.url}`;
213
+ posts = await getTaxonomies({
160
214
  ...fromOptions,
161
- fields: mediaFields,
162
- include: posts.map((p) => { if(p.featured_media){return p.featured_media } })
215
+ orderby: 'parent',
216
+ include: tax && include ? include.join(',') : null
163
217
  },
164
- 'media'
218
+ 'posts'
165
219
  );
166
-
167
- // before we can upload media files.
168
- for( let mediaObj of [].concat( attachedMedia, featuredMedia ) ){
169
- // generate a blob from the media object source_url.
170
- const mediaBlob = await axios.request(
171
- {
172
- ...fromOptions,
173
- url: mediaObj.source_url,
174
- responseType: 'arraybuffer'
175
- }
176
- )
177
- .then( (img) => { return new Blob([img.data]) })
178
- .catch( (error) => {
179
- errors.push(`${mediaObj.source_url} could not be downloaded.`)
180
- return false;
181
- });
182
-
183
- if( mediaBlob ){
184
- mediaObj.data = mediaBlob;
220
+
221
+ // we only do this if specific ids were requested.
222
+ if( include ){
223
+ // if we have parent ids, we have to collect any parent items.
224
+ posts = await getParentItems(
225
+ posts,
226
+ fromOptions,
227
+ 'posts'
228
+ )
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Media Library Handling
234
+ *
235
+ * We iterate thru the media library to identify which media should be synced.
236
+ * 1) attached media items by default are only possible on pages.
237
+ * 2) featured media by default are only possible on posts.
238
+ * 3) save any linked media in the content of pages and posts.
239
+ */
240
+ for( let m of mediaLibrary ){
241
+ // check if the media is attached to a page.
242
+ // if tax is undefined, we collect all media attached to posts and pages.
243
+ // if tax is defined, we only collect media attached to posts and pages if the id match.
244
+ if( ( ! tax && m.post ) ||
245
+ ( tax && include.includes(m.id.toString()) ) ){
246
+ media.push( m );
247
+
248
+ // we don't have to check any further.
249
+ continue;
250
+ }
251
+
252
+ for( let p of [].concat( pages, posts ) ){
253
+ // if the media is featured on a post or linked in the content.
254
+ if( p.featured_media === m.id ||
255
+ p.content.rendered.match( new RegExp(`src=&#8221;(${m.source_url}.*)&#8221;`, 'g') )){
256
+ media.push( m );
185
257
  }
186
258
  }
187
259
  }
260
+
261
+ // filter any duplicate media.
262
+ media = media.filter((m, index, self) => { return index === self.findIndex((t) => { return t.id === m.id; })} );
263
+
264
+ // before we can upload media files we have to generate the media blob data.
265
+ for( let m of media ){
266
+ const mediaBlob = await axios.request(
267
+ {
268
+ ...fromOptions,
269
+ url: m.source_url,
270
+ responseType: 'arraybuffer'
271
+ }
272
+ )
273
+ .then( (img) => { return new Blob([img.data]) })
274
+ .catch( (error) => {
275
+ errors.push(`${m.source_url} could not be downloaded.`)
276
+ return false;
277
+ });
278
+
279
+ if( mediaBlob ){
280
+ m.data = mediaBlob;
281
+ }
282
+ }
283
+
284
+ // this has to be done after we have the media data.
285
+ // Lets replace the url references in the content of the pages and posts.
286
+ for( let p of [].concat( pages, posts ) ){
287
+ p.content.rendered = p.content.rendered.replace( new RegExp(from.url, 'g'), to.url );
288
+ }
188
289
 
189
- // collect menu and nav items.
290
+
291
+ // Menu and Nav Items.
190
292
  if( undefined === tax || tax.includes('menus')){
191
293
  spinner.text = `Collecting assigned navigation menus from ${from.url}`;
192
294
  // get all menus and navigation links
@@ -199,7 +301,8 @@ export default async function sync({
199
301
  'slug',
200
302
  'meta',
201
303
  'locations'
202
- ]
304
+ ],
305
+ include: tax && include ? include.join(',') : null
203
306
  }, 'menus');
204
307
 
205
308
  menuNavItems = await getTaxonomies(
@@ -224,6 +327,7 @@ export default async function sync({
224
327
  'meta',
225
328
  'menus'
226
329
  ],
330
+ include: tax && include ? include.join(',') : null,
227
331
  menus: menus.map((menu) => { return menu.id; })
228
332
  },
229
333
  'menu-items'
@@ -234,43 +338,50 @@ export default async function sync({
234
338
  /**
235
339
  * Now we have all the data we can begin to create the taxonomies on the target.
236
340
  *
237
- * Creation order is important otherwise missing dependencies will cause errors to trigger.
238
- * We create media first, then pages, then posts, then menus and navigation links.
341
+ * Import Order is important otherwise missing dependencies will cause errors to trigger
342
+ * 1) Media
343
+ * 2) Pages
344
+ * 3) Posts
345
+ * 4) Menus & Navigation Links
346
+ * 5) Settings
239
347
  */
240
348
 
241
- // create media attachments
242
- if( undefined === tax || tax.includes('media')){
349
+ // Media.
350
+ if( media ){
243
351
  spinner.text = `Uploading media files to ${to.url}`;
244
352
  await createTaxonomies(
245
- [].concat( attachedMedia, featuredMedia ).filter((img) => { return img.data }),
353
+ media.filter((img) => { return img.data }),
246
354
  toOptions,
247
355
  'media',
248
356
  spinner
249
357
  )
250
358
  }
251
359
 
252
- // create pages
253
- if( undefined === tax || tax.includes('pages')){
360
+ // Pages.
361
+ if( pages ){
254
362
  spinner.text = `Creating all pages to ${to.url}`;
255
363
  await createTaxonomies( pages, toOptions, 'pages', spinner );
256
364
  }
257
365
 
258
- // create posts
259
- if( undefined === tax || tax.includes('posts')){
366
+ // Posts.
367
+ if( posts ){
260
368
  spinner.text = `Creating all posts to ${to.url}`;
261
369
  await createTaxonomies( posts, toOptions, 'posts', spinner );
262
370
  }
263
371
 
264
- // create menus and navigation links
265
- if( undefined === tax || tax.includes('menus')){
372
+ // Menus and Navigation Links.
373
+ if( menus ){
266
374
  spinner.text = `Reconstructing navigation menus to ${to.url}`;
267
375
  await createTaxonomies(menus, toOptions, 'menus', spinner);
268
376
  await createTaxonomies(menuNavItems, toOptions, 'menu-items', spinner);
269
377
  }
270
378
 
271
379
 
272
- // update settings
273
- await createTaxonomies(settings, toOptions, 'settings', spinner);
380
+ // Settings.
381
+ if( settings ){
382
+ spinner.text = `Updating site settings to ${to.url}`;
383
+ await createTaxonomies(settings, toOptions, 'settings', spinner);
384
+ }
274
385
 
275
386
  spinner.text = `Sync from ${from.url} to ${to.url} completed successfully.`
276
387
 
@@ -24,21 +24,6 @@ async function generateCLIConfig(workDirectoryPath){
24
24
  apache_modules: ['mod_rewrite']
25
25
  };
26
26
 
27
- let { homedir } = os.userInfo();
28
-
29
- // add any ssh hosts to the config
30
- if( fs.existsSync( path.join( homedir, '.ssh', 'config' )) ){
31
-
32
- let ssh_hosts = fs.readFileSync(path.join( homedir, '.ssh', 'config' )).toString().match(/Host\s+(.*)[^\n\r]/g);
33
-
34
- ssh_hosts.forEach((host) => {
35
- let s = host.replace(/Host\s+/, '');
36
-
37
- yml[`@${s}`] = { ssh: s};
38
- })
39
- }
40
-
41
-
42
27
  fs.writeFileSync(
43
28
  path.join(workDirectoryPath, 'config.yml'),
44
29
  yaml.dump(yml));
package/lib/cli.js CHANGED
@@ -262,7 +262,11 @@ export default function cli() {
262
262
  .addOption(new Option(
263
263
  '-t,--tax [tax...]',
264
264
  'Taxonomy that should be synced. Omitting this option will sync the full site.'
265
- ).choices(['pages', 'posts', 'media', 'menus']))
265
+ ).choices(['pages', 'posts', 'media', 'menus', 'settings']))
266
+ .addOption(new Option(
267
+ '-i,--include [include...]',
268
+ 'IDs to of taxonomies to include. Omitting this option will sync all items for given taxonomy.'
269
+ ))
266
270
  .allowUnknownOption(true)
267
271
  .action( withSpinner(env.sync) )
268
272
 
@@ -4,21 +4,29 @@
4
4
  import path from 'path';
5
5
  import axios from 'axios';
6
6
  import terminalLink from 'terminal-link';
7
-
7
+ import axiosRetry from 'axios-retry';
8
8
 
9
9
  /**
10
10
  * Internal dependencies
11
11
  */
12
- import {
13
- projectPath,
14
- runCmd
15
- } from '../index.js';
16
-
17
12
  const endpoint = '/wp-json/wp/v2';
18
- const requestDelay = 1000; // we wait 1 second between requests
19
-
20
-
21
- const sleep = (waitTimeInMs) => new Promise(resolve => setTimeout(resolve, waitTimeInMs));
13
+ const syncEndpoint = '/wp-json/caweb/v1/sync';
14
+
15
+ // Axios Configurations.
16
+ axiosRetry(axios,
17
+ {
18
+ retries: 3, // attempt requests 3 times
19
+ shouldResetTimeout: true, // reset timeout on retries
20
+ retryCondition: (error) => {
21
+ if( error.response && error.response.status === 500 ){
22
+ return true;
23
+ }
24
+ }, // retry on any network or 5xx error
25
+ retryDelay: (retryCount, error ) => {
26
+ return 5000 // retry delay
27
+ }
28
+ }
29
+ );
22
30
 
23
31
  function processData( data ){
24
32
  /**
@@ -73,19 +81,19 @@ async function getTaxonomies( request, tax = 'pages' ){
73
81
  'categories',
74
82
  'tags',
75
83
  'meta',
84
+ 'parent',
85
+ '_links'
76
86
  ];
77
87
 
78
88
  let urlParams = [
79
- 'order=asc'
89
+ 'order=' + ( request.order ? processUrlParam(request.order) : 'asc' )
80
90
  ]
81
91
 
82
-
83
92
  // if fields are passed
84
93
  if( fields.length && 'all' !== fields ){
85
94
  urlParams.push( `_fields=${ fields.join(',')}` );
86
95
  }
87
96
 
88
-
89
97
  // if no request is added default to GET
90
98
  if( ! request.method ){
91
99
  request.method = 'GET'
@@ -106,13 +114,23 @@ async function getTaxonomies( request, tax = 'pages' ){
106
114
  urlParams.push( 'menus=' + processUrlParam( request.menus ) );
107
115
  }
108
116
 
109
- // if per page argument.
110
- urlParams.push( 'per_page=' + request.per_page ? request.per_page : '100' )
111
-
117
+ // if taxonomies is not menus, we add the orderby parameter.
118
+ if( 'menus' !== tax ){
119
+ urlParams.push( `orderby=` + (request.orderby ? processUrlParam(request.orderby) : 'id') );
120
+ }
112
121
 
113
- // if posts_per_page argument.
114
- urlParams.push( 'posts_per_page=' + request.posts_per_page ? request.posts_per_page : '10' )
122
+ // if embed is set to true, we add the _embed parameter.
123
+ if( request.embed ){
124
+ urlParams.push( '_embed' );
125
+ }
115
126
 
127
+ /**
128
+ * The per_page parameter is capped at 100.
129
+ *
130
+ * @see https://developer.wordpress.org/rest-api/using-the-rest-api/pagination/
131
+ */
132
+ request.per_page = request.per_page && request.per_page <= 100 ? request.per_page : 100;
133
+ urlParams.push( 'per_page=' + request.per_page );
116
134
 
117
135
  let collection = [];
118
136
  let results = false;
@@ -172,8 +190,26 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
172
190
  for( let obj of taxData ){
173
191
  // endpoint url.
174
192
  let url = `${request.url}${endpoint}/${tax}`;
175
- let existingID = false;
193
+ let existingID = obj.id ? obj.id : false;
176
194
 
195
+ // process object properties.
196
+ for( let prop in obj ){
197
+ // we process the rendered property and delete the rendered property.
198
+ if( 'object' === typeof obj[prop] && null !== obj[prop] && obj[prop].hasOwnProperty('rendered') ){
199
+ obj[prop] = processData(obj[prop].rendered);
200
+ }
201
+
202
+ // if obj has a parent and it's 0, we delete it.
203
+ if( 'parent' === prop && 0 === obj[prop] ){
204
+ delete obj[prop];
205
+ }
206
+
207
+ // disallowed props.
208
+ if( ['_links', '_embedded'].includes(prop) ){
209
+ delete obj[prop];
210
+ }
211
+ }
212
+
177
213
  // The REST API doesn't allow passing the id, in order to maintain ID's we make an additional request to our plugin endpoint.
178
214
  if( obj.id ){
179
215
  // first we check if the ID exist
@@ -187,9 +223,11 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
187
223
  .then((res) => { return res.data.id })
188
224
  .catch(error => {return false;})
189
225
 
190
- // for menus if ID doesn't exist we also check for matching slug
191
- // if a menu with a matching slug exists we return that id instead.
192
- if( 'menus' === tax && ! idExists ){
226
+ /**
227
+ * Menus Only
228
+ * Since we can't have duplicated slugs, if the ID doesn't exist we also check for matching slugs.
229
+ */
230
+ if( 'menus' === tax && false === idExists ){
193
231
  idExists = await axios.request(
194
232
  {
195
233
  ...request,
@@ -202,37 +240,21 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
202
240
 
203
241
  }
204
242
 
205
- /**
206
- * if the ID doesn't exist we save it so we can make a request to our plugin endpoint.
207
- * if it does exist update endpoint url so we update the existing item.
208
- */
209
- if( ! idExists ){
210
- existingID = obj.id;
211
- }else{
212
- url = `${url}/${ obj.id }`;
243
+ // if the ID exists we append it to the url.
244
+ if( idExists ){
245
+ url = `${url}/${ idExists }`;
246
+
247
+ // we store the existing ID for later use.
248
+ existingID = idExists;
213
249
  }
214
250
 
215
251
  // id has to be deleted.
216
252
  delete obj.id;
217
253
  }
218
254
 
219
- // process properties
220
- if( obj.content && obj.content.rendered ){
221
- obj.content = processData(obj.content.rendered);
222
- }
223
-
224
- if( obj.title && obj.title.rendered ){
225
- obj.title = processData(obj.title.rendered);
226
- }
227
-
228
- if( obj.caption && obj.caption.rendered ){
229
- obj.caption = processData(obj.caption.rendered);
230
- }
231
-
232
255
  // make WordPress REST API request.
233
256
  let results = await axios.request({
234
257
  ...request,
235
- timeout: 7500,
236
258
  data: 'media' === tax ? createMediaItem(obj) : obj,
237
259
  url
238
260
  })
@@ -240,25 +262,43 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
240
262
 
241
263
 
242
264
  /**
243
- * if the obj had an existing ID that didn't exist we make a request to our plugin endpoint to update the IDs.
265
+ * if the obj had an existing ID we make a request to our plugin endpoint to update the IDs.
244
266
  */
245
267
  if( existingID ){
268
+ let extraArgs = {};
269
+
270
+ // if media
271
+ if( 'media' === tax ){
272
+ // get original source domain
273
+ let sourceDomain = obj.source_url.substring(0, obj.source_url.indexOf('/wp-content/uploads'));
274
+ // get expected guid, replace source domain with the new domain.
275
+ let expectedGuid = obj.source_url.replace(sourceDomain, request.url);
276
+
277
+ extraArgs = {
278
+ guid: results.guid.rendered,
279
+ newGuid: expectedGuid,
280
+ // if media and source_url is different from the guid, we update the guid.
281
+ media_details: results.media_details,
282
+ }
283
+ }
246
284
 
247
285
  await axios.request({
248
286
  ...request,
249
- url: `${request.url}/wp-json/caweb-dev/v1/sync`,
250
- timeout: 7500,
287
+ url: `${request.url}${syncEndpoint}`,
251
288
  data: {
289
+ ...extraArgs,
252
290
  id: results.id,
253
291
  newId: existingID,
254
292
  tax,
255
- locations: 'menus' === tax ? obj.locations : []
293
+ locations: 'menus' === tax ? obj.locations : [],
294
+
256
295
  }
257
296
  })
258
297
  .then( async (res) => { return res.data; } )
259
298
 
260
299
  // update the API results ID, back to the existing ID from earlier.
261
- results.id = existingID;
300
+ results.id = existingID;
301
+
262
302
  }
263
303
 
264
304
  // add results to collection
@@ -275,8 +315,13 @@ async function createTaxonomies( taxData, request, tax = 'pages', spinner ){
275
315
 
276
316
  }
277
317
 
318
+ /**
319
+ * Create a FormData from the Media Item Object
320
+ *
321
+ * @param {Object} media Media Object.
322
+ * @returns {FormData}
323
+ */
278
324
  function createMediaItem( media ){
279
-
280
325
  let fd = new FormData();
281
326
  let featureMediaFile = new File(
282
327
  [
@@ -288,10 +333,12 @@ function createMediaItem( media ){
288
333
  }
289
334
  );
290
335
  fd.append('file', featureMediaFile );
336
+
291
337
  fd.append('title', media.title);
292
- fd.append('caption', media.caption);
338
+ fd.append('caption', media.caption );
293
339
  fd.append('alt_text', media.alt_text);
294
340
  fd.append('date', media.date);
341
+ fd.append('media_details', media.media_details);
295
342
 
296
343
  return fd;
297
344
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caweb/cli",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "CAWebPublishing Command Line Interface.",
5
5
  "exports": "./lib/env.js",
6
6
  "type": "module",
@@ -62,6 +62,7 @@
62
62
  "accessibility-checker": "^3.1.67",
63
63
  "autoprefixer": "^10.4.17",
64
64
  "axios": "^1.6.7",
65
+ "axios-retry": "^4.0.0",
65
66
  "chalk": "^5.3.0",
66
67
  "commander": "^12.0.0",
67
68
  "cross-spawn": "^7.0.3",
package/bin/wp-cli.phar DELETED
Binary file