@dbp-wp/core 0.1.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/LICENSE +201 -0
- package/README.md +30 -0
- package/dist/index.d.ts +252 -0
- package/dist/index.js +454 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @dbp-wp/core
|
|
2
|
+
|
|
3
|
+
Core library for [DBP WP](https://github.com/takashi-matsuyama/dbp_wp): a WordPress REST
|
|
4
|
+
client, a safe formula engine, a CSV/JSON importer, and typesetting data generation.
|
|
5
|
+
|
|
6
|
+
This package is the Node layer of DBP WP. Most people will want the `dbp-wp` CLI, which
|
|
7
|
+
runs the full app via `npx dbp-wp`. This library is published separately so it can also be
|
|
8
|
+
used on its own and by the browser demo.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @dbp-wp/core
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { WpClient } from '@dbp-wp/core';
|
|
16
|
+
|
|
17
|
+
const client = new WpClient({
|
|
18
|
+
siteUrl: 'https://example.com',
|
|
19
|
+
username: 'editor',
|
|
20
|
+
applicationPassword: 'xxxx xxxx xxxx xxxx xxxx xxxx',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const posts = await client.listPosts({ perPage: 20 });
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Node.js >= 20.
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection credentials for a WordPress site.
|
|
3
|
+
*
|
|
4
|
+
* DBP WP authenticates with WordPress 5.6+ Application Passwords, which are sent as the
|
|
5
|
+
* password of an HTTP Basic `Authorization` header.
|
|
6
|
+
*/
|
|
7
|
+
interface WpCredentials {
|
|
8
|
+
/** Base URL of the WordPress site, e.g. `https://example.com`. */
|
|
9
|
+
siteUrl: string;
|
|
10
|
+
/** WordPress username. */
|
|
11
|
+
username: string;
|
|
12
|
+
/** Application Password issued by WordPress (used as the Basic-auth password). */
|
|
13
|
+
applicationPassword: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Raw post shape as returned by the WordPress REST API (`/wp/v2/<type>`).
|
|
17
|
+
*
|
|
18
|
+
* `title` is an object; `raw` is only present in `context=edit`. `menu_order` is
|
|
19
|
+
* snake_case. Use {@link WpPost} for the normalized internal model.
|
|
20
|
+
*/
|
|
21
|
+
interface WpPostResponse {
|
|
22
|
+
id: number;
|
|
23
|
+
type: string;
|
|
24
|
+
status: string;
|
|
25
|
+
title: {
|
|
26
|
+
rendered: string;
|
|
27
|
+
raw?: string;
|
|
28
|
+
};
|
|
29
|
+
menu_order: number;
|
|
30
|
+
meta: Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Arbitrary post meta exposed by the companion plugin's `dbp_wp_meta` field.
|
|
33
|
+
* Present only when the connector is active; absent in restricted mode.
|
|
34
|
+
*/
|
|
35
|
+
dbp_wp_meta?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
/** Normalized, internal post model used by DBP WP. */
|
|
38
|
+
interface WpPost {
|
|
39
|
+
id: number;
|
|
40
|
+
type: string;
|
|
41
|
+
status: string;
|
|
42
|
+
/** Editable title (the `raw` value when available, else `rendered`). */
|
|
43
|
+
title: string;
|
|
44
|
+
menuOrder: number;
|
|
45
|
+
/**
|
|
46
|
+
* Core REST post meta (only keys registered with `show_in_rest`). For arbitrary
|
|
47
|
+
* meta exposed by the companion plugin, see {@link WpPost.dbpWpMeta}.
|
|
48
|
+
*/
|
|
49
|
+
meta: Record<string, unknown>;
|
|
50
|
+
/**
|
|
51
|
+
* Arbitrary post meta from the companion plugin (all keys, single value each).
|
|
52
|
+
* Present only when the connector returned `dbp_wp_meta`; `undefined` in restricted
|
|
53
|
+
* mode (no connector installed).
|
|
54
|
+
*/
|
|
55
|
+
dbpWpMeta?: Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Editable standard post fields. These map to core WordPress REST fields and need no
|
|
59
|
+
* companion plugin. Arbitrary meta editing is handled separately (companion plugin).
|
|
60
|
+
*/
|
|
61
|
+
interface UpdatePostFields {
|
|
62
|
+
/** Editable title (sent as the post `title`). */
|
|
63
|
+
title?: string;
|
|
64
|
+
/** Ordering value (sent as `menu_order`). */
|
|
65
|
+
menuOrder?: number;
|
|
66
|
+
/** Post status (e.g. `publish`, `draft`). */
|
|
67
|
+
status?: string;
|
|
68
|
+
}
|
|
69
|
+
/** A WordPress post type available for listing/editing over REST. */
|
|
70
|
+
interface WpPostType {
|
|
71
|
+
/** Internal type slug (e.g. `post`). */
|
|
72
|
+
slug: string;
|
|
73
|
+
/** REST route base used to list/update items of this type (e.g. `posts`). */
|
|
74
|
+
restBase: string;
|
|
75
|
+
/** Human-readable name (e.g. `Posts`). */
|
|
76
|
+
name: string;
|
|
77
|
+
}
|
|
78
|
+
/** Result of a per-post meta delete via the companion plugin. */
|
|
79
|
+
interface DeleteMetaResult {
|
|
80
|
+
/** The post the keys were deleted from. */
|
|
81
|
+
postId: number;
|
|
82
|
+
/** Keys actually deleted (a key not present on the post is omitted). */
|
|
83
|
+
deleted: string[];
|
|
84
|
+
}
|
|
85
|
+
/** Parameters for listing posts. */
|
|
86
|
+
interface ListPostsParams {
|
|
87
|
+
/** REST post type slug (e.g. `posts`, `pages`). Defaults to `posts`. */
|
|
88
|
+
type?: string;
|
|
89
|
+
/** Page size (WordPress caps this at 100). Defaults to 100. */
|
|
90
|
+
perPage?: number;
|
|
91
|
+
/** 1-based page number. Defaults to 1. */
|
|
92
|
+
page?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validate and normalize a WordPress site URL into a REST base (origin + base path).
|
|
97
|
+
*
|
|
98
|
+
* Requires https, except plain http is allowed for local development hosts. Rejects
|
|
99
|
+
* embedded credentials, query strings, and fragments, and strips trailing slashes — so
|
|
100
|
+
* an Application Password is never sent over cleartext to an unexpected target.
|
|
101
|
+
*/
|
|
102
|
+
declare function normalizeSiteUrl(siteUrl: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Build the HTTP Basic `Authorization` header value from Application Password
|
|
105
|
+
* credentials. WordPress treats the Application Password as the Basic-auth password.
|
|
106
|
+
*/
|
|
107
|
+
declare function buildAuthHeader(credentials: WpCredentials): string;
|
|
108
|
+
/** Error thrown when the WordPress REST API returns a non-2xx response. */
|
|
109
|
+
declare class WpRequestError extends Error {
|
|
110
|
+
readonly status: number;
|
|
111
|
+
readonly path: string;
|
|
112
|
+
constructor(status: number, path: string, message: string);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Minimal WordPress REST client.
|
|
116
|
+
*
|
|
117
|
+
* Runs in the Node process (CLI shell), never in the browser, so Application Password
|
|
118
|
+
* credentials stay server-side. MVP scope: list posts, read/write post meta.
|
|
119
|
+
*/
|
|
120
|
+
declare class WpClient {
|
|
121
|
+
private readonly credentials;
|
|
122
|
+
private readonly restBase;
|
|
123
|
+
constructor(credentials: WpCredentials);
|
|
124
|
+
private request;
|
|
125
|
+
/**
|
|
126
|
+
* List the REST-enabled post types on the site (edit context), so the app can offer
|
|
127
|
+
* a type selector. Returns each type's REST route base and display name.
|
|
128
|
+
*/
|
|
129
|
+
listPostTypes(): Promise<WpPostType[]>;
|
|
130
|
+
/** List posts of a given type in edit context (raw fields, for editing). */
|
|
131
|
+
listPosts(params?: ListPostsParams): Promise<WpPost[]>;
|
|
132
|
+
/**
|
|
133
|
+
* Update post fields in a single request. Standard fields (title, menu_order,
|
|
134
|
+
* status) are core REST fields and need no plugin. When `meta` is supplied it rides
|
|
135
|
+
* the same request through the companion plugin's `dbp_wp_meta` field (ignored by
|
|
136
|
+
* WordPress without the connector). Pass the REST route slug as `type` (e.g.
|
|
137
|
+
* `posts`, `pages`) — not the object type returned on a post.
|
|
138
|
+
*/
|
|
139
|
+
updatePost(id: number, fields: UpdatePostFields, type?: string, meta?: Record<string, unknown>): Promise<WpPost>;
|
|
140
|
+
/**
|
|
141
|
+
* Create a new post in a single request, symmetric to {@link WpClient.updatePost}.
|
|
142
|
+
* Standard fields (title, menu_order, status) are core REST fields; when `meta` is
|
|
143
|
+
* supplied it rides the same request through the companion plugin's `dbp_wp_meta`
|
|
144
|
+
* field. Pass the REST route slug as `type` (e.g. `posts`, `pages`).
|
|
145
|
+
*/
|
|
146
|
+
createPost(fields: UpdatePostFields, type?: string, meta?: Record<string, unknown>): Promise<WpPost>;
|
|
147
|
+
/**
|
|
148
|
+
* Update only arbitrary post meta through the companion plugin's `dbp_wp_meta`
|
|
149
|
+
* field. A thin wrapper over {@link WpClient.updatePost} with no standard fields.
|
|
150
|
+
* Requires the connector; the connector writes scalar values only.
|
|
151
|
+
*/
|
|
152
|
+
updatePostMeta(id: number, meta: Record<string, unknown>, type?: string): Promise<WpPost>;
|
|
153
|
+
/**
|
|
154
|
+
* Delete named meta keys from a single post via the companion plugin's
|
|
155
|
+
* `DELETE /dbp-wp/v1/posts/<id>/meta` route. This route is keyed by id only (the
|
|
156
|
+
* post type is irrelevant). Requires the connector.
|
|
157
|
+
*/
|
|
158
|
+
deletePostMeta(id: number, keys: string[]): Promise<DeleteMetaResult>;
|
|
159
|
+
/**
|
|
160
|
+
* Detect whether the companion plugin is active by checking the REST index
|
|
161
|
+
* (`/wp-json/`) for the connector's namespace. Throws on a failed request; a caller
|
|
162
|
+
* that wants a non-fatal probe should treat a thrown error as "not available".
|
|
163
|
+
*/
|
|
164
|
+
detectConnector(): Promise<boolean>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Formula engine: evaluates a spreadsheet expression against named numeric cells.
|
|
169
|
+
*
|
|
170
|
+
* Implementations MUST NOT use `eval`, `Function`, or any other dynamic code execution.
|
|
171
|
+
*/
|
|
172
|
+
interface FormulaEngine {
|
|
173
|
+
/**
|
|
174
|
+
* Evaluate a single expression against a map of cell references to numbers.
|
|
175
|
+
* Throws on invalid syntax, unknown variables, or a non-numeric result.
|
|
176
|
+
*/
|
|
177
|
+
evaluate(expression: string, context: Record<string, number>): number;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Formula engine backed by expr-eval-fork, which parses to an AST and evaluates without
|
|
181
|
+
* `eval`/`Function`. Member access, assignment, and function definitions are disabled,
|
|
182
|
+
* the nondeterministic `random()` function is removed, and results are constrained to
|
|
183
|
+
* finite numbers, so expressions stay pure, deterministic, and side-effect free.
|
|
184
|
+
*/
|
|
185
|
+
declare class SafeFormulaEngine implements FormulaEngine {
|
|
186
|
+
private readonly parser;
|
|
187
|
+
constructor();
|
|
188
|
+
evaluate(expression: string, context: Record<string, number>): number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A tabular view of an import file: a header row plus data rows. Both CSV and JSON
|
|
193
|
+
* sources are normalized to this shape so the column-mapping logic is source-agnostic.
|
|
194
|
+
* Each data row is aligned to `headers` by index; a short row has missing trailing cells.
|
|
195
|
+
*/
|
|
196
|
+
interface ParsedTable {
|
|
197
|
+
/** Column headers (CSV first row, or the union of JSON object keys). */
|
|
198
|
+
headers: string[];
|
|
199
|
+
/** Data rows; `rows[r][c]` is the cell under `headers[c]`. */
|
|
200
|
+
rows: string[][];
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Where a file column is imported to. `skip` drops the column; `title`/`status`/
|
|
204
|
+
* `menuOrder` map to standard post fields; `meta` writes an arbitrary post-meta key
|
|
205
|
+
* (companion plugin required).
|
|
206
|
+
*/
|
|
207
|
+
type ImportTarget = {
|
|
208
|
+
kind: 'skip';
|
|
209
|
+
} | {
|
|
210
|
+
kind: 'title';
|
|
211
|
+
} | {
|
|
212
|
+
kind: 'status';
|
|
213
|
+
} | {
|
|
214
|
+
kind: 'menuOrder';
|
|
215
|
+
} | {
|
|
216
|
+
kind: 'meta';
|
|
217
|
+
key: string;
|
|
218
|
+
};
|
|
219
|
+
/** A single new post to create, derived from one import row. */
|
|
220
|
+
interface ImportCreate {
|
|
221
|
+
/** Standard fields (title / menuOrder / status). */
|
|
222
|
+
fields: UpdatePostFields;
|
|
223
|
+
/** Arbitrary meta to write via the companion plugin (omitted when none). */
|
|
224
|
+
meta?: Record<string, unknown>;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Parse CSV text into a table, taking the first record as headers. Implements the
|
|
228
|
+
* RFC 4180 essentials: double-quoted fields, embedded commas/newlines, `""` escapes,
|
|
229
|
+
* and CRLF or LF line endings. A trailing newline does not produce an empty record.
|
|
230
|
+
*/
|
|
231
|
+
declare function parseCsv(text: string): ParsedTable;
|
|
232
|
+
/**
|
|
233
|
+
* Parse JSON text (an array of objects) into a table. Headers are the union of all
|
|
234
|
+
* object keys in first-seen order. Object/array cell values are JSON-stringified;
|
|
235
|
+
* null and undefined become an empty string. Throws if the JSON is not an array.
|
|
236
|
+
*/
|
|
237
|
+
declare function parseJsonRecords(text: string): ParsedTable;
|
|
238
|
+
/**
|
|
239
|
+
* Normalize a status cell to a WordPress status. Known labels/values (case-insensitive,
|
|
240
|
+
* e.g. `Published` → `publish`) are mapped; anything else passes through trimmed so the
|
|
241
|
+
* WordPress REST API can validate it and surface a per-row error if invalid.
|
|
242
|
+
*/
|
|
243
|
+
declare function normalizeStatus(value: string): string;
|
|
244
|
+
/**
|
|
245
|
+
* Apply a column mapping to a parsed table, producing one {@link ImportCreate} per row.
|
|
246
|
+
* Empty cells contribute nothing; a row that maps to no fields and no meta is skipped.
|
|
247
|
+
* Non-integer `menuOrder` cells are ignored. Meta is stored on a null-prototype object
|
|
248
|
+
* so a header named `__proto__` is kept as data, never touching any prototype.
|
|
249
|
+
*/
|
|
250
|
+
declare function buildImportPlan(table: ParsedTable, mapping: ImportTarget[]): ImportCreate[];
|
|
251
|
+
|
|
252
|
+
export { type DeleteMetaResult, type FormulaEngine, type ImportCreate, type ImportTarget, type ListPostsParams, type ParsedTable, SafeFormulaEngine, type UpdatePostFields, WpClient, type WpCredentials, type WpPost, type WpPostResponse, type WpPostType, WpRequestError, buildAuthHeader, buildImportPlan, normalizeSiteUrl, normalizeStatus, parseCsv, parseJsonRecords };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// src/wp-client.ts
|
|
2
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
3
|
+
var ROUTE_SEGMENT = /^[a-z0-9_-]+$/i;
|
|
4
|
+
var META_FIELD = "dbp_wp_meta";
|
|
5
|
+
var CONNECTOR_NAMESPACE = "dbp-wp/v1";
|
|
6
|
+
function normalizeSiteUrl(siteUrl) {
|
|
7
|
+
let url;
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(siteUrl);
|
|
10
|
+
} catch {
|
|
11
|
+
throw new Error(`Invalid site URL: ${siteUrl}`);
|
|
12
|
+
}
|
|
13
|
+
const isLocal = LOCAL_HOSTS.has(url.hostname);
|
|
14
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocal)) {
|
|
15
|
+
throw new Error(`Site URL must use https (http is allowed only for local hosts): ${siteUrl}`);
|
|
16
|
+
}
|
|
17
|
+
if (url.username !== "" || url.password !== "") {
|
|
18
|
+
throw new Error("Site URL must not contain embedded credentials.");
|
|
19
|
+
}
|
|
20
|
+
if (url.search !== "" || url.hash !== "") {
|
|
21
|
+
throw new Error("Site URL must not contain a query string or fragment.");
|
|
22
|
+
}
|
|
23
|
+
return `${url.origin}${url.pathname}`.replace(/\/+$/, "");
|
|
24
|
+
}
|
|
25
|
+
function buildAuthHeader(credentials) {
|
|
26
|
+
if (credentials.username.includes(":")) {
|
|
27
|
+
throw new Error('Username must not contain a colon (":") for HTTP Basic authentication.');
|
|
28
|
+
}
|
|
29
|
+
const token = `${credentials.username}:${credentials.applicationPassword}`;
|
|
30
|
+
const base64 = Buffer.from(token, "utf-8").toString("base64");
|
|
31
|
+
return `Basic ${base64}`;
|
|
32
|
+
}
|
|
33
|
+
var WpRequestError = class extends Error {
|
|
34
|
+
constructor(status, path, message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.status = status;
|
|
37
|
+
this.path = path;
|
|
38
|
+
this.name = "WpRequestError";
|
|
39
|
+
}
|
|
40
|
+
status;
|
|
41
|
+
path;
|
|
42
|
+
};
|
|
43
|
+
var WpClient = class {
|
|
44
|
+
constructor(credentials) {
|
|
45
|
+
this.credentials = credentials;
|
|
46
|
+
this.restBase = normalizeSiteUrl(credentials.siteUrl);
|
|
47
|
+
}
|
|
48
|
+
credentials;
|
|
49
|
+
restBase;
|
|
50
|
+
async request(path, init = {}) {
|
|
51
|
+
const response = await fetch(`${this.restBase}/wp-json${path}`, {
|
|
52
|
+
...init,
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: buildAuthHeader(this.credentials),
|
|
55
|
+
// Only declare a JSON body when one is actually sent (GETs carry none).
|
|
56
|
+
...init.body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
57
|
+
...init.headers
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new WpRequestError(
|
|
62
|
+
response.status,
|
|
63
|
+
path,
|
|
64
|
+
`WordPress REST request failed: ${response.status} ${response.statusText}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return await response.json();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* List the REST-enabled post types on the site (edit context), so the app can offer
|
|
71
|
+
* a type selector. Returns each type's REST route base and display name.
|
|
72
|
+
*/
|
|
73
|
+
async listPostTypes() {
|
|
74
|
+
const raw = await this.request("/wp/v2/types?context=edit");
|
|
75
|
+
return normalizePostTypes(raw);
|
|
76
|
+
}
|
|
77
|
+
/** List posts of a given type in edit context (raw fields, for editing). */
|
|
78
|
+
async listPosts(params = {}) {
|
|
79
|
+
const type = params.type ?? "posts";
|
|
80
|
+
assertRouteSegment(type);
|
|
81
|
+
const perPage = clampInt(params.perPage ?? 100, 1, 100);
|
|
82
|
+
const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);
|
|
83
|
+
const query = new URLSearchParams({
|
|
84
|
+
context: "edit",
|
|
85
|
+
per_page: String(perPage),
|
|
86
|
+
page: String(page)
|
|
87
|
+
});
|
|
88
|
+
const raw = await this.request(`/wp/v2/${type}?${query.toString()}`);
|
|
89
|
+
return raw.map(normalizePost);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Update post fields in a single request. Standard fields (title, menu_order,
|
|
93
|
+
* status) are core REST fields and need no plugin. When `meta` is supplied it rides
|
|
94
|
+
* the same request through the companion plugin's `dbp_wp_meta` field (ignored by
|
|
95
|
+
* WordPress without the connector). Pass the REST route slug as `type` (e.g.
|
|
96
|
+
* `posts`, `pages`) — not the object type returned on a post.
|
|
97
|
+
*/
|
|
98
|
+
async updatePost(id, fields, type = "posts", meta) {
|
|
99
|
+
assertPostId(id);
|
|
100
|
+
assertRouteSegment(type);
|
|
101
|
+
const raw = await this.request(`/wp/v2/${type}/${String(id)}?context=edit`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
body: JSON.stringify(buildPostBody(fields, meta))
|
|
104
|
+
});
|
|
105
|
+
return normalizePost(raw);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Create a new post in a single request, symmetric to {@link WpClient.updatePost}.
|
|
109
|
+
* Standard fields (title, menu_order, status) are core REST fields; when `meta` is
|
|
110
|
+
* supplied it rides the same request through the companion plugin's `dbp_wp_meta`
|
|
111
|
+
* field. Pass the REST route slug as `type` (e.g. `posts`, `pages`).
|
|
112
|
+
*/
|
|
113
|
+
async createPost(fields, type = "posts", meta) {
|
|
114
|
+
assertRouteSegment(type);
|
|
115
|
+
const raw = await this.request(`/wp/v2/${type}?context=edit`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
body: JSON.stringify(buildPostBody(fields, meta))
|
|
118
|
+
});
|
|
119
|
+
return normalizePost(raw);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Update only arbitrary post meta through the companion plugin's `dbp_wp_meta`
|
|
123
|
+
* field. A thin wrapper over {@link WpClient.updatePost} with no standard fields.
|
|
124
|
+
* Requires the connector; the connector writes scalar values only.
|
|
125
|
+
*/
|
|
126
|
+
async updatePostMeta(id, meta, type = "posts") {
|
|
127
|
+
return this.updatePost(id, {}, type, meta);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Delete named meta keys from a single post via the companion plugin's
|
|
131
|
+
* `DELETE /dbp-wp/v1/posts/<id>/meta` route. This route is keyed by id only (the
|
|
132
|
+
* post type is irrelevant). Requires the connector.
|
|
133
|
+
*/
|
|
134
|
+
async deletePostMeta(id, keys) {
|
|
135
|
+
assertPostId(id);
|
|
136
|
+
const cleanKeys = sanitizeMetaKeys(keys);
|
|
137
|
+
const raw = await this.request(
|
|
138
|
+
`/${CONNECTOR_NAMESPACE}/posts/${String(id)}/meta`,
|
|
139
|
+
{ method: "DELETE", body: JSON.stringify({ keys: cleanKeys }) }
|
|
140
|
+
);
|
|
141
|
+
return {
|
|
142
|
+
postId: typeof raw.post_id === "number" ? raw.post_id : id,
|
|
143
|
+
deleted: Array.isArray(raw.deleted) ? raw.deleted : []
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Detect whether the companion plugin is active by checking the REST index
|
|
148
|
+
* (`/wp-json/`) for the connector's namespace. Throws on a failed request; a caller
|
|
149
|
+
* that wants a non-fatal probe should treat a thrown error as "not available".
|
|
150
|
+
*/
|
|
151
|
+
async detectConnector() {
|
|
152
|
+
const index = await this.request("/");
|
|
153
|
+
return hasConnectorNamespace(index.namespaces);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function buildUpdateBody(fields) {
|
|
157
|
+
const body = {};
|
|
158
|
+
if (fields.title !== void 0) {
|
|
159
|
+
body.title = fields.title;
|
|
160
|
+
}
|
|
161
|
+
if (fields.menuOrder !== void 0) {
|
|
162
|
+
body.menu_order = fields.menuOrder;
|
|
163
|
+
}
|
|
164
|
+
if (fields.status !== void 0) {
|
|
165
|
+
body.status = fields.status;
|
|
166
|
+
}
|
|
167
|
+
return body;
|
|
168
|
+
}
|
|
169
|
+
function assertRouteSegment(segment) {
|
|
170
|
+
if (!ROUTE_SEGMENT.test(segment)) {
|
|
171
|
+
throw new Error(`Invalid REST route segment: ${segment}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function assertPostId(id) {
|
|
175
|
+
if (!Number.isSafeInteger(id) || id <= 0) {
|
|
176
|
+
throw new Error(`Invalid post id: ${id}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function clampInt(value, min, max) {
|
|
180
|
+
if (!Number.isFinite(value)) {
|
|
181
|
+
return min;
|
|
182
|
+
}
|
|
183
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
184
|
+
}
|
|
185
|
+
function buildMetaBody(meta) {
|
|
186
|
+
return { [META_FIELD]: meta };
|
|
187
|
+
}
|
|
188
|
+
function buildPostBody(fields, meta) {
|
|
189
|
+
const body = buildUpdateBody(fields);
|
|
190
|
+
if (meta !== void 0) {
|
|
191
|
+
Object.assign(body, buildMetaBody(meta));
|
|
192
|
+
}
|
|
193
|
+
return body;
|
|
194
|
+
}
|
|
195
|
+
function sanitizeMetaKeys(keys) {
|
|
196
|
+
if (!Array.isArray(keys)) {
|
|
197
|
+
throw new Error("Meta keys must be an array of strings.");
|
|
198
|
+
}
|
|
199
|
+
const clean = keys.filter((key) => typeof key === "string" && key.length > 0);
|
|
200
|
+
if (clean.length === 0) {
|
|
201
|
+
throw new Error("At least one non-empty meta key is required.");
|
|
202
|
+
}
|
|
203
|
+
return clean;
|
|
204
|
+
}
|
|
205
|
+
function hasConnectorNamespace(namespaces) {
|
|
206
|
+
return Array.isArray(namespaces) && namespaces.includes(CONNECTOR_NAMESPACE);
|
|
207
|
+
}
|
|
208
|
+
function normalizePostTypes(raw) {
|
|
209
|
+
if (typeof raw !== "object" || raw === null) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
const result = [];
|
|
213
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
214
|
+
if (typeof value !== "object" || value === null) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const entry = value;
|
|
218
|
+
if (typeof entry.rest_base !== "string" || !ROUTE_SEGMENT.test(entry.rest_base)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
result.push({
|
|
222
|
+
slug: typeof entry.slug === "string" ? entry.slug : key,
|
|
223
|
+
restBase: entry.rest_base,
|
|
224
|
+
name: typeof entry.name === "string" ? entry.name : key
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
function normalizePost(raw) {
|
|
230
|
+
const post = {
|
|
231
|
+
id: raw.id,
|
|
232
|
+
type: raw.type,
|
|
233
|
+
status: raw.status,
|
|
234
|
+
title: raw.title.raw ?? raw.title.rendered,
|
|
235
|
+
menuOrder: raw.menu_order,
|
|
236
|
+
meta: raw.meta
|
|
237
|
+
};
|
|
238
|
+
if (raw.dbp_wp_meta !== void 0) {
|
|
239
|
+
post.dbpWpMeta = raw.dbp_wp_meta;
|
|
240
|
+
}
|
|
241
|
+
return post;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/calc/index.ts
|
|
245
|
+
import { Parser } from "expr-eval-fork";
|
|
246
|
+
var SafeFormulaEngine = class {
|
|
247
|
+
parser;
|
|
248
|
+
constructor() {
|
|
249
|
+
this.parser = new Parser({
|
|
250
|
+
allowMemberAccess: false,
|
|
251
|
+
operators: { assignment: false, fndef: false }
|
|
252
|
+
});
|
|
253
|
+
delete this.parser.functions.random;
|
|
254
|
+
}
|
|
255
|
+
evaluate(expression, context) {
|
|
256
|
+
let parsed;
|
|
257
|
+
try {
|
|
258
|
+
parsed = this.parser.parse(expression);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
throw new Error(`Invalid formula: ${e instanceof Error ? e.message : "parse error"}`);
|
|
261
|
+
}
|
|
262
|
+
let result;
|
|
263
|
+
try {
|
|
264
|
+
result = parsed.evaluate(context);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
throw new Error(`Formula evaluation failed: ${e instanceof Error ? e.message : "error"}`);
|
|
267
|
+
}
|
|
268
|
+
if (typeof result !== "number" || !Number.isFinite(result)) {
|
|
269
|
+
throw new Error("Formula must evaluate to a finite number.");
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/importer.ts
|
|
276
|
+
var MENU_ORDER_MIN = -2147483648;
|
|
277
|
+
var MENU_ORDER_MAX = 2147483647;
|
|
278
|
+
function parseCsv(text) {
|
|
279
|
+
const records = parseCsvRecords(text);
|
|
280
|
+
const headers = records[0] ?? [];
|
|
281
|
+
return { headers, rows: records.slice(1) };
|
|
282
|
+
}
|
|
283
|
+
function parseCsvRecords(text) {
|
|
284
|
+
const records = [];
|
|
285
|
+
let record = [];
|
|
286
|
+
let field = "";
|
|
287
|
+
let inQuotes = false;
|
|
288
|
+
let i = 0;
|
|
289
|
+
const endField = () => {
|
|
290
|
+
record.push(field);
|
|
291
|
+
field = "";
|
|
292
|
+
};
|
|
293
|
+
const endRecord = () => {
|
|
294
|
+
endField();
|
|
295
|
+
records.push(record);
|
|
296
|
+
record = [];
|
|
297
|
+
};
|
|
298
|
+
while (i < text.length) {
|
|
299
|
+
const ch = text[i];
|
|
300
|
+
if (inQuotes) {
|
|
301
|
+
if (ch === '"') {
|
|
302
|
+
if (text[i + 1] === '"') {
|
|
303
|
+
field += '"';
|
|
304
|
+
i += 2;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
inQuotes = false;
|
|
308
|
+
i += 1;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
field += ch;
|
|
312
|
+
i += 1;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (ch === '"') {
|
|
316
|
+
inQuotes = true;
|
|
317
|
+
i += 1;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (ch === ",") {
|
|
321
|
+
endField();
|
|
322
|
+
i += 1;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (ch === "\r") {
|
|
326
|
+
endRecord();
|
|
327
|
+
i += text[i + 1] === "\n" ? 2 : 1;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (ch === "\n") {
|
|
331
|
+
endRecord();
|
|
332
|
+
i += 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
field += ch;
|
|
336
|
+
i += 1;
|
|
337
|
+
}
|
|
338
|
+
if (inQuotes) {
|
|
339
|
+
throw new Error("Malformed CSV: unterminated quoted field.");
|
|
340
|
+
}
|
|
341
|
+
if (field !== "" || record.length > 0) {
|
|
342
|
+
endRecord();
|
|
343
|
+
}
|
|
344
|
+
return records;
|
|
345
|
+
}
|
|
346
|
+
function parseJsonRecords(text) {
|
|
347
|
+
const data = JSON.parse(text);
|
|
348
|
+
if (!Array.isArray(data)) {
|
|
349
|
+
throw new Error("JSON import must be an array of objects.");
|
|
350
|
+
}
|
|
351
|
+
const headers = [];
|
|
352
|
+
const seen = /* @__PURE__ */ new Set();
|
|
353
|
+
for (const entry of data) {
|
|
354
|
+
if (isPlainRecord(entry)) {
|
|
355
|
+
for (const key of Object.keys(entry)) {
|
|
356
|
+
if (!seen.has(key)) {
|
|
357
|
+
seen.add(key);
|
|
358
|
+
headers.push(key);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const rows = data.map((entry) => {
|
|
364
|
+
const record = isPlainRecord(entry) ? entry : {};
|
|
365
|
+
return headers.map((header) => stringifyCell(record[header]));
|
|
366
|
+
});
|
|
367
|
+
return { headers, rows };
|
|
368
|
+
}
|
|
369
|
+
function isPlainRecord(value) {
|
|
370
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
371
|
+
}
|
|
372
|
+
function stringifyCell(value) {
|
|
373
|
+
if (value === null || value === void 0) {
|
|
374
|
+
return "";
|
|
375
|
+
}
|
|
376
|
+
if (typeof value === "object") {
|
|
377
|
+
return JSON.stringify(value);
|
|
378
|
+
}
|
|
379
|
+
return String(value);
|
|
380
|
+
}
|
|
381
|
+
var STATUS_LABELS = Object.assign(
|
|
382
|
+
/* @__PURE__ */ Object.create(null),
|
|
383
|
+
{
|
|
384
|
+
publish: "publish",
|
|
385
|
+
published: "publish",
|
|
386
|
+
draft: "draft",
|
|
387
|
+
pending: "pending",
|
|
388
|
+
private: "private",
|
|
389
|
+
future: "future"
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
function normalizeStatus(value) {
|
|
393
|
+
const trimmed = value.trim();
|
|
394
|
+
return STATUS_LABELS[trimmed.toLowerCase()] ?? trimmed;
|
|
395
|
+
}
|
|
396
|
+
function buildImportPlan(table, mapping) {
|
|
397
|
+
const creates = [];
|
|
398
|
+
for (const row of table.rows) {
|
|
399
|
+
const fields = {};
|
|
400
|
+
let meta;
|
|
401
|
+
for (let col = 0; col < mapping.length; col += 1) {
|
|
402
|
+
const target = mapping[col];
|
|
403
|
+
if (!target || target.kind === "skip") {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const value = row[col] ?? "";
|
|
407
|
+
if (value === "") {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
switch (target.kind) {
|
|
411
|
+
case "title":
|
|
412
|
+
fields.title = value;
|
|
413
|
+
break;
|
|
414
|
+
case "status":
|
|
415
|
+
fields.status = normalizeStatus(value);
|
|
416
|
+
break;
|
|
417
|
+
case "menuOrder": {
|
|
418
|
+
const order = Number(value);
|
|
419
|
+
if (Number.isInteger(order) && order >= MENU_ORDER_MIN && order <= MENU_ORDER_MAX) {
|
|
420
|
+
fields.menuOrder = order;
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
case "meta":
|
|
425
|
+
if (target.key.trim() === "") {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
if (!meta) {
|
|
429
|
+
meta = /* @__PURE__ */ Object.create(null);
|
|
430
|
+
}
|
|
431
|
+
meta[target.key] = value;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (meta !== void 0 && Object.keys(meta).length > 0) {
|
|
436
|
+
creates.push({ fields, meta });
|
|
437
|
+
} else if (Object.keys(fields).length > 0) {
|
|
438
|
+
creates.push({ fields });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return creates;
|
|
442
|
+
}
|
|
443
|
+
export {
|
|
444
|
+
SafeFormulaEngine,
|
|
445
|
+
WpClient,
|
|
446
|
+
WpRequestError,
|
|
447
|
+
buildAuthHeader,
|
|
448
|
+
buildImportPlan,
|
|
449
|
+
normalizeSiteUrl,
|
|
450
|
+
normalizeStatus,
|
|
451
|
+
parseCsv,
|
|
452
|
+
parseJsonRecords
|
|
453
|
+
};
|
|
454
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/wp-client.ts","../src/calc/index.ts","../src/importer.ts"],"sourcesContent":["import type {\n DeleteMetaResult,\n ListPostsParams,\n UpdatePostFields,\n WpCredentials,\n WpPost,\n WpPostResponse,\n WpPostType,\n} from './types';\n\n/** Hosts for which plain http is tolerated (local development). */\nconst LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);\n\n/** Allowed characters for a REST route segment (post type slug). No dots: a `.`/`..`\n * segment would be resolved by the URL parser and traverse the REST path. */\nconst ROUTE_SEGMENT = /^[a-z0-9_-]+$/i;\n\n/** REST field added by the companion plugin to carry arbitrary post meta. */\nconst META_FIELD = 'dbp_wp_meta';\n\n/** REST namespace registered by the companion plugin. */\nconst CONNECTOR_NAMESPACE = 'dbp-wp/v1';\n\n/**\n * Validate and normalize a WordPress site URL into a REST base (origin + base path).\n *\n * Requires https, except plain http is allowed for local development hosts. Rejects\n * embedded credentials, query strings, and fragments, and strips trailing slashes — so\n * an Application Password is never sent over cleartext to an unexpected target.\n */\nexport function normalizeSiteUrl(siteUrl: string): string {\n let url: URL;\n try {\n url = new URL(siteUrl);\n } catch {\n throw new Error(`Invalid site URL: ${siteUrl}`);\n }\n\n const isLocal = LOCAL_HOSTS.has(url.hostname);\n if (url.protocol !== 'https:' && !(url.protocol === 'http:' && isLocal)) {\n throw new Error(`Site URL must use https (http is allowed only for local hosts): ${siteUrl}`);\n }\n if (url.username !== '' || url.password !== '') {\n throw new Error('Site URL must not contain embedded credentials.');\n }\n if (url.search !== '' || url.hash !== '') {\n throw new Error('Site URL must not contain a query string or fragment.');\n }\n\n return `${url.origin}${url.pathname}`.replace(/\\/+$/, '');\n}\n\n/**\n * Build the HTTP Basic `Authorization` header value from Application Password\n * credentials. WordPress treats the Application Password as the Basic-auth password.\n */\nexport function buildAuthHeader(credentials: WpCredentials): string {\n if (credentials.username.includes(':')) {\n throw new Error('Username must not contain a colon (\":\") for HTTP Basic authentication.');\n }\n const token = `${credentials.username}:${credentials.applicationPassword}`;\n const base64 = Buffer.from(token, 'utf-8').toString('base64');\n return `Basic ${base64}`;\n}\n\n/** Error thrown when the WordPress REST API returns a non-2xx response. */\nexport class WpRequestError extends Error {\n constructor(\n readonly status: number,\n readonly path: string,\n message: string,\n ) {\n super(message);\n this.name = 'WpRequestError';\n }\n}\n\n/**\n * Minimal WordPress REST client.\n *\n * Runs in the Node process (CLI shell), never in the browser, so Application Password\n * credentials stay server-side. MVP scope: list posts, read/write post meta.\n */\nexport class WpClient {\n private readonly restBase: string;\n\n constructor(private readonly credentials: WpCredentials) {\n this.restBase = normalizeSiteUrl(credentials.siteUrl);\n }\n\n private async request<T>(path: string, init: RequestInit = {}): Promise<T> {\n const response = await fetch(`${this.restBase}/wp-json${path}`, {\n ...init,\n headers: {\n Authorization: buildAuthHeader(this.credentials),\n // Only declare a JSON body when one is actually sent (GETs carry none).\n ...(init.body !== undefined ? { 'Content-Type': 'application/json' } : {}),\n ...init.headers,\n },\n });\n\n if (!response.ok) {\n throw new WpRequestError(\n response.status,\n path,\n `WordPress REST request failed: ${response.status} ${response.statusText}`,\n );\n }\n\n return (await response.json()) as T;\n }\n\n /**\n * List the REST-enabled post types on the site (edit context), so the app can offer\n * a type selector. Returns each type's REST route base and display name.\n */\n async listPostTypes(): Promise<WpPostType[]> {\n const raw = await this.request<unknown>('/wp/v2/types?context=edit');\n return normalizePostTypes(raw);\n }\n\n /** List posts of a given type in edit context (raw fields, for editing). */\n async listPosts(params: ListPostsParams = {}): Promise<WpPost[]> {\n const type = params.type ?? 'posts';\n assertRouteSegment(type);\n const perPage = clampInt(params.perPage ?? 100, 1, 100);\n const page = clampInt(params.page ?? 1, 1, Number.MAX_SAFE_INTEGER);\n const query = new URLSearchParams({\n context: 'edit',\n per_page: String(perPage),\n page: String(page),\n });\n const raw = await this.request<WpPostResponse[]>(`/wp/v2/${type}?${query.toString()}`);\n return raw.map(normalizePost);\n }\n\n /**\n * Update post fields in a single request. Standard fields (title, menu_order,\n * status) are core REST fields and need no plugin. When `meta` is supplied it rides\n * the same request through the companion plugin's `dbp_wp_meta` field (ignored by\n * WordPress without the connector). Pass the REST route slug as `type` (e.g.\n * `posts`, `pages`) — not the object type returned on a post.\n */\n async updatePost(\n id: number,\n fields: UpdatePostFields,\n type = 'posts',\n meta?: Record<string, unknown>,\n ): Promise<WpPost> {\n assertPostId(id);\n assertRouteSegment(type);\n const raw = await this.request<WpPostResponse>(`/wp/v2/${type}/${String(id)}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(buildPostBody(fields, meta)),\n });\n return normalizePost(raw);\n }\n\n /**\n * Create a new post in a single request, symmetric to {@link WpClient.updatePost}.\n * Standard fields (title, menu_order, status) are core REST fields; when `meta` is\n * supplied it rides the same request through the companion plugin's `dbp_wp_meta`\n * field. Pass the REST route slug as `type` (e.g. `posts`, `pages`).\n */\n async createPost(\n fields: UpdatePostFields,\n type = 'posts',\n meta?: Record<string, unknown>,\n ): Promise<WpPost> {\n assertRouteSegment(type);\n const raw = await this.request<WpPostResponse>(`/wp/v2/${type}?context=edit`, {\n method: 'POST',\n body: JSON.stringify(buildPostBody(fields, meta)),\n });\n return normalizePost(raw);\n }\n\n /**\n * Update only arbitrary post meta through the companion plugin's `dbp_wp_meta`\n * field. A thin wrapper over {@link WpClient.updatePost} with no standard fields.\n * Requires the connector; the connector writes scalar values only.\n */\n async updatePostMeta(\n id: number,\n meta: Record<string, unknown>,\n type = 'posts',\n ): Promise<WpPost> {\n return this.updatePost(id, {}, type, meta);\n }\n\n /**\n * Delete named meta keys from a single post via the companion plugin's\n * `DELETE /dbp-wp/v1/posts/<id>/meta` route. This route is keyed by id only (the\n * post type is irrelevant). Requires the connector.\n */\n async deletePostMeta(id: number, keys: string[]): Promise<DeleteMetaResult> {\n assertPostId(id);\n const cleanKeys = sanitizeMetaKeys(keys);\n const raw = await this.request<{ post_id?: unknown; deleted?: string[] }>(\n `/${CONNECTOR_NAMESPACE}/posts/${String(id)}/meta`,\n { method: 'DELETE', body: JSON.stringify({ keys: cleanKeys }) },\n );\n // Trust our own request `id` over a malformed connector `post_id`.\n return {\n postId: typeof raw.post_id === 'number' ? raw.post_id : id,\n deleted: Array.isArray(raw.deleted) ? raw.deleted : [],\n };\n }\n\n /**\n * Detect whether the companion plugin is active by checking the REST index\n * (`/wp-json/`) for the connector's namespace. Throws on a failed request; a caller\n * that wants a non-fatal probe should treat a thrown error as \"not available\".\n */\n async detectConnector(): Promise<boolean> {\n const index = await this.request<{ namespaces?: unknown }>('/');\n return hasConnectorNamespace(index.namespaces);\n }\n}\n\n/** Map editable fields to the WordPress REST request body (camelCase → snake_case). */\nexport function buildUpdateBody(fields: UpdatePostFields): Record<string, unknown> {\n const body: Record<string, unknown> = {};\n if (fields.title !== undefined) {\n body.title = fields.title;\n }\n if (fields.menuOrder !== undefined) {\n body.menu_order = fields.menuOrder;\n }\n if (fields.status !== undefined) {\n body.status = fields.status;\n }\n return body;\n}\n\nfunction assertRouteSegment(segment: string): void {\n if (!ROUTE_SEGMENT.test(segment)) {\n throw new Error(`Invalid REST route segment: ${segment}`);\n }\n}\n\nfunction assertPostId(id: number): void {\n if (!Number.isSafeInteger(id) || id <= 0) {\n throw new Error(`Invalid post id: ${id}`);\n }\n}\n\nfunction clampInt(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) {\n return min;\n }\n return Math.min(max, Math.max(min, Math.trunc(value)));\n}\n\n/** Wrap arbitrary meta in the companion plugin's REST field for a write request. */\nexport function buildMetaBody(meta: Record<string, unknown>): Record<string, unknown> {\n return { [META_FIELD]: meta };\n}\n\n/**\n * Build a post-update body, folding in connector meta under `dbp_wp_meta` when given.\n * A provided `meta` is included as-is, even when empty — callers that should skip empty\n * meta (e.g. the CLI batch parser) omit it before calling.\n */\nexport function buildPostBody(\n fields: UpdatePostFields,\n meta?: Record<string, unknown>,\n): Record<string, unknown> {\n const body = buildUpdateBody(fields);\n if (meta !== undefined) {\n Object.assign(body, buildMetaBody(meta));\n }\n return body;\n}\n\n/** Validate and clean a list of meta keys for deletion (non-empty strings only). */\nexport function sanitizeMetaKeys(keys: unknown): string[] {\n if (!Array.isArray(keys)) {\n throw new Error('Meta keys must be an array of strings.');\n }\n const clean = keys.filter((key): key is string => typeof key === 'string' && key.length > 0);\n if (clean.length === 0) {\n throw new Error('At least one non-empty meta key is required.');\n }\n return clean;\n}\n\n/** True when a REST index `namespaces` list includes the connector namespace. */\nexport function hasConnectorNamespace(namespaces: unknown): boolean {\n return Array.isArray(namespaces) && namespaces.includes(CONNECTOR_NAMESPACE);\n}\n\n/**\n * Normalize the `/wp/v2/types` response (an object keyed by type name) into a list.\n * Skips entries without a string `rest_base` (not addressable over REST).\n */\nexport function normalizePostTypes(raw: unknown): WpPostType[] {\n if (typeof raw !== 'object' || raw === null) {\n return [];\n }\n const result: WpPostType[] = [];\n for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value !== 'object' || value === null) {\n continue;\n }\n const entry = value as Record<string, unknown>;\n // Validate rest_base at ingestion so a malformed slug can't become a broken option.\n if (typeof entry.rest_base !== 'string' || !ROUTE_SEGMENT.test(entry.rest_base)) {\n continue;\n }\n result.push({\n slug: typeof entry.slug === 'string' ? entry.slug : key,\n restBase: entry.rest_base,\n name: typeof entry.name === 'string' ? entry.name : key,\n });\n }\n return result;\n}\n\nfunction normalizePost(raw: WpPostResponse): WpPost {\n const post: WpPost = {\n id: raw.id,\n type: raw.type,\n status: raw.status,\n title: raw.title.raw ?? raw.title.rendered,\n menuOrder: raw.menu_order,\n meta: raw.meta,\n };\n if (raw.dbp_wp_meta !== undefined) {\n post.dbpWpMeta = raw.dbp_wp_meta;\n }\n return post;\n}\n","import { Parser, type Expression } from 'expr-eval-fork';\n\n/**\n * Formula engine: evaluates a spreadsheet expression against named numeric cells.\n *\n * Implementations MUST NOT use `eval`, `Function`, or any other dynamic code execution.\n */\nexport interface FormulaEngine {\n /**\n * Evaluate a single expression against a map of cell references to numbers.\n * Throws on invalid syntax, unknown variables, or a non-numeric result.\n */\n evaluate(expression: string, context: Record<string, number>): number;\n}\n\n/**\n * Formula engine backed by expr-eval-fork, which parses to an AST and evaluates without\n * `eval`/`Function`. Member access, assignment, and function definitions are disabled,\n * the nondeterministic `random()` function is removed, and results are constrained to\n * finite numbers, so expressions stay pure, deterministic, and side-effect free.\n */\nexport class SafeFormulaEngine implements FormulaEngine {\n private readonly parser: Parser;\n\n constructor() {\n this.parser = new Parser({\n allowMemberAccess: false,\n operators: { assignment: false, fndef: false },\n });\n // The `operators.random` flag does not remove the random() function; delete it so\n // evaluation stays deterministic.\n delete this.parser.functions.random;\n }\n\n evaluate(expression: string, context: Record<string, number>): number {\n let parsed: Expression;\n try {\n parsed = this.parser.parse(expression);\n } catch (e) {\n throw new Error(`Invalid formula: ${e instanceof Error ? e.message : 'parse error'}`);\n }\n\n let result: unknown;\n try {\n result = parsed.evaluate(context);\n } catch (e) {\n throw new Error(`Formula evaluation failed: ${e instanceof Error ? e.message : 'error'}`);\n }\n\n if (typeof result !== 'number' || !Number.isFinite(result)) {\n throw new Error('Formula must evaluate to a finite number.');\n }\n return result;\n }\n}\n","import type { UpdatePostFields } from './types';\n\n// WordPress stores menu_order in a signed 32-bit column; ignore cells outside that range\n// so one bad value does not get the whole server-side chunk rejected.\nconst MENU_ORDER_MIN = -2_147_483_648;\nconst MENU_ORDER_MAX = 2_147_483_647;\n\n/**\n * A tabular view of an import file: a header row plus data rows. Both CSV and JSON\n * sources are normalized to this shape so the column-mapping logic is source-agnostic.\n * Each data row is aligned to `headers` by index; a short row has missing trailing cells.\n */\nexport interface ParsedTable {\n /** Column headers (CSV first row, or the union of JSON object keys). */\n headers: string[];\n /** Data rows; `rows[r][c]` is the cell under `headers[c]`. */\n rows: string[][];\n}\n\n/**\n * Where a file column is imported to. `skip` drops the column; `title`/`status`/\n * `menuOrder` map to standard post fields; `meta` writes an arbitrary post-meta key\n * (companion plugin required).\n */\nexport type ImportTarget =\n | { kind: 'skip' }\n | { kind: 'title' }\n | { kind: 'status' }\n | { kind: 'menuOrder' }\n | { kind: 'meta'; key: string };\n\n/** A single new post to create, derived from one import row. */\nexport interface ImportCreate {\n /** Standard fields (title / menuOrder / status). */\n fields: UpdatePostFields;\n /** Arbitrary meta to write via the companion plugin (omitted when none). */\n meta?: Record<string, unknown>;\n}\n\n/**\n * Parse CSV text into a table, taking the first record as headers. Implements the\n * RFC 4180 essentials: double-quoted fields, embedded commas/newlines, `\"\"` escapes,\n * and CRLF or LF line endings. A trailing newline does not produce an empty record.\n */\nexport function parseCsv(text: string): ParsedTable {\n const records = parseCsvRecords(text);\n const headers = records[0] ?? [];\n return { headers, rows: records.slice(1) };\n}\n\nfunction parseCsvRecords(text: string): string[][] {\n const records: string[][] = [];\n let record: string[] = [];\n let field = '';\n let inQuotes = false;\n let i = 0;\n\n const endField = (): void => {\n record.push(field);\n field = '';\n };\n const endRecord = (): void => {\n endField();\n records.push(record);\n record = [];\n };\n\n while (i < text.length) {\n const ch = text[i];\n if (inQuotes) {\n if (ch === '\"') {\n if (text[i + 1] === '\"') {\n field += '\"';\n i += 2;\n continue;\n }\n inQuotes = false;\n i += 1;\n continue;\n }\n field += ch;\n i += 1;\n continue;\n }\n if (ch === '\"') {\n inQuotes = true;\n i += 1;\n continue;\n }\n if (ch === ',') {\n endField();\n i += 1;\n continue;\n }\n if (ch === '\\r') {\n endRecord();\n i += text[i + 1] === '\\n' ? 2 : 1;\n continue;\n }\n if (ch === '\\n') {\n endRecord();\n i += 1;\n continue;\n }\n field += ch;\n i += 1;\n }\n // An unclosed quote means the file is malformed; surface it rather than silently\n // merging the rest of the file into one field and writing corrupted data.\n if (inQuotes) {\n throw new Error('Malformed CSV: unterminated quoted field.');\n }\n // Flush a final record only if there is pending content (no trailing-newline ghost row).\n if (field !== '' || record.length > 0) {\n endRecord();\n }\n return records;\n}\n\n/**\n * Parse JSON text (an array of objects) into a table. Headers are the union of all\n * object keys in first-seen order. Object/array cell values are JSON-stringified;\n * null and undefined become an empty string. Throws if the JSON is not an array.\n */\nexport function parseJsonRecords(text: string): ParsedTable {\n const data: unknown = JSON.parse(text);\n if (!Array.isArray(data)) {\n throw new Error('JSON import must be an array of objects.');\n }\n const headers: string[] = [];\n const seen = new Set<string>();\n for (const entry of data) {\n if (isPlainRecord(entry)) {\n for (const key of Object.keys(entry)) {\n if (!seen.has(key)) {\n seen.add(key);\n headers.push(key);\n }\n }\n }\n }\n const rows = data.map((entry) => {\n const record = isPlainRecord(entry) ? entry : {};\n return headers.map((header) => stringifyCell(record[header]));\n });\n return { headers, rows };\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction stringifyCell(value: unknown): string {\n if (value === null || value === undefined) {\n return '';\n }\n if (typeof value === 'object') {\n return JSON.stringify(value);\n }\n return String(value);\n}\n\n/**\n * Known post-status values and English/value labels, mapped to the WordPress status.\n * A null-prototype map so inherited keys (`constructor`, `toString`, `__proto__`, …) do\n * not accidentally resolve to a function/object instead of falling back to the raw value.\n */\nconst STATUS_LABELS: Record<string, string> = Object.assign(\n Object.create(null) as Record<string, string>,\n {\n publish: 'publish',\n published: 'publish',\n draft: 'draft',\n pending: 'pending',\n private: 'private',\n future: 'future',\n },\n);\n\n/**\n * Normalize a status cell to a WordPress status. Known labels/values (case-insensitive,\n * e.g. `Published` → `publish`) are mapped; anything else passes through trimmed so the\n * WordPress REST API can validate it and surface a per-row error if invalid.\n */\nexport function normalizeStatus(value: string): string {\n const trimmed = value.trim();\n return STATUS_LABELS[trimmed.toLowerCase()] ?? trimmed;\n}\n\n/**\n * Apply a column mapping to a parsed table, producing one {@link ImportCreate} per row.\n * Empty cells contribute nothing; a row that maps to no fields and no meta is skipped.\n * Non-integer `menuOrder` cells are ignored. Meta is stored on a null-prototype object\n * so a header named `__proto__` is kept as data, never touching any prototype.\n */\nexport function buildImportPlan(table: ParsedTable, mapping: ImportTarget[]): ImportCreate[] {\n const creates: ImportCreate[] = [];\n for (const row of table.rows) {\n const fields: UpdatePostFields = {};\n let meta: Record<string, unknown> | undefined;\n\n for (let col = 0; col < mapping.length; col += 1) {\n const target = mapping[col];\n if (!target || target.kind === 'skip') {\n continue;\n }\n const value = row[col] ?? '';\n if (value === '') {\n continue;\n }\n switch (target.kind) {\n case 'title':\n fields.title = value;\n break;\n case 'status':\n fields.status = normalizeStatus(value);\n break;\n case 'menuOrder': {\n const order = Number(value);\n if (Number.isInteger(order) && order >= MENU_ORDER_MIN && order <= MENU_ORDER_MAX) {\n fields.menuOrder = order;\n }\n break;\n }\n case 'meta':\n // An empty/whitespace meta key would be rejected by the server (empty key),\n // failing the whole chunk; skip it rather than emit `meta[\"\"]`.\n if (target.key.trim() === '') {\n break;\n }\n if (!meta) {\n meta = Object.create(null) as Record<string, unknown>;\n }\n meta[target.key] = value;\n break;\n }\n }\n\n if (meta !== undefined && Object.keys(meta).length > 0) {\n creates.push({ fields, meta });\n } else if (Object.keys(fields).length > 0) {\n creates.push({ fields });\n }\n }\n return creates;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dbp-wp/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Core library for DBP WP: WordPress REST client, formula engine, importer, and typesetting data generation.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Takashi Matsuyama",
|
|
7
|
+
"homepage": "https://wp.dbp.jp",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/takashi-matsuyama/dbp_wp.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/takashi-matsuyama/dbp_wp/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"wordpress",
|
|
18
|
+
"wordpress-rest-api",
|
|
19
|
+
"bulk-edit"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"expr-eval-fork": "^3.0.3"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"tsup": "^8.3.0"
|
|
48
|
+
}
|
|
49
|
+
}
|