@miso.ai/server-wordpress 0.6.0-beta.1
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 +141 -0
- package/cli/index.js +28 -0
- package/cli/profile.js +46 -0
- package/cli/taxonomies.js +21 -0
- package/cli/utils.js +24 -0
- package/package.json +25 -0
- package/src/axios.js +6 -0
- package/src/client.js +97 -0
- package/src/entities/entity-index.js +160 -0
- package/src/entities/index.js +95 -0
- package/src/entities/transform-default.js +63 -0
- package/src/entities/transform-legacy.js +63 -0
- package/src/entities/transform.js +57 -0
- package/src/helpers.js +173 -0
- package/src/index.js +1 -0
- package/src/media.js +26 -0
- package/src/posts/index.js +21 -0
- package/src/source/base.js +74 -0
- package/src/source/ids.js +37 -0
- package/src/source/index.js +7 -0
- package/src/source/paged.js +65 -0
- package/src/version.js +1 -0
package/cli/entities.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { stream, parseDuration } from '@miso.ai/server-commons';
|
|
2
|
+
import { WordPressClient } from '../src/index.js';
|
|
3
|
+
import { normalizeOptions, normalizeTransform, parseDate } from './utils.js';
|
|
4
|
+
|
|
5
|
+
export function buildForEntities(yargs) {
|
|
6
|
+
// TODO: make them mutually exclusive
|
|
7
|
+
return yargs
|
|
8
|
+
.option('terms', {
|
|
9
|
+
describe: 'Display terms associated with this type of resource',
|
|
10
|
+
type: 'boolean',
|
|
11
|
+
})
|
|
12
|
+
.option('count', {
|
|
13
|
+
alias: 'c',
|
|
14
|
+
describe: 'Return the total number of records',
|
|
15
|
+
type: 'boolean',
|
|
16
|
+
})
|
|
17
|
+
.option('date', {
|
|
18
|
+
alias: 'd',
|
|
19
|
+
describe: 'Only include records in this year/month/day',
|
|
20
|
+
})
|
|
21
|
+
.option('after', {
|
|
22
|
+
alias: 'a',
|
|
23
|
+
describe: 'Only include records after this time',
|
|
24
|
+
})
|
|
25
|
+
.option('before', {
|
|
26
|
+
alias: 'b',
|
|
27
|
+
describe: 'Only include records before this time',
|
|
28
|
+
})
|
|
29
|
+
.option('update', {
|
|
30
|
+
alias: 'u',
|
|
31
|
+
describe: 'Only include records modified in given duration (3h, 2d, etc.)',
|
|
32
|
+
})
|
|
33
|
+
.option('ids', {
|
|
34
|
+
alias: 'include',
|
|
35
|
+
describe: 'Specify post ids'
|
|
36
|
+
})
|
|
37
|
+
.option('resolve', {
|
|
38
|
+
alias: 'r',
|
|
39
|
+
describe: 'Attach resolved entities (author, catagories) linked with the subjects',
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
})
|
|
42
|
+
.option('transform', {
|
|
43
|
+
alias: 't',
|
|
44
|
+
describe: 'Apply transform function to the entities',
|
|
45
|
+
});
|
|
46
|
+
/*
|
|
47
|
+
.option('limit', {
|
|
48
|
+
alias: 'n',
|
|
49
|
+
describe: 'Limit the amount of records',
|
|
50
|
+
type: 'number',
|
|
51
|
+
})
|
|
52
|
+
*/
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function build(yargs) {
|
|
56
|
+
return buildForEntities(yargs)
|
|
57
|
+
.positional('name', {
|
|
58
|
+
describe: 'Resource type',
|
|
59
|
+
type: 'string',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function run({ count, terms, update, name, ...options }) {
|
|
64
|
+
options = normalizeOptions(options);
|
|
65
|
+
const client = new WordPressClient(options);
|
|
66
|
+
if (count) {
|
|
67
|
+
await runCount(client, name, options);
|
|
68
|
+
} else if (terms) {
|
|
69
|
+
await runTerms(client, name, options);
|
|
70
|
+
} else if (update) {
|
|
71
|
+
await runUpdate(client, name, update, options);
|
|
72
|
+
} else {
|
|
73
|
+
await runGet(client, name, options);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function runCount(client, name, options) {
|
|
78
|
+
console.log(await client.entities(name).count(options));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function runTerms(client, name, options) {
|
|
82
|
+
const terms = await client.entities(name).terms(options);
|
|
83
|
+
for (const term of terms) {
|
|
84
|
+
console.log(JSON.stringify(term));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function runGet(client, name, { transform, ...options }) {
|
|
89
|
+
await stream.pipelineToStdout(
|
|
90
|
+
await client.entities(name).stream({
|
|
91
|
+
...options,
|
|
92
|
+
transform: await normalizeTransform(transform),
|
|
93
|
+
}),
|
|
94
|
+
stream.stringify(),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function runUpdate(client, name, update, {
|
|
99
|
+
date, after, before, orderBy, order, // strip off date filters and order criteria
|
|
100
|
+
transform,
|
|
101
|
+
...options
|
|
102
|
+
}) {
|
|
103
|
+
transform = await normalizeTransform(transform);
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
update = parseDuration(update);
|
|
106
|
+
const threshold = now - update;
|
|
107
|
+
const entities = client.entities(name);
|
|
108
|
+
await stream.pipelineToStdout(
|
|
109
|
+
stream.concat(
|
|
110
|
+
...await Promise.all([
|
|
111
|
+
// get recent published
|
|
112
|
+
entities.stream({
|
|
113
|
+
...options,
|
|
114
|
+
transform,
|
|
115
|
+
after: threshold,
|
|
116
|
+
}),
|
|
117
|
+
// get recent modified, excluding ones already fetched
|
|
118
|
+
entities.stream({
|
|
119
|
+
...options,
|
|
120
|
+
transform,
|
|
121
|
+
orderBy: 'modified',
|
|
122
|
+
before: threshold,
|
|
123
|
+
pageSize: 20,
|
|
124
|
+
strategy: {
|
|
125
|
+
highWatermark: 100,
|
|
126
|
+
eagerLoad: true,
|
|
127
|
+
terminate: entity => parseDate(entity.modified_gmt) < threshold,
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
])
|
|
131
|
+
),
|
|
132
|
+
stream.stringify(),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default {
|
|
137
|
+
command: ['$0 <name>'],
|
|
138
|
+
desc: 'List entities from WordPress REST API',
|
|
139
|
+
builder: build,
|
|
140
|
+
handler: run,
|
|
141
|
+
};
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { yargs } from '@miso.ai/server-commons';
|
|
3
|
+
import version from '../src/version.js';
|
|
4
|
+
import profile from './profile.js';
|
|
5
|
+
import taxonomies from './taxonomies.js';
|
|
6
|
+
import entities from './entities.js';
|
|
7
|
+
|
|
8
|
+
yargs.build(yargs => {
|
|
9
|
+
yargs
|
|
10
|
+
.env('MISO_WORDPRESS')
|
|
11
|
+
.option('site', {
|
|
12
|
+
alias: 's',
|
|
13
|
+
describe: 'the WordPress site',
|
|
14
|
+
})
|
|
15
|
+
.option('profile', {
|
|
16
|
+
alias: 'p',
|
|
17
|
+
describe: 'Site profile file location',
|
|
18
|
+
})
|
|
19
|
+
.option('debug', {
|
|
20
|
+
type: 'boolean',
|
|
21
|
+
default: false,
|
|
22
|
+
})
|
|
23
|
+
.hide('debug')
|
|
24
|
+
.command(profile)
|
|
25
|
+
.command(taxonomies)
|
|
26
|
+
.command(entities)
|
|
27
|
+
.version(version);
|
|
28
|
+
});
|
package/cli/profile.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { fileExists } from '@miso.ai/server-commons';
|
|
3
|
+
import { WordPressClient } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
function build(yargs) {
|
|
6
|
+
return yargs
|
|
7
|
+
.option('generate', {
|
|
8
|
+
alias: 'g',
|
|
9
|
+
describe: 'Generate profile',
|
|
10
|
+
type: 'boolean',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function run({ generate, ...options }) {
|
|
15
|
+
if (generate) {
|
|
16
|
+
await runGenerate(options);
|
|
17
|
+
} else {
|
|
18
|
+
await runView(options);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function runGenerate(options) {
|
|
23
|
+
// it's OK if profile file doesn't exist in generate mode
|
|
24
|
+
// but we need to leave it out from the options
|
|
25
|
+
const { profile } = options;
|
|
26
|
+
if (profile && !await fileExists(profile)) {
|
|
27
|
+
delete options.profile;
|
|
28
|
+
}
|
|
29
|
+
const client = new WordPressClient(options);
|
|
30
|
+
await client.generateProfile();
|
|
31
|
+
await client.saveProfile(profile);
|
|
32
|
+
const location = resolve(process.env.PWD, profile || WordPressClient.DEFAULT_PROFILE_LOCATION);
|
|
33
|
+
console.log(`WordPress profile generated at: ${location}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function runView(options) {
|
|
37
|
+
const client = new WordPressClient(options);
|
|
38
|
+
console.log(JSON.stringify(client.profile, undefined, 2));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
command: 'profile',
|
|
43
|
+
desc: 'WordPress site profile management',
|
|
44
|
+
builder: build,
|
|
45
|
+
handler: run,
|
|
46
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { WordPressClient } from '../src/index.js';
|
|
2
|
+
|
|
3
|
+
function build(yargs) {
|
|
4
|
+
return yargs;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function run(options) {
|
|
8
|
+
const client = new WordPressClient(options);
|
|
9
|
+
const taxonomies = await client.taxonomies({ noCache: true });
|
|
10
|
+
for (const taxonomy of taxonomies) {
|
|
11
|
+
console.log(JSON.stringify(taxonomy));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
command: 'taxonomies',
|
|
17
|
+
aliases: ['tax'],
|
|
18
|
+
desc: 'List taxonomies from WordPress REST API',
|
|
19
|
+
builder: build,
|
|
20
|
+
handler: run,
|
|
21
|
+
};
|
package/cli/utils.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { startOfDate, endOfDate } from '@miso.ai/server-commons';
|
|
3
|
+
|
|
4
|
+
const PWD = process.env.PWD;
|
|
5
|
+
|
|
6
|
+
export function normalizeOptions({ date, after, before, ids, ...options }) {
|
|
7
|
+
[after, before] = [startOfDate(date || after), endOfDate(date || before)];
|
|
8
|
+
ids = ids ? `${ids}`.split(',').map(s => s.trim()) : ids;
|
|
9
|
+
return { ...options, after, before, ids };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function normalizeTransform(transform) {
|
|
13
|
+
if (typeof transform === 'string') {
|
|
14
|
+
if (transform === 'default' || transform === 'legacy') {
|
|
15
|
+
return transform;
|
|
16
|
+
}
|
|
17
|
+
return (await import(join(PWD, transform))).default;
|
|
18
|
+
}
|
|
19
|
+
return !!transform;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseDate(value) {
|
|
23
|
+
return Date.parse(`${value}Z`);
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@miso.ai/server-wordpress",
|
|
3
|
+
"description": "Miso WordPress API data tools",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"miso-wordpress": "cli/index.js",
|
|
8
|
+
"miso-wp": "cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {},
|
|
14
|
+
"repository": "MisoAI/miso-server-js-sdk",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"contributors": [
|
|
17
|
+
"simonpai <simon.pai@askmiso.com>"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@miso.ai/server-commons": "0.6.0-beta.1",
|
|
21
|
+
"axios": "^0.27.2",
|
|
22
|
+
"axios-retry": "^3.3.1"
|
|
23
|
+
},
|
|
24
|
+
"version": "0.6.0-beta.1"
|
|
25
|
+
}
|
package/src/axios.js
ADDED
package/src/client.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { defineValues, copyValues, trimObj, loadConfigSync, saveConfig, splitObj, fileExistsSync } from '@miso.ai/server-commons';
|
|
2
|
+
import { constants } from 'fs';
|
|
3
|
+
import Helpers from './helpers.js';
|
|
4
|
+
import Media from './media.js';
|
|
5
|
+
import Posts from './posts/index.js';
|
|
6
|
+
import Entities from './entities/index.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_PROFILE = './wordpress.json';
|
|
9
|
+
|
|
10
|
+
export default class WordPressClient {
|
|
11
|
+
|
|
12
|
+
static DEFAULT_PROFILE_LOCATION = DEFAULT_PROFILE;
|
|
13
|
+
|
|
14
|
+
constructor({ profile, ...options } = {}) {
|
|
15
|
+
if (!profile && fileExistsSync(DEFAULT_PROFILE, constants.R_OK)) {
|
|
16
|
+
profile = DEFAULT_PROFILE;
|
|
17
|
+
}
|
|
18
|
+
if (profile) {
|
|
19
|
+
options = { ...loadConfigSync(profile), ...options };
|
|
20
|
+
}
|
|
21
|
+
if (!options.site) {
|
|
22
|
+
throw new Error(`Require 'site' property in either options or profile.`);
|
|
23
|
+
}
|
|
24
|
+
defineValues(this, { site: options.site });
|
|
25
|
+
let profileObj;
|
|
26
|
+
[profileObj, options] = splitObj(options, SITE_PROFILE_PROPS);
|
|
27
|
+
this._options = options;
|
|
28
|
+
this._profile = new SiteProfile(profileObj);
|
|
29
|
+
|
|
30
|
+
this._helpers = new Helpers(this);
|
|
31
|
+
this._entities = {
|
|
32
|
+
media: new Media(this),
|
|
33
|
+
posts: new Posts(this),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async generateProfile() {
|
|
38
|
+
this._profile.clear();
|
|
39
|
+
// write UTC offset
|
|
40
|
+
await this._helpers.utcOffsetInMs();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async saveProfile(file = DEFAULT_PROFILE) {
|
|
44
|
+
await saveConfig(file, this._profile.export());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get profile() {
|
|
48
|
+
return this._profile.export();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
taxonomies(options) {
|
|
52
|
+
return this._helpers.taxonomies(options);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
get posts() {
|
|
56
|
+
return this.entities('posts');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get users() {
|
|
60
|
+
return this.entities('users');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get media() {
|
|
64
|
+
return this.entities('media');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
entities(name) {
|
|
68
|
+
return this._entities[name] || (this._entities[name] = new Entities(this, name));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const SITE_PROFILE_PROPS = ['site', 'utcOffset'];
|
|
74
|
+
|
|
75
|
+
class SiteProfile {
|
|
76
|
+
|
|
77
|
+
constructor(profile) {
|
|
78
|
+
profile && this.load(profile);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clear() {
|
|
82
|
+
for (const key in this) {
|
|
83
|
+
if (key !== 'site') {
|
|
84
|
+
delete this[key];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
load(profile) {
|
|
90
|
+
copyValues(this, profile, SITE_PROFILE_PROPS);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export() {
|
|
94
|
+
return trimObj(copyValues({}, this, SITE_PROFILE_PROPS));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { asArray, Resolution } from '@miso.ai/server-commons';
|
|
2
|
+
|
|
3
|
+
export default class EntityIndex {
|
|
4
|
+
|
|
5
|
+
constructor(entities, { process, value } = {}) {
|
|
6
|
+
this._entities = entities;
|
|
7
|
+
if (process) {
|
|
8
|
+
this._process = process;
|
|
9
|
+
}
|
|
10
|
+
if (value) {
|
|
11
|
+
this._value = (en => en && value(en)); // null-safe
|
|
12
|
+
}
|
|
13
|
+
this.name = entities.name;
|
|
14
|
+
this._index = new Map();
|
|
15
|
+
this._notFound = new Set();
|
|
16
|
+
this._fetching = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async ready() {
|
|
20
|
+
if (!this._ready) {
|
|
21
|
+
this._ready = this._build();
|
|
22
|
+
}
|
|
23
|
+
return this._ready;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async _dataReady() {
|
|
27
|
+
await this.ready();
|
|
28
|
+
if (this.hierarchical) {
|
|
29
|
+
return this._allFetched || (this._allFetched = this._fetchAll());
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async _fetchAll() {
|
|
34
|
+
const records = await this._entities.getAll();
|
|
35
|
+
for (const record of records) {
|
|
36
|
+
this._index.set(record.id, record);
|
|
37
|
+
}
|
|
38
|
+
if (this.hierarchical) {
|
|
39
|
+
shimFullPath(records, this._index);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async _build() {
|
|
44
|
+
const taxonomy = this._taxonomy = await this._entities._taxonomy();
|
|
45
|
+
this.hierarchical = !!(taxonomy && taxonomy.hierarchical);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async fetch(ids) {
|
|
49
|
+
if (this.hierarchical) {
|
|
50
|
+
return; // already all fetched
|
|
51
|
+
}
|
|
52
|
+
ids = asArray(ids);
|
|
53
|
+
|
|
54
|
+
const promises = []
|
|
55
|
+
const idsToFetch = [];
|
|
56
|
+
for (const id of ids) {
|
|
57
|
+
if (this._index.has(id) || this._notFound.has(id)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!this._fetching.has(id)) {
|
|
61
|
+
this._fetching.set(id, new Resolution());
|
|
62
|
+
idsToFetch.push(id);
|
|
63
|
+
}
|
|
64
|
+
promises.push(this._fetching.get(id).promise);
|
|
65
|
+
}
|
|
66
|
+
if (idsToFetch.length > 0) {
|
|
67
|
+
(async () => {
|
|
68
|
+
const idsFetchSet = new Set(idsToFetch);
|
|
69
|
+
const stream = await this._entities.stream({ ids: idsToFetch });
|
|
70
|
+
for await (const entity of stream) {
|
|
71
|
+
const { id } = entity;
|
|
72
|
+
this._index.set(id, this._process(entity));
|
|
73
|
+
idsFetchSet.delete(id);
|
|
74
|
+
this._resolveFetch(id);
|
|
75
|
+
}
|
|
76
|
+
// handle unavailable ones
|
|
77
|
+
for (const id of idsFetchSet) {
|
|
78
|
+
this._notFound.add(id);
|
|
79
|
+
this._resolveFetch(id);
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
}
|
|
83
|
+
return Promise.all(promises);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_resolveFetch(id) {
|
|
87
|
+
this._fetching.get(id).resolve();
|
|
88
|
+
this._fetching.delete(id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async get(id) {
|
|
92
|
+
await this._dataReady();
|
|
93
|
+
await this.fetch([id]);
|
|
94
|
+
return this._index.get(id);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getAll(ids) {
|
|
98
|
+
await this._dataReady();
|
|
99
|
+
await this.fetch(ids);
|
|
100
|
+
return ids.map(id => this._index.get(id));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getValue(id) {
|
|
104
|
+
if (id === undefined) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return this._value(await this.get(id));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async getValues(ids = []) {
|
|
111
|
+
if (ids.length === 0) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
const entities = await this.getAll(ids);
|
|
115
|
+
return entities.map(en => this._value(en)).filter(v => v);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_process(entity) {
|
|
119
|
+
return entity;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_value(entity) {
|
|
123
|
+
return entity && (this.hierarchical ? entity.fullPath.names : entity.name);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async patch(post, propName) {
|
|
127
|
+
propName = propName || this.name;
|
|
128
|
+
const { [propName]: ids } = post;
|
|
129
|
+
const value = await (Array.isArray(ids) ? this.getValues(ids) : this.getValue(ids));
|
|
130
|
+
return value ? { [propName]: value } : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function shimFullPath(entities, cache) {
|
|
136
|
+
// DP to compute full path
|
|
137
|
+
function fullPath(entity) {
|
|
138
|
+
if (!entity.fullPath) {
|
|
139
|
+
const { parent, id, name } = entity;
|
|
140
|
+
if (parent) {
|
|
141
|
+
const { ids, names } = fullPath(cache.get(parent));
|
|
142
|
+
entity.fullPath = {
|
|
143
|
+
ids: [...ids, id],
|
|
144
|
+
names: [...names, name],
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
entity.fullPath = {
|
|
148
|
+
ids: [id],
|
|
149
|
+
names: [name],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return entity.fullPath;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return entities.map(c => {
|
|
157
|
+
fullPath(c);
|
|
158
|
+
return c;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { asArray, stream } from '@miso.ai/server-commons';
|
|
2
|
+
import EntityIndex from './entity-index.js';
|
|
3
|
+
import EntityTransformStream from './transform.js';
|
|
4
|
+
import defaultTransform from './transform-default.js';
|
|
5
|
+
import legacyTransform from './transform-legacy.js';
|
|
6
|
+
|
|
7
|
+
export default class Entities {
|
|
8
|
+
|
|
9
|
+
constructor(client, name) {
|
|
10
|
+
this._client = client;
|
|
11
|
+
this.name = name;
|
|
12
|
+
this._index = this._createIndex();
|
|
13
|
+
Object.freeze(this);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async stream({ resolve = false, transform, ...options } = {}) {
|
|
17
|
+
if (!resolve && !transform) {
|
|
18
|
+
return this._client._helpers.stream(this.name, options);
|
|
19
|
+
}
|
|
20
|
+
transform = getTransformFn(transform);
|
|
21
|
+
|
|
22
|
+
const client = this._client;
|
|
23
|
+
|
|
24
|
+
// we need taxonomy fetched so we know whether it's hierarchical
|
|
25
|
+
const taxonomies = await client._helpers.findAssociatedTaxonomies(this.name);
|
|
26
|
+
|
|
27
|
+
// prepare entity indicies
|
|
28
|
+
const indicies = [
|
|
29
|
+
client.users.index,
|
|
30
|
+
client.media.index,
|
|
31
|
+
...taxonomies.map(({ rest_base }) => client.entities(rest_base).index),
|
|
32
|
+
];
|
|
33
|
+
await Promise.all(indicies.map(index => index.ready()));
|
|
34
|
+
for (const index of indicies) {
|
|
35
|
+
if (index.hierarchical) {
|
|
36
|
+
index._dataReady(); // kick off fetch all, but don't wait
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// onLoad function, which let us fill index caches more efficiently
|
|
41
|
+
const onLoad = records => {
|
|
42
|
+
for (const index of indicies) {
|
|
43
|
+
if (!index.hierarchical) {
|
|
44
|
+
index.fetch(aggregateIds(records, EntityTransformStream.getPropName(index.name)));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// transform stream
|
|
50
|
+
const transformStream = new EntityTransformStream(this._client, indicies, { transform });
|
|
51
|
+
|
|
52
|
+
return (await this._client._helpers.stream(this.name, { ...options, onLoad }))
|
|
53
|
+
.pipe(transformStream);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getAll(options) {
|
|
57
|
+
return stream.collect(await this.stream(options));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async count(options) {
|
|
61
|
+
return this._client._helpers.count(this.name, options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async terms(options) {
|
|
65
|
+
return this._client._helpers.terms(this.name, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get index() {
|
|
69
|
+
return this._index;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_createIndex() {
|
|
73
|
+
return new EntityIndex(this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async _taxonomy(options) {
|
|
77
|
+
return await this._client._helpers.findTaxonomyByResourceName(this.name, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function aggregateIds(records, propName) {
|
|
83
|
+
return Array.from(records.reduce((idSet, record) => {
|
|
84
|
+
for (const id of asArray(record[propName])) {
|
|
85
|
+
idSet.add(id);
|
|
86
|
+
}
|
|
87
|
+
return idSet;
|
|
88
|
+
}, new Set()));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getTransformFn(transform) {
|
|
92
|
+
return typeof transform === 'function' ? post => transform(post, { defaultTransform }) :
|
|
93
|
+
(transform === true || transform === 'default') ? defaultTransform :
|
|
94
|
+
transform === 'legacy' ? legacyTransform : undefined;
|
|
95
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { trimObj, asArray } from '@miso.ai/server-commons';
|
|
2
|
+
|
|
3
|
+
export default function transform({
|
|
4
|
+
_linked: {
|
|
5
|
+
author,
|
|
6
|
+
featured_media,
|
|
7
|
+
categories,
|
|
8
|
+
tags,
|
|
9
|
+
//..._linked
|
|
10
|
+
} = {},
|
|
11
|
+
id,
|
|
12
|
+
type,
|
|
13
|
+
date_gmt,
|
|
14
|
+
modified_gmt,
|
|
15
|
+
guid: {
|
|
16
|
+
rendered: guid,
|
|
17
|
+
},
|
|
18
|
+
slug,
|
|
19
|
+
title: {
|
|
20
|
+
rendered: title,
|
|
21
|
+
},
|
|
22
|
+
content: {
|
|
23
|
+
rendered: html,
|
|
24
|
+
},
|
|
25
|
+
link: url,
|
|
26
|
+
status,
|
|
27
|
+
sticky,
|
|
28
|
+
comment_status,
|
|
29
|
+
ping_status,
|
|
30
|
+
}) {
|
|
31
|
+
const product_id = `${id}`;
|
|
32
|
+
if (!product_id) {
|
|
33
|
+
throw new Error(`ID is absent.`);
|
|
34
|
+
}
|
|
35
|
+
const created_at = date_gmt && `${date_gmt}Z`;
|
|
36
|
+
const updated_at = modified_gmt && `${modified_gmt}Z`;
|
|
37
|
+
|
|
38
|
+
const authors = asArray(author);
|
|
39
|
+
const cover_image = featured_media && encodeURI(featured_media);
|
|
40
|
+
|
|
41
|
+
return trimObj({
|
|
42
|
+
product_id,
|
|
43
|
+
type,
|
|
44
|
+
created_at,
|
|
45
|
+
updated_at,
|
|
46
|
+
title,
|
|
47
|
+
cover_image,
|
|
48
|
+
html,
|
|
49
|
+
url,
|
|
50
|
+
authors,
|
|
51
|
+
categories,
|
|
52
|
+
tags,
|
|
53
|
+
custom_attributes: trimObj({
|
|
54
|
+
guid,
|
|
55
|
+
slug,
|
|
56
|
+
status,
|
|
57
|
+
sticky,
|
|
58
|
+
comment_status,
|
|
59
|
+
ping_status,
|
|
60
|
+
//..._linked,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { trimObj, asArray } from '@miso.ai/server-commons';
|
|
2
|
+
|
|
3
|
+
export default function transform({
|
|
4
|
+
_linked = {},
|
|
5
|
+
id,
|
|
6
|
+
type,
|
|
7
|
+
date_gmt,
|
|
8
|
+
modified_gmt,
|
|
9
|
+
title: {
|
|
10
|
+
rendered: title,
|
|
11
|
+
},
|
|
12
|
+
content: {
|
|
13
|
+
rendered: html,
|
|
14
|
+
},
|
|
15
|
+
author: authorId,
|
|
16
|
+
categories: categoryIds,
|
|
17
|
+
tags_names: tags,
|
|
18
|
+
link: url,
|
|
19
|
+
better_featured_image,
|
|
20
|
+
status,
|
|
21
|
+
sticky,
|
|
22
|
+
comment_status,
|
|
23
|
+
ping_status,
|
|
24
|
+
format,
|
|
25
|
+
yoast,
|
|
26
|
+
}) {
|
|
27
|
+
const product_id = `${id}`;
|
|
28
|
+
if (!product_id) {
|
|
29
|
+
throw new Error(`ID is absent.`);
|
|
30
|
+
}
|
|
31
|
+
const created_at = date_gmt && `${date_gmt}Z`;
|
|
32
|
+
const updated_at = modified_gmt && `${modified_gmt}Z`;
|
|
33
|
+
const authors = asArray(_linked.author);
|
|
34
|
+
const categories = _linked.categories;
|
|
35
|
+
|
|
36
|
+
// TODO: ad-hoc
|
|
37
|
+
const description = yoast && yoast.metadesc;
|
|
38
|
+
const cover_image = better_featured_image && better_featured_image.source_url && encodeURI(better_featured_image.source_url);
|
|
39
|
+
|
|
40
|
+
return trimObj({
|
|
41
|
+
product_id,
|
|
42
|
+
type,
|
|
43
|
+
created_at,
|
|
44
|
+
updated_at,
|
|
45
|
+
title,
|
|
46
|
+
description,
|
|
47
|
+
html,
|
|
48
|
+
cover_image,
|
|
49
|
+
url,
|
|
50
|
+
authors,
|
|
51
|
+
categories,
|
|
52
|
+
tags,
|
|
53
|
+
custom_attributes: trimObj({
|
|
54
|
+
status,
|
|
55
|
+
sticky,
|
|
56
|
+
comment_status,
|
|
57
|
+
ping_status,
|
|
58
|
+
format,
|
|
59
|
+
authorId,
|
|
60
|
+
categoryIds,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Transform } from 'stream';
|
|
2
|
+
|
|
3
|
+
function applyPatch(post, patch) {
|
|
4
|
+
if (!patch) {
|
|
5
|
+
return post;
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
...post,
|
|
9
|
+
_linked: {
|
|
10
|
+
...post._linked,
|
|
11
|
+
...patch,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const PROP_NAME_OVERRIDES = {
|
|
17
|
+
users: 'author',
|
|
18
|
+
media: 'featured_media',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default class EntityTransformStream extends Transform {
|
|
22
|
+
|
|
23
|
+
static getPropName(resource) {
|
|
24
|
+
return PROP_NAME_OVERRIDES[resource] || resource;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
constructor(client, indicies, { transform } = {}) {
|
|
28
|
+
super({ objectMode: true });
|
|
29
|
+
this._client = client;
|
|
30
|
+
this._indicies = indicies;
|
|
31
|
+
this._transformFn = transform;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async _transform(post, _, next) {
|
|
35
|
+
try {
|
|
36
|
+
post = await this._resolveLinkedEntities(post);
|
|
37
|
+
if (this._transformFn) {
|
|
38
|
+
post = await this._transformFn(post);
|
|
39
|
+
}
|
|
40
|
+
next(undefined, post);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
next(error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async _resolveLinkedEntities(post) {
|
|
47
|
+
const patches = await Promise.all(
|
|
48
|
+
this._indicies.map(index =>
|
|
49
|
+
index.patch(post, EntityTransformStream.getPropName(index.name)))
|
|
50
|
+
);
|
|
51
|
+
for (const patch of patches) {
|
|
52
|
+
post = applyPatch(post, patch);
|
|
53
|
+
}
|
|
54
|
+
return post;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
package/src/helpers.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { asNumber, splitObj, stream } from '@miso.ai/server-commons';
|
|
2
|
+
import axios from './axios.js';
|
|
3
|
+
import DataSource from './source/index.js';
|
|
4
|
+
|
|
5
|
+
const MS_PER_HOUR = 1000 * 60 * 60;
|
|
6
|
+
|
|
7
|
+
const STREAM_OPTIONS = ['offset', 'limit', 'strategy', 'filter', 'transform', 'onLoad'];
|
|
8
|
+
|
|
9
|
+
export default class Helpers {
|
|
10
|
+
|
|
11
|
+
constructor(client) {
|
|
12
|
+
this._start = Date.now();
|
|
13
|
+
this._client = client;
|
|
14
|
+
this.url = new Url(this);
|
|
15
|
+
this._samples = {};
|
|
16
|
+
this.debug = this.debug.bind(this);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async stream(resource, options) {
|
|
20
|
+
const [streamOptions, sourceOptions] = splitObj(options, STREAM_OPTIONS);
|
|
21
|
+
const source = new DataSource(this, resource, sourceOptions);
|
|
22
|
+
return new stream.BufferedReadStream(source, { ...streamOptions, debug: this.debug });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async sample(resource, { noCache = false } = {}) {
|
|
26
|
+
if (noCache || !this._samples[resource]) {
|
|
27
|
+
// don't await, save the promise
|
|
28
|
+
this._samples[resource] = this._fetchSample(resource);
|
|
29
|
+
}
|
|
30
|
+
return this._samples[resource];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async _fetchSample(resource) {
|
|
34
|
+
const url = await this.url.build(resource, { page: 0, pageSize: 1 });
|
|
35
|
+
const { data, headers } = await axios.get(url);
|
|
36
|
+
if (!data.length) {
|
|
37
|
+
throw new Error(`No record of ${resource} avaliable`);
|
|
38
|
+
}
|
|
39
|
+
this.debug(`Fetched ${resource} sample, total = ${asNumber(headers['x-wp-total'])}`);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
data: data[0],
|
|
43
|
+
headers,
|
|
44
|
+
terms: this.extractTerms(data[0]),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async findTaxonomyByResourceName(name, options) {
|
|
49
|
+
const taxonomies = await this.taxonomies(options);
|
|
50
|
+
for (const taxonomy of taxonomies) {
|
|
51
|
+
if (taxonomy.rest_base === name) {
|
|
52
|
+
return taxonomy;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async findAssociatedTaxonomies(type, options) {
|
|
59
|
+
// TODO: try using terms()
|
|
60
|
+
type = type === 'posts' ? 'post' : type;
|
|
61
|
+
return (await this.taxonomies(options)).filter(taxonomy => taxonomy.types.includes(type));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async taxonomies({ noCache = false } = {}) {
|
|
65
|
+
if (noCache || !this._taxonomies) {
|
|
66
|
+
// don't await, save the promise
|
|
67
|
+
this._taxonomies = this._fetchTaxonomies();
|
|
68
|
+
}
|
|
69
|
+
return this._taxonomies;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async _fetchTaxonomies() {
|
|
73
|
+
const url = await this.url.build('taxonomies');
|
|
74
|
+
const { data } = await axios.get(url);
|
|
75
|
+
this.debug(`Fetched taxonomies.`);
|
|
76
|
+
return Object.values(data);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
extractTerms(data) {
|
|
80
|
+
return data._links['wp:term'] || [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async count(resource, { offset: _, ...options } = {}) {
|
|
84
|
+
const url = await this.url.build(resource, { ...options, page: 0, pageSize: 1 });
|
|
85
|
+
const { headers } = await axios.get(url);
|
|
86
|
+
return asNumber(headers['x-wp-total']);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async terms(resource, { noCache = false } = {}) {
|
|
90
|
+
return (await this.sample(resource, { noCache })).terms;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async countUrl(url) {
|
|
94
|
+
url = await this.url.append(url, { page: 0, pageSize: 1 });
|
|
95
|
+
const { headers } = await axios.get(url);
|
|
96
|
+
return asNumber(headers['x-wp-total']);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Return the UTC offset in milliseconds for this site.
|
|
101
|
+
*/
|
|
102
|
+
async utcOffsetInMs() {
|
|
103
|
+
const profile = this._client._profile;
|
|
104
|
+
if (profile.utcOffset === undefined) {
|
|
105
|
+
// we may call this multiple times but it's ok
|
|
106
|
+
const { data: post } = await this.sample('posts');
|
|
107
|
+
profile.utcOffset = (Date.parse(`${post.date}Z`) - Date.parse(`${post.date_gmt}Z`)) / MS_PER_HOUR;
|
|
108
|
+
this.debug(`Got utcOffset: ${profile.utcOffset}`);
|
|
109
|
+
}
|
|
110
|
+
return profile.utcOffset * MS_PER_HOUR;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
removeLinks({ _links: _, ...record }) {
|
|
114
|
+
return record;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
debug(v) {
|
|
118
|
+
const elapsed = (Date.now() - this._start) / 1000;
|
|
119
|
+
this._client._options.debug && console.error(`[${elapsed}]`, v);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
class Url {
|
|
125
|
+
|
|
126
|
+
constructor(helpers) {
|
|
127
|
+
this._helpers = helpers;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async build(resource, options) {
|
|
131
|
+
return this.append(`https://${this._helpers._client.site}/wp-json/wp/v2/${resource}`, options);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// modifiedAfter, modifiedBefore is supported since WordPress 5.7
|
|
135
|
+
// https://make.wordpress.org/core/2021/02/23/rest-api-changes-in-wordpress-5-7/
|
|
136
|
+
async append(url, options = {}) {
|
|
137
|
+
const { after, before, order, orderBy, page, pageSize, offset, include, exclude } = options;
|
|
138
|
+
const params = [];
|
|
139
|
+
|
|
140
|
+
// TODO: support single id
|
|
141
|
+
|
|
142
|
+
// The date is compared against site's local time, not UTC, so we have to work on timezone offset
|
|
143
|
+
if (has(after) || has(before)) {
|
|
144
|
+
const utcOffset = await this._helpers.utcOffsetInMs();
|
|
145
|
+
has(after) && params.push(`after=${toISOString(after, utcOffset)}`);
|
|
146
|
+
has(before) && params.push(`before=${toISOString(before, utcOffset)}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
has(order) && params.push(`order=${order}`);
|
|
150
|
+
has(orderBy) && params.push(`orderby=${orderBy}`);
|
|
151
|
+
has(page) && params.push(`page=${page + 1}`); // 0-based to 1-based
|
|
152
|
+
has(pageSize) && params.push(`per_page=${pageSize}`);
|
|
153
|
+
has(offset) && params.push(`offset=${offset}`);
|
|
154
|
+
has(include) && include.length && params.push(`include=${joinIds(include)}`);
|
|
155
|
+
has(exclude) && exclude.length && params.push(`exclude=${joinIds(exclude)}`);
|
|
156
|
+
|
|
157
|
+
const head = params.length === 0 ? '' : url.indexOf('?') < 0 ? '?' : '&';
|
|
158
|
+
return `${url}${head}${params.join('&')}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function joinIds(ids) {
|
|
164
|
+
return ids.map(id => encodeURIComponent(`${id}`)).join(',');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function has(value) {
|
|
168
|
+
return value !== undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toISOString(ts, utcOffset) {
|
|
172
|
+
return new Date(ts + utcOffset).toISOString();
|
|
173
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as WordPressClient } from './client.js';
|
package/src/media.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Entities from './entities/index.js';
|
|
2
|
+
import EntityIndex from './entities/entity-index.js';
|
|
3
|
+
|
|
4
|
+
const RESOURCE_NAME = 'media';
|
|
5
|
+
|
|
6
|
+
export default class Media extends Entities {
|
|
7
|
+
|
|
8
|
+
constructor(client) {
|
|
9
|
+
super(client, RESOURCE_NAME);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
_createIndex() {
|
|
13
|
+
return new EntityIndex(this, {
|
|
14
|
+
process,
|
|
15
|
+
value: en => en.source_url,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* We don't keep track of everything, for media entity is generally too large.
|
|
23
|
+
*/
|
|
24
|
+
function process({ id, date, date_gmt, alt_text, media_details: { width, height, filesize }, source_url }) {
|
|
25
|
+
return { id, date, date_gmt, alt_text, width, height, filesize, source_url };
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Entities from '../entities/index.js';
|
|
2
|
+
|
|
3
|
+
const RESOURCE_NAME = 'posts';
|
|
4
|
+
|
|
5
|
+
// TODO: classify post-like entities
|
|
6
|
+
|
|
7
|
+
export default class Posts extends Entities {
|
|
8
|
+
|
|
9
|
+
constructor(client) {
|
|
10
|
+
super(client, RESOURCE_NAME);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getAll() {
|
|
14
|
+
throw new Error(`Getting all posts is not supported.`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async index() {
|
|
18
|
+
throw new Error(`Indexing posts is not supported.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import axios from '../axios.js';
|
|
2
|
+
|
|
3
|
+
export default class WordPressDataSource {
|
|
4
|
+
|
|
5
|
+
constructor(helpers, resource, options = {}) {
|
|
6
|
+
this._helpers = helpers;
|
|
7
|
+
this._resource = resource;
|
|
8
|
+
this._options = options;
|
|
9
|
+
this._debug(`[WordPressDataSource] constructed for resource: ${resource}.`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
request() {
|
|
13
|
+
throw new Error(`Unimplemented`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async url(request) {
|
|
17
|
+
const baseUrl = await this.baseUrl();
|
|
18
|
+
return this._url(baseUrl, request);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async baseUrl() {
|
|
22
|
+
return this._baseUrl || (this._baseUrl = this._buildBaseUrl());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async total() {
|
|
26
|
+
throw new Error(`Unimplemented`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async get(request) {
|
|
30
|
+
this._debug(`[WordPressDataSource] get ${JSON.stringify(request)}`);
|
|
31
|
+
const url = await this.url(request);
|
|
32
|
+
this._debug(`[WordPressDataSource] request ${url}`);
|
|
33
|
+
const response = await this._axiosGet(url);
|
|
34
|
+
this._debug(`[WordPressDataSource] response ${response.status} ${url}`);
|
|
35
|
+
return this._process(response);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_process({ status, data }) {
|
|
39
|
+
if (status >= 400 && status < 500 && data.code === 'rest_post_invalid_page_number') {
|
|
40
|
+
// out of bound, so there is no more data
|
|
41
|
+
return { data: [], terminate: true };
|
|
42
|
+
}
|
|
43
|
+
if (!this._options.preserveLinks) {
|
|
44
|
+
data = data.map(this._helpers.removeLinks);
|
|
45
|
+
}
|
|
46
|
+
return { data };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async _url(baseUrl, request) {
|
|
50
|
+
throw new Error(`Unimplemented`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async _buildBaseUrl() {
|
|
54
|
+
// exclude parameters meant to be dealt with state
|
|
55
|
+
const { page, ...options } = this._options;
|
|
56
|
+
return this._helpers.url.build(this._resource, options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async _axiosGet(url) {
|
|
60
|
+
try {
|
|
61
|
+
return await axios.get(url);
|
|
62
|
+
} catch(error) {
|
|
63
|
+
if (error.response) {
|
|
64
|
+
return error.response;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_debug(...args) {
|
|
71
|
+
this._helpers.debug(...args);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import WordPressDataSource from './base.js';
|
|
2
|
+
|
|
3
|
+
const PAGE_SIZE = 100;
|
|
4
|
+
|
|
5
|
+
export default class IdsWordPressDataSource extends WordPressDataSource {
|
|
6
|
+
|
|
7
|
+
constructor(helpers, resource, { ids, page: _, limit, ...options } = {}) {
|
|
8
|
+
super(helpers, resource, options);
|
|
9
|
+
if (!Array.isArray(ids)) {
|
|
10
|
+
throw new Error(`ids must be an array: ${ids}`);
|
|
11
|
+
}
|
|
12
|
+
this._pageSize = options.pageSize = PAGE_SIZE;
|
|
13
|
+
this._ids = ids;
|
|
14
|
+
this._page = 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
request() {
|
|
18
|
+
const pageSize = this._pageSize;
|
|
19
|
+
const page = this._page++;
|
|
20
|
+
const start = pageSize * page;
|
|
21
|
+
const end = start + pageSize;
|
|
22
|
+
const ids = this._ids.slice(start, end);
|
|
23
|
+
const records = ids.length;
|
|
24
|
+
const exhaust = end >= this._ids.length;
|
|
25
|
+
return exhaust ? { ids, records, exhaust } : { ids, records };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async _url(baseUrl, { ids }) {
|
|
29
|
+
const head = baseUrl.indexOf('?') < 0 ? '?' : '&';
|
|
30
|
+
return `${baseUrl}${head}include=${joinIds(ids)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function joinIds(ids) {
|
|
36
|
+
return ids.map(id => encodeURIComponent(`${id}`)).join(',');
|
|
37
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import PagedWordPressDataSource from './paged.js';
|
|
2
|
+
import IdsWordPressDataSource from './ids.js';
|
|
3
|
+
|
|
4
|
+
export default function source(helpers, resource, options) {
|
|
5
|
+
const DataSource = options.ids ? IdsWordPressDataSource : PagedWordPressDataSource;
|
|
6
|
+
return new DataSource(helpers, resource, options);
|
|
7
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { asNumber } from '@miso.ai/server-commons';
|
|
2
|
+
import WordPressDataSource from './base.js';
|
|
3
|
+
|
|
4
|
+
const MAX_PAGE_SIZE = 100;
|
|
5
|
+
|
|
6
|
+
export default class PagedWordPressDataSource extends WordPressDataSource {
|
|
7
|
+
|
|
8
|
+
constructor(helpers, resource, { page: _, limit, ...options } = {}) {
|
|
9
|
+
super(helpers, resource, options);
|
|
10
|
+
const { pageSize = MAX_PAGE_SIZE } = options;
|
|
11
|
+
if (pageSize <= 0) {
|
|
12
|
+
throw new Error(`Page size must be positive: ${pageSize}`);
|
|
13
|
+
}
|
|
14
|
+
if (pageSize > MAX_PAGE_SIZE) {
|
|
15
|
+
throw new Error(`Page size cannot be greater than ${MAX_PAGE_SIZE}: ${pageSize}`);
|
|
16
|
+
}
|
|
17
|
+
// TODO: limit
|
|
18
|
+
this._pageSize = options.pageSize = pageSize;
|
|
19
|
+
this._page = 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async init() {
|
|
23
|
+
await this.total();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
request() {
|
|
27
|
+
const page = this._page++;
|
|
28
|
+
const records = this._pageSize;
|
|
29
|
+
const total = this._totalValue;
|
|
30
|
+
// if we know total, we know when the data is exhausted
|
|
31
|
+
const exhaust = total !== undefined && ((page + 1) * this._pageSize > total + 10); // 10 for a buffer
|
|
32
|
+
return exhaust ? { records, page, exhaust } : { records, page };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async total() {
|
|
36
|
+
return this._totalPromise || (this._totalPromise = this._fetchTotal());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async _url(baseUrl, { page }) {
|
|
40
|
+
const head = baseUrl.indexOf('?') < 0 ? '?' : '&';
|
|
41
|
+
return `${baseUrl}${head}page=${page + 1}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async _fetchTotal() {
|
|
45
|
+
const baseUrl = await this.baseUrl();
|
|
46
|
+
this._debug(`[PagedWordPressDataSource] fetch total for ${baseUrl}`);
|
|
47
|
+
const total = await this._helpers.countUrl(baseUrl);
|
|
48
|
+
this._debug(`[PagedWordPressDataSource] fetch total for ${baseUrl} = ${total}`);
|
|
49
|
+
this._totalValue = total;
|
|
50
|
+
return total;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_process({ status, data, headers }) {
|
|
54
|
+
const result = super._process({ status, data, headers });
|
|
55
|
+
const total = asNumber(headers['x-wp-total']);
|
|
56
|
+
if (total !== undefined) {
|
|
57
|
+
result.total = total;
|
|
58
|
+
}
|
|
59
|
+
if (data.length < this._pageSize) {
|
|
60
|
+
result.terminate = true;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
package/src/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default '0.6.0-beta.1';
|