@miso.ai/server-wordpress 0.6.3-beta.1 → 0.6.3-beta.11

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/cli/entities.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Transform } from 'stream';
2
+ import split2 from 'split2';
2
3
  import { stream, parseDuration } from '@miso.ai/server-commons';
3
4
  import { WordPressClient } from '../src/index.js';
4
- import { normalizeOptions, normalizeTransform, parseDate } from './utils.js';
5
+ import { normalizeOptions, normalizeTransform } from './utils.js';
5
6
 
6
7
  export function buildForEntities(yargs) {
7
8
  // TODO: make them mutually exclusive
@@ -33,7 +34,7 @@ export function buildForEntities(yargs) {
33
34
  })
34
35
  .option('ids', {
35
36
  alias: 'include',
36
- describe: 'Specify post ids'
37
+ describe: 'Specify post ids',
37
38
  })
38
39
  .option('fields', {
39
40
  describe: 'Specify which record fields are retrieved',
@@ -76,6 +77,12 @@ async function run({ subcmd, count, terms, update, name, ...options }) {
76
77
  case 'count':
77
78
  await runCount(client, name, options);
78
79
  return;
80
+ case 'absence':
81
+ await runPresence(client, name, { present: false });
82
+ return;
83
+ case 'presence':
84
+ await runPresence(client, name, { present: true });
85
+ return;
79
86
  }
80
87
  if (count) {
81
88
  await runCount(client, name, options);
@@ -136,6 +143,17 @@ export async function runUpdate(client, name, update, options) {
136
143
  );
137
144
  }
138
145
 
146
+ export async function runPresence(client, name, options) {
147
+ await stream.pipeline(
148
+ process.stdin,
149
+ split2(),
150
+ client.entities(name).presence(options),
151
+ new stream.OutputStream({
152
+ objectMode: false,
153
+ }),
154
+ );
155
+ }
156
+
139
157
  async function buildUpdateStream(client, name, update, {
140
158
  date, after, before, orderBy, order, // strip off date filters and order criteria
141
159
  transform,
@@ -156,6 +174,14 @@ async function buildUpdateStream(client, name, update, {
156
174
  after: threshold,
157
175
  }),
158
176
  // get recent modified, excluding ones already fetched
177
+ entities.stream({
178
+ ...options,
179
+ transform,
180
+ orderBy: 'modified',
181
+ modifiedAfter: threshold,
182
+ before: threshold,
183
+ }),
184
+ /*
159
185
  entities.stream({
160
186
  ...options,
161
187
  transform,
@@ -168,6 +194,7 @@ async function buildUpdateStream(client, name, update, {
168
194
  terminate: entity => parseDate(entity.modified_gmt) < threshold,
169
195
  },
170
196
  })
197
+ */
171
198
  ])
172
199
  );
173
200
  }
package/cli/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { yargs } from '@miso.ai/server-commons';
3
3
  import version from '../src/version.js';
4
- import profile from './profile.js';
4
+ import { profile, init } from './profile.js';
5
5
  import taxonomies from './taxonomies.js';
6
6
  import entities from './entities.js';
7
7
 
@@ -16,11 +16,15 @@ yargs.build(yargs => {
16
16
  alias: 'p',
17
17
  describe: 'Site profile file location',
18
18
  })
19
+ .option('auth', {
20
+ describe: 'Authentication string',
21
+ })
19
22
  .option('debug', {
20
23
  type: 'boolean',
21
24
  default: false,
22
25
  })
23
26
  .hide('debug')
27
+ .command(init)
24
28
  .command(profile)
25
29
  .command(taxonomies)
26
30
  .command(entities)
package/cli/profile.js CHANGED
@@ -11,7 +11,7 @@ function build(yargs) {
11
11
  });
12
12
  }
13
13
 
14
- async function run({ generate, ...options }) {
14
+ async function runProfile({ generate, ...options }) {
15
15
  if (generate) {
16
16
  await runGenerate(options);
17
17
  } else {
@@ -38,9 +38,19 @@ async function runView(options) {
38
38
  console.log(JSON.stringify(client.profile, undefined, 2));
39
39
  }
40
40
 
41
- export default {
41
+ async function runInit(options) {
42
+ await runGenerate(options);
43
+ }
44
+
45
+ export const profile = {
42
46
  command: 'profile',
43
47
  desc: 'WordPress site profile management',
44
48
  builder: build,
45
- handler: run,
49
+ handler: runProfile,
50
+ };
51
+
52
+ export const init = {
53
+ command: 'init <site>',
54
+ desc: 'Initialize WordPress site profile',
55
+ handler: runInit,
46
56
  };
package/cli/utils.js CHANGED
@@ -3,8 +3,9 @@ import { startOfDate, endOfDate } from '@miso.ai/server-commons';
3
3
 
4
4
  const PWD = process.env.PWD;
5
5
 
6
- export function normalizeOptions({ date, after, before, ids, ...options }) {
6
+ export function normalizeOptions({ date, after, before, ids, include, ...options }) {
7
7
  [after, before] = [startOfDate(date || after), endOfDate(date || before)];
8
+ // TODO: rely on yargs to coerce to array
8
9
  ids = ids ? `${ids}`.split(',').map(s => s.trim()) : ids;
9
10
  return { ...options, after, before, ids };
10
11
  }
package/package.json CHANGED
@@ -17,9 +17,9 @@
17
17
  "simonpai <simon.pai@askmiso.com>"
18
18
  ],
19
19
  "dependencies": {
20
- "@miso.ai/server-commons": "0.6.3-beta.1",
21
- "axios": "^0.27.2",
20
+ "@miso.ai/server-commons": "0.6.3-beta.11",
21
+ "axios": "^1.6.2",
22
22
  "axios-retry": "^3.3.1"
23
23
  },
24
- "version": "0.6.3-beta.1"
24
+ "version": "0.6.3-beta.11"
25
25
  }
package/src/client.js CHANGED
@@ -70,7 +70,7 @@ export default class WordPressClient {
70
70
 
71
71
  }
72
72
 
73
- const SITE_PROFILE_PROPS = ['site', 'utcOffset'];
73
+ const SITE_PROFILE_PROPS = ['site', 'utcOffset', 'resources'];
74
74
 
75
75
  class SiteProfile {
76
76
 
@@ -2,7 +2,7 @@ import { asArray, Resolution } from '@miso.ai/server-commons';
2
2
 
3
3
  export default class EntityIndex {
4
4
 
5
- constructor(entities, { process, value } = {}) {
5
+ constructor(entities, { process, value, fields } = {}) {
6
6
  this._entities = entities;
7
7
  if (process) {
8
8
  this._process = process;
@@ -10,6 +10,7 @@ export default class EntityIndex {
10
10
  if (value) {
11
11
  this._value = (en => en && value(en)); // null-safe
12
12
  }
13
+ this._fields = fields;
13
14
  this.name = entities.name;
14
15
  this._index = new Map();
15
16
  this._notFound = new Set();
@@ -66,7 +67,7 @@ export default class EntityIndex {
66
67
  if (idsToFetch.length > 0) {
67
68
  (async () => {
68
69
  const idsFetchSet = new Set(idsToFetch);
69
- const stream = await this._entities.stream({ ids: idsToFetch });
70
+ const stream = await this._entities.stream({ ids: idsToFetch, fields: this._fields });
70
71
  for await (const entity of stream) {
71
72
  const { id } = entity;
72
73
  this._index.set(id, this._process(entity));
@@ -2,6 +2,7 @@ import { Transform } from 'stream';
2
2
  import { asArray, stream } from '@miso.ai/server-commons';
3
3
  import EntityIndex from './entity-index.js';
4
4
  import EntityTransformStream from './transform.js';
5
+ import EntityPresenceStream from './presence.js';
5
6
  import defaultTransform from './transform-default.js';
6
7
  import legacyTransform from './transform-legacy.js';
7
8
 
@@ -26,12 +27,17 @@ export default class Entities {
26
27
  // we need taxonomy fetched so we know whether it's hierarchical
27
28
  const taxonomies = await client._helpers.findAssociatedTaxonomies(this.name);
28
29
 
30
+ // TODO: omit specific indicies by config
29
31
  // prepare entity indicies
32
+ const { resources = {} } = client._profile || {};
33
+ const ignored = new Set(resources.ignore || []);
34
+
30
35
  const indicies = [
31
36
  client.users.index,
32
37
  client.media.index,
33
38
  ...taxonomies.map(({ rest_base }) => client.entities(rest_base).index),
34
- ];
39
+ ].filter(index => !ignored.has(index.name));
40
+
35
41
  await Promise.all(indicies.map(index => index.ready()));
36
42
  for (const index of indicies) {
37
43
  if (index.hierarchical) {
@@ -82,6 +88,10 @@ export default class Entities {
82
88
  return this._client._helpers.terms(this.name, options);
83
89
  }
84
90
 
91
+ presence(options) {
92
+ return new EntityPresenceStream(this._client, this.name, options);
93
+ }
94
+
85
95
  get index() {
86
96
  return this._index;
87
97
  }
@@ -0,0 +1,105 @@
1
+ import { Transform } from 'stream';
2
+
3
+ export default class EntityPresenceStream extends Transform {
4
+
5
+ constructor(client, name, {
6
+ present = true,
7
+ fetchSize = 20,
8
+ preserveOrder = true,
9
+ } = {}) {
10
+ super();
11
+ this._client = client;
12
+ this._name = name;
13
+ this._options = {
14
+ present,
15
+ fetchSize,
16
+ preserveOrder,
17
+ }
18
+ this._inputs = [];
19
+ this._pendingSet = new Set();
20
+ this._requests = [];
21
+ this._map = new Map();
22
+ this._done = false;
23
+ }
24
+
25
+ async _transform(id, _, next) {
26
+ id = `${id}`; // buffer -> string
27
+ if (id) {
28
+ this._inputs.push(id);
29
+ this._outputAll();
30
+ this._requestAll();
31
+ }
32
+ next();
33
+ }
34
+
35
+ _flush(done) {
36
+ this._done = done;
37
+ this._outputAll();
38
+ if (this._inputs.length > 0) {
39
+ this._requestAll(true);
40
+ }
41
+ }
42
+
43
+ _outputAll() {
44
+ // TODO: implement when preserveOrder = false
45
+ let i = 0;
46
+ for (const len = this._inputs.length; i < len; i++) {
47
+ const id = this._inputs[i];
48
+ const entry = this._map.get(id);
49
+ if (!entry || entry.value === undefined) {
50
+ break;
51
+ }
52
+ if (this._options.present === entry.value) {
53
+ this.push(id);
54
+ }
55
+ }
56
+ if (i > 0) {
57
+ this._inputs = this._inputs.slice(i);
58
+ }
59
+ if (this._done && this._inputs.length === 0) {
60
+ this._done();
61
+ }
62
+ }
63
+
64
+ _requestAll(flush = false) {
65
+ for (const id of this._inputs) {
66
+ this._fetchAll();
67
+ if (!this._map.has(id)) {
68
+ this._map.set(id, { status: 'pending' });
69
+ this._pendingSet.add(id);
70
+ }
71
+ }
72
+ this._fetchAll(flush);
73
+ }
74
+
75
+ async _fetchAll(flush = false) {
76
+ if (!flush && this._pendingSet.size < this._options.fetchSize) {
77
+ return;
78
+ }
79
+ const ids = Array.from(this._pendingSet);
80
+ for (const id of ids) {
81
+ this._map.get(id).status = 'fetching';
82
+ }
83
+ this._pendingSet = new Set();
84
+
85
+ const presences = await this._fetch(ids);
86
+
87
+ for (const id of ids) {
88
+ const entry = this._map.get(id);
89
+ entry.status = 'ready';
90
+ entry.value = presences.has(id);
91
+ }
92
+ this._outputAll();
93
+ }
94
+
95
+ async _fetch(ids) {
96
+ const url = await this._client._helpers.url.build(this._name, { include: ids, fields: ['id'] });
97
+ const { data } = await this._client._helpers.axios.get(url);
98
+ const presences = new Set();
99
+ for (const { id } of data) {
100
+ presences.add(`${id}`);
101
+ }
102
+ return presences;
103
+ }
104
+
105
+ }
@@ -14,14 +14,14 @@ export default function transform({
14
14
  modified_gmt,
15
15
  guid: {
16
16
  rendered: guid,
17
- },
17
+ } = {},
18
18
  slug,
19
19
  title: {
20
20
  rendered: title,
21
- },
21
+ } = {},
22
22
  content: {
23
23
  rendered: html,
24
- },
24
+ } = {},
25
25
  link: url,
26
26
  status,
27
27
  sticky,
@@ -42,6 +42,7 @@ export default function transform({
42
42
  product_id,
43
43
  type,
44
44
  created_at,
45
+ published_at: created_at,
45
46
  updated_at,
46
47
  title,
47
48
  cover_image,
package/src/helpers.js CHANGED
@@ -1,21 +1,49 @@
1
+ import axios from 'axios';
2
+ import axiosRetry from 'axios-retry';
1
3
  import { asNumber, splitObj, stream } from '@miso.ai/server-commons';
2
- import axios from './axios.js';
3
4
  import DataSource from './source/index.js';
5
+ import version from './version.js';
4
6
 
5
7
  const MS_PER_HOUR = 1000 * 60 * 60;
6
8
 
7
9
  const STREAM_OPTIONS = ['offset', 'limit', 'strategy', 'filter', 'transform', 'onLoad'];
8
10
 
11
+ function createAxios(client) {
12
+ const { auth } = client._options || {};
13
+ const headers = {
14
+ 'User-Agent': `MisoBot/${version}`,
15
+ };
16
+ if (auth) {
17
+ if (typeof auth === 'object' && auth.username && auth.password) {
18
+ auth = `${auth.username}:${auth.password}`;
19
+ }
20
+ if (typeof auth !== 'string') {
21
+ throw new TypeError(`Invalid auth: must me a string or an object.`);
22
+ }
23
+ headers['Authorization'] = 'Basic ' + Buffer.from(auth).toString('base64');
24
+ }
25
+ const instance = axios.create({
26
+ headers,
27
+ });
28
+ axiosRetry(instance, { retries: 5, retryDelay: count => count * 300 });
29
+ return instance;
30
+ }
31
+
9
32
  export default class Helpers {
10
33
 
11
34
  constructor(client) {
12
35
  this._start = Date.now();
13
36
  this._client = client;
37
+ this._axios = createAxios(client);
14
38
  this.url = new Url(this);
15
39
  this._samples = {};
16
40
  this.debug = this.debug.bind(this);
17
41
  }
18
42
 
43
+ get axios() {
44
+ return this._axios;
45
+ }
46
+
19
47
  async stream(resource, options) {
20
48
  const [streamOptions, sourceOptions] = splitObj(options, STREAM_OPTIONS);
21
49
  const source = new DataSource(this, resource, sourceOptions);
@@ -32,7 +60,7 @@ export default class Helpers {
32
60
 
33
61
  async _fetchSample(resource) {
34
62
  const url = await this.url.build(resource, { page: 0, pageSize: 1 });
35
- const { data, headers } = await axios.get(url);
63
+ const { data, headers } = await this.axios.get(url);
36
64
  if (!data.length) {
37
65
  throw new Error(`No record of ${resource} avaliable`);
38
66
  }
@@ -71,7 +99,7 @@ export default class Helpers {
71
99
 
72
100
  async _fetchTaxonomies() {
73
101
  const url = await this.url.build('taxonomies');
74
- const { data } = await axios.get(url);
102
+ const { data } = await this.axios.get(url);
75
103
  this.debug(`Fetched taxonomies.`);
76
104
  return Object.values(data);
77
105
  }
@@ -82,7 +110,7 @@ export default class Helpers {
82
110
 
83
111
  async count(resource, { offset: _, ...options } = {}) {
84
112
  const url = await this.url.build(resource, { ...options, page: 0, pageSize: 1 });
85
- const { headers } = await axios.get(url);
113
+ const { headers } = await this.axios.get(url);
86
114
  return asNumber(headers['x-wp-total']);
87
115
  }
88
116
 
@@ -92,7 +120,7 @@ export default class Helpers {
92
120
 
93
121
  async countUrl(url) {
94
122
  url = await this.url.append(url, { page: 0, pageSize: 1 });
95
- const { headers } = await axios.get(url);
123
+ const { headers } = await this.axios.get(url);
96
124
  return asNumber(headers['x-wp-total']);
97
125
  }
98
126
 
@@ -134,17 +162,19 @@ class Url {
134
162
  // modifiedAfter, modifiedBefore is supported since WordPress 5.7
135
163
  // https://make.wordpress.org/core/2021/02/23/rest-api-changes-in-wordpress-5-7/
136
164
  async append(url, options = {}) {
137
- const { after, before, order, orderBy, page, pageSize, offset, include, exclude } = options;
165
+ const { after, before, modifiedAfter, modifiedBefore, order, orderBy, page, pageSize, offset, include, exclude } = options;
138
166
  let { fields } = options;
139
167
  const params = [];
140
168
 
141
169
  // TODO: support single id
142
170
 
143
171
  // The date is compared against site's local time, not UTC, so we have to work on timezone offset
144
- if (has(after) || has(before)) {
172
+ if (has(after) || has(before) || has(modifiedAfter) || has(modifiedBefore)) {
145
173
  const utcOffset = await this._helpers.utcOffsetInMs();
146
174
  has(after) && params.push(`after=${toISOString(after, utcOffset)}`);
147
175
  has(before) && params.push(`before=${toISOString(before, utcOffset)}`);
176
+ has(modifiedAfter) && params.push(`modified_after=${toISOString(modifiedAfter, utcOffset)}`);
177
+ has(modifiedBefore) && params.push(`modified_before=${toISOString(modifiedBefore, utcOffset)}`);
148
178
  }
149
179
 
150
180
  has(order) && params.push(`order=${order}`);
@@ -1,5 +1,3 @@
1
- import axios from '../axios.js';
2
-
3
1
  export default class WordPressDataSource {
4
2
 
5
3
  constructor(helpers, resource, options = {}) {
@@ -32,14 +30,17 @@ export default class WordPressDataSource {
32
30
  this._debug(`[WordPressDataSource] request ${url}`);
33
31
  const response = await this._axiosGet(url);
34
32
  this._debug(`[WordPressDataSource] response ${response.status} ${url}`);
35
- return this._process(response);
33
+ return this._process(response, { url });
36
34
  }
37
35
 
38
- _process({ status, data }) {
36
+ _process({ status, data }, { url }) {
39
37
  if (status >= 400 && status < 500 && data.code === 'rest_post_invalid_page_number') {
40
38
  // out of bound, so there is no more data
41
39
  return { data: [], terminate: true };
42
40
  }
41
+ if (!Array.isArray(data)) {
42
+ throw new Error(`Unexpected response from WordPress API for ${url}. Expected an array of objects: ${data}`);
43
+ }
43
44
  if (!this._options.preserveLinks) {
44
45
  data = data.map(this._helpers.removeLinks);
45
46
  }
@@ -52,13 +53,13 @@ export default class WordPressDataSource {
52
53
 
53
54
  async _buildBaseUrl() {
54
55
  // exclude parameters meant to be dealt with state
55
- const { page, ...options } = this._options;
56
+ const { page, ids, ...options } = this._options;
56
57
  return this._helpers.url.build(this._resource, options);
57
58
  }
58
59
 
59
60
  async _axiosGet(url) {
60
61
  try {
61
- return await axios.get(url);
62
+ return await this._helpers.axios.get(url);
62
63
  } catch(error) {
63
64
  if (error.response) {
64
65
  return error.response;
@@ -50,8 +50,8 @@ export default class PagedWordPressDataSource extends WordPressDataSource {
50
50
  return total;
51
51
  }
52
52
 
53
- _process({ status, data, headers }) {
54
- const result = super._process({ status, data, headers });
53
+ _process({ status, data, headers }, meta) {
54
+ const result = super._process({ status, data, headers }, meta);
55
55
  const total = asNumber(headers['x-wp-total']);
56
56
  if (total !== undefined) {
57
57
  result.total = total;
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '0.6.3-beta.1';
1
+ export default '0.6.3-beta.11';
package/src/axios.js DELETED
@@ -1,6 +0,0 @@
1
- import axios from 'axios';
2
- import axiosRetry from 'axios-retry';
3
-
4
- axiosRetry(axios, { retries: 5, retryDelay: count => count * 300 });
5
-
6
- export default axios;