@mrtimmy/payload-connector 0.2.0 → 0.2.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/README.md +25 -2
- package/package.json +2 -1
- package/src/index.js +70 -18
- package/src/lexical.js +12 -0
- package/src/utils.js +96 -48
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ export default buildConfig({
|
|
|
18
18
|
plugins: [
|
|
19
19
|
mrTimmyPayloadConnector({
|
|
20
20
|
postsCollection: "posts",
|
|
21
|
+
taxonomies: {
|
|
22
|
+
categories: { matchFields: ["slug", "title"] },
|
|
23
|
+
tags: { matchFields: ["slug", "title"] },
|
|
24
|
+
},
|
|
21
25
|
}),
|
|
22
26
|
],
|
|
23
27
|
})
|
|
@@ -25,6 +29,17 @@ export default buildConfig({
|
|
|
25
29
|
|
|
26
30
|
Deploy the Payload site after changing the config.
|
|
27
31
|
|
|
32
|
+
If your Payload app runs inside Next.js and external packages are bundled too
|
|
33
|
+
aggressively, keep the connector external in `next.config.js`:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const nextConfig = {
|
|
37
|
+
serverExternalPackages: ["@mrtimmy/payload-connector"],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default nextConfig
|
|
41
|
+
```
|
|
42
|
+
|
|
28
43
|
## Create a Connector Key
|
|
29
44
|
|
|
30
45
|
After deployment, open the Payload admin panel:
|
|
@@ -87,10 +102,12 @@ mrTimmyPayloadConnector({
|
|
|
87
102
|
categories: {
|
|
88
103
|
collection: "categories",
|
|
89
104
|
create: true,
|
|
105
|
+
matchFields: ["slug", "title"],
|
|
90
106
|
},
|
|
91
107
|
tags: {
|
|
92
108
|
collection: "tags",
|
|
93
109
|
create: true,
|
|
110
|
+
matchFields: ["slug", "title"],
|
|
94
111
|
},
|
|
95
112
|
},
|
|
96
113
|
})
|
|
@@ -146,8 +163,9 @@ The connector accepts category/tag input from fields like `categories`,
|
|
|
146
163
|
`categoryNames`, `categorySlugs`, `tags`, `tagNames`, and `tagSlugs`.
|
|
147
164
|
|
|
148
165
|
If the target Payload fields are relationship fields, the connector looks up
|
|
149
|
-
matching documents by `slug
|
|
150
|
-
|
|
166
|
+
matching documents by `slug` or `title`. Missing terms are created by default.
|
|
167
|
+
Configured `matchFields` that do not exist in the target Payload collection are
|
|
168
|
+
ignored.
|
|
151
169
|
|
|
152
170
|
```ts
|
|
153
171
|
mrTimmyPayloadConnector({
|
|
@@ -160,12 +178,14 @@ mrTimmyPayloadConnector({
|
|
|
160
178
|
collection: "categories",
|
|
161
179
|
labelField: "title",
|
|
162
180
|
slugField: "slug",
|
|
181
|
+
matchFields: ["slug", "title"],
|
|
163
182
|
create: true,
|
|
164
183
|
},
|
|
165
184
|
tags: {
|
|
166
185
|
collection: "tags",
|
|
167
186
|
labelField: "title",
|
|
168
187
|
slugField: "slug",
|
|
188
|
+
matchFields: ["slug", "title"],
|
|
169
189
|
create: true,
|
|
170
190
|
},
|
|
171
191
|
},
|
|
@@ -181,3 +201,6 @@ If your taxonomy collections use another required field, provide `createData`.
|
|
|
181
201
|
- Draft/publish defaults to `_status: "draft" | "published"`, matching Payload draft-enabled collections.
|
|
182
202
|
- If your collection does not use drafts or uses a custom status field, configure `fields.status` and `statuses`, or set `fields.status` to `null`.
|
|
183
203
|
- Uploads use Payload's Local API with `filePath`; hooks and storage adapters on your media collection still run.
|
|
204
|
+
- Connector errors include the failing step (`RichText-Konvertierung`,
|
|
205
|
+
`Taxonomie-Mapping`, `Media-Upload` or `Payload-Create`) so mr.timmy can show a
|
|
206
|
+
useful error instead of a generic Payload failure.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrtimmy/payload-connector",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Payload CMS connector for publishing mr.timmy blog posts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"src/index.js",
|
|
24
24
|
"src/index.d.ts",
|
|
25
|
+
"src/lexical.js",
|
|
25
26
|
"src/utils.js",
|
|
26
27
|
"README.md"
|
|
27
28
|
],
|
package/src/index.js
CHANGED
|
@@ -2,17 +2,19 @@ import {
|
|
|
2
2
|
DEFAULT_ENDPOINT_PATH,
|
|
3
3
|
DEFAULT_HEADER_NAMES,
|
|
4
4
|
DEFAULT_KEYS_COLLECTION,
|
|
5
|
+
ConnectorStepError,
|
|
5
6
|
buildApiUrlHint,
|
|
6
7
|
buildPostData,
|
|
7
8
|
buildPublishData,
|
|
8
9
|
constantTimeEquals,
|
|
10
|
+
errorMessage,
|
|
9
11
|
generateConnectorKey,
|
|
10
12
|
maskKey,
|
|
11
13
|
normalizeEndpointPath,
|
|
12
14
|
readConnectorHeader,
|
|
13
15
|
} from "./utils.js"
|
|
14
16
|
|
|
15
|
-
const VERSION = "0.2.
|
|
17
|
+
const VERSION = "0.2.1"
|
|
16
18
|
|
|
17
19
|
export function mrTimmyPayloadConnector(pluginOptions = {}) {
|
|
18
20
|
return (incomingConfig = {}) => {
|
|
@@ -169,14 +171,25 @@ function createConnectorEndpoints(options) {
|
|
|
169
171
|
return connectorError("Titel fehlt.", 400)
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
let data
|
|
175
|
+
try {
|
|
176
|
+
data = await buildPostData(input, options, req)
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return connectorException(req, "Datenaufbereitung", error)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let doc
|
|
182
|
+
try {
|
|
183
|
+
doc = await req.payload.create({
|
|
184
|
+
collection: options.postsCollection,
|
|
185
|
+
data,
|
|
186
|
+
depth: 0,
|
|
187
|
+
draft: true,
|
|
188
|
+
overrideAccess: true,
|
|
189
|
+
})
|
|
190
|
+
} catch (error) {
|
|
191
|
+
return connectorException(req, "Payload-Create", error)
|
|
192
|
+
}
|
|
180
193
|
|
|
181
194
|
return Response.json({
|
|
182
195
|
success: true,
|
|
@@ -199,15 +212,26 @@ function createConnectorEndpoints(options) {
|
|
|
199
212
|
}
|
|
200
213
|
|
|
201
214
|
const input = await req.json().catch(() => ({}))
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
let data
|
|
216
|
+
try {
|
|
217
|
+
data = await buildPublishData(options, input, req)
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return connectorException(req, "Publish-Datenaufbereitung", error)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let doc
|
|
223
|
+
try {
|
|
224
|
+
doc = await req.payload.update({
|
|
225
|
+
collection: options.postsCollection,
|
|
226
|
+
id,
|
|
227
|
+
data,
|
|
228
|
+
depth: 0,
|
|
229
|
+
draft: false,
|
|
230
|
+
overrideAccess: true,
|
|
231
|
+
})
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return connectorException(req, "Payload-Publish", error)
|
|
234
|
+
}
|
|
211
235
|
|
|
212
236
|
const url =
|
|
213
237
|
typeof options.resolvePublishedUrl === "function"
|
|
@@ -257,6 +281,34 @@ function connectorError(message, status = 500) {
|
|
|
257
281
|
return Response.json({ success: false, error: message, message }, { status })
|
|
258
282
|
}
|
|
259
283
|
|
|
284
|
+
function connectorException(req, step, error, status = 500) {
|
|
285
|
+
const finalStep = error instanceof ConnectorStepError ? error.step : step
|
|
286
|
+
const message =
|
|
287
|
+
error instanceof ConnectorStepError
|
|
288
|
+
? error.message
|
|
289
|
+
: `${step}: ${errorMessage(error)}`
|
|
290
|
+
|
|
291
|
+
logConnectorException(req, finalStep, error, message)
|
|
292
|
+
return Response.json(
|
|
293
|
+
{ success: false, error: message, message, step: finalStep },
|
|
294
|
+
{ status },
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function logConnectorException(req, step, error, message) {
|
|
299
|
+
const line = `[mr.timmy Payload Connector] ${step} fehlgeschlagen: ${message}`
|
|
300
|
+
const logger = req?.payload?.logger
|
|
301
|
+
if (logger?.error) {
|
|
302
|
+
try {
|
|
303
|
+
logger.error(line)
|
|
304
|
+
return
|
|
305
|
+
} catch {
|
|
306
|
+
// Fall through to console logging.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
console.error(line, error)
|
|
310
|
+
}
|
|
311
|
+
|
|
260
312
|
function defaultPublishedUrl(doc, req) {
|
|
261
313
|
if (typeof doc.url === "string") return doc.url
|
|
262
314
|
if (typeof doc.publishedUrl === "string") return doc.publishedUrl
|
package/src/lexical.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { convertHTMLToLexical, editorConfigFactory } from "@payloadcms/richtext-lexical"
|
|
2
|
+
import { JSDOM } from "jsdom"
|
|
3
|
+
|
|
4
|
+
export async function convertHtmlToLexicalContent(html, req) {
|
|
5
|
+
return convertHTMLToLexical({
|
|
6
|
+
editorConfig: await editorConfigFactory.default({
|
|
7
|
+
config: req.payload.config,
|
|
8
|
+
}),
|
|
9
|
+
html,
|
|
10
|
+
JSDOM,
|
|
11
|
+
})
|
|
12
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -2,8 +2,7 @@ import crypto from "node:crypto"
|
|
|
2
2
|
import fs from "node:fs/promises"
|
|
3
3
|
import os from "node:os"
|
|
4
4
|
import path from "node:path"
|
|
5
|
-
|
|
6
|
-
const optionalImport = (specifier) => import(/* @vite-ignore */ specifier)
|
|
5
|
+
import { marked } from "marked"
|
|
7
6
|
|
|
8
7
|
export const DEFAULT_ENDPOINT_PATH = "/mrtimmy/create-post"
|
|
9
8
|
export const DEFAULT_KEYS_COLLECTION = "mrtimmy-connector-keys"
|
|
@@ -47,17 +46,42 @@ export const DEFAULT_TAXONOMIES = {
|
|
|
47
46
|
inputFields: ["categories", "categoryNames", "categorySlugs"],
|
|
48
47
|
labelField: "title",
|
|
49
48
|
slugField: "slug",
|
|
50
|
-
matchFields: ["slug", "title"
|
|
49
|
+
matchFields: ["slug", "title"],
|
|
51
50
|
},
|
|
52
51
|
tags: {
|
|
53
52
|
create: true,
|
|
54
53
|
inputFields: ["tags", "tagNames", "tagSlugs"],
|
|
55
54
|
labelField: "title",
|
|
56
55
|
slugField: "slug",
|
|
57
|
-
matchFields: ["slug", "title"
|
|
56
|
+
matchFields: ["slug", "title"],
|
|
58
57
|
},
|
|
59
58
|
}
|
|
60
59
|
|
|
60
|
+
export class ConnectorStepError extends Error {
|
|
61
|
+
constructor(step, error) {
|
|
62
|
+
super(`${step}: ${errorMessage(error)}`)
|
|
63
|
+
this.name = "ConnectorStepError"
|
|
64
|
+
this.step = step
|
|
65
|
+
this.cause = error
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function errorMessage(error) {
|
|
70
|
+
if (error instanceof Error) return error.message
|
|
71
|
+
if (typeof error === "string") return error
|
|
72
|
+
try {
|
|
73
|
+
return JSON.stringify(error)
|
|
74
|
+
} catch {
|
|
75
|
+
return String(error)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function wrapStepError(step, error) {
|
|
80
|
+
return error instanceof ConnectorStepError
|
|
81
|
+
? error
|
|
82
|
+
: new ConnectorStepError(step, error)
|
|
83
|
+
}
|
|
84
|
+
|
|
61
85
|
export function generateConnectorKey(prefix = "timmy") {
|
|
62
86
|
return `${prefix}_${crypto.randomBytes(32).toString("hex")}`
|
|
63
87
|
}
|
|
@@ -371,7 +395,6 @@ export function readConnectorHeader(req, headerNames = DEFAULT_HEADER_NAMES) {
|
|
|
371
395
|
}
|
|
372
396
|
|
|
373
397
|
export async function markdownToHtml(markdown) {
|
|
374
|
-
const { marked } = await import("marked")
|
|
375
398
|
return marked.parse(String(markdown ?? ""))
|
|
376
399
|
}
|
|
377
400
|
|
|
@@ -380,19 +403,8 @@ export async function convertContent(markdown, mode, req) {
|
|
|
380
403
|
const html = await markdownToHtml(markdown)
|
|
381
404
|
if (mode === "html") return html
|
|
382
405
|
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
optionalImport("@payloadcms/richtext-lexical"),
|
|
386
|
-
optionalImport("jsdom"),
|
|
387
|
-
])
|
|
388
|
-
|
|
389
|
-
return convertHTMLToLexical({
|
|
390
|
-
editorConfig: await editorConfigFactory.default({
|
|
391
|
-
config: req.payload.config,
|
|
392
|
-
}),
|
|
393
|
-
html,
|
|
394
|
-
JSDOM,
|
|
395
|
-
})
|
|
406
|
+
const { convertHtmlToLexicalContent } = await import("./lexical.js")
|
|
407
|
+
return convertHtmlToLexicalContent(html, req)
|
|
396
408
|
}
|
|
397
409
|
|
|
398
410
|
function taxonomyInput(input, config) {
|
|
@@ -408,7 +420,7 @@ async function findOrCreateTaxonomyDoc(term, config, collection, req) {
|
|
|
408
420
|
|
|
409
421
|
const label = term.label ?? term.slug
|
|
410
422
|
const slug = term.slug ?? slugify(label)
|
|
411
|
-
const matchFields = config.matchFields ?? []
|
|
423
|
+
const matchFields = existingMatchFields(req, collection, config.matchFields ?? [])
|
|
412
424
|
const where = {
|
|
413
425
|
or: matchFields.flatMap((field) => {
|
|
414
426
|
const values = field === config.slugField ? [slug, label] : [label, slug]
|
|
@@ -448,6 +460,18 @@ async function findOrCreateTaxonomyDoc(term, config, collection, req) {
|
|
|
448
460
|
return doc.id ?? doc._id
|
|
449
461
|
}
|
|
450
462
|
|
|
463
|
+
function existingMatchFields(req, collection, matchFields) {
|
|
464
|
+
const collectionConfig = getCollectionConfig(req, collection)
|
|
465
|
+
const unique = [...new Set(matchFields.filter(Boolean))]
|
|
466
|
+
if (!collectionConfig) return unique
|
|
467
|
+
return unique.filter(
|
|
468
|
+
(field) =>
|
|
469
|
+
field === "id" ||
|
|
470
|
+
field === "_id" ||
|
|
471
|
+
Boolean(getFieldConfig(req, collection, field)),
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
|
|
451
475
|
async function resolveTaxonomyValues(input, options, req, key, fieldConfig) {
|
|
452
476
|
const config = options.taxonomies[key]
|
|
453
477
|
if (!config) return undefined
|
|
@@ -483,43 +507,67 @@ export async function buildPostData(input, options, req) {
|
|
|
483
507
|
const resolvedOptions = { ...options, taxonomies }
|
|
484
508
|
const contentMode = options.contentMode ?? "lexical"
|
|
485
509
|
const uploadCache = new Map()
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
510
|
+
let markdown
|
|
511
|
+
try {
|
|
512
|
+
markdown = await rewriteInlineImages(
|
|
513
|
+
input.content ?? input.body ?? "",
|
|
514
|
+
media,
|
|
515
|
+
req,
|
|
516
|
+
uploadCache,
|
|
517
|
+
)
|
|
518
|
+
} catch (error) {
|
|
519
|
+
throw wrapStepError("Media-Upload/Inline-Bilder", error)
|
|
520
|
+
}
|
|
492
521
|
const data = {}
|
|
493
522
|
const featuredImageField = getFieldConfig(
|
|
494
523
|
req,
|
|
495
524
|
options.postsCollection,
|
|
496
525
|
fields.featuredImageUrl,
|
|
497
526
|
)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
527
|
+
let featuredImage
|
|
528
|
+
try {
|
|
529
|
+
featuredImage = input.featuredImage
|
|
530
|
+
? fieldExpectsUpload(featuredImageField) && media.uploadFeaturedImage
|
|
531
|
+
? await getUploadedImage(input.featuredImage, media, req, uploadCache, {
|
|
532
|
+
alt: input.imageAlt ?? input.title,
|
|
533
|
+
title: input.title,
|
|
534
|
+
})
|
|
535
|
+
: { url: input.featuredImage }
|
|
536
|
+
: null
|
|
537
|
+
} catch (error) {
|
|
538
|
+
throw wrapStepError("Media-Upload/Beitragsbild", error)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let categories
|
|
542
|
+
let tags
|
|
543
|
+
try {
|
|
544
|
+
categories = await resolveTaxonomyValues(
|
|
545
|
+
input,
|
|
546
|
+
resolvedOptions,
|
|
547
|
+
req,
|
|
548
|
+
"categories",
|
|
549
|
+
getFieldConfig(req, options.postsCollection, fields.categories),
|
|
550
|
+
)
|
|
551
|
+
tags = await resolveTaxonomyValues(
|
|
552
|
+
input,
|
|
553
|
+
resolvedOptions,
|
|
554
|
+
req,
|
|
555
|
+
"tags",
|
|
556
|
+
getFieldConfig(req, options.postsCollection, fields.tags),
|
|
557
|
+
)
|
|
558
|
+
} catch (error) {
|
|
559
|
+
throw wrapStepError("Taxonomie-Mapping", error)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let content
|
|
563
|
+
try {
|
|
564
|
+
content = await convertContent(markdown, contentMode, req)
|
|
565
|
+
} catch (error) {
|
|
566
|
+
throw wrapStepError("RichText-Konvertierung", error)
|
|
567
|
+
}
|
|
520
568
|
|
|
521
569
|
setPath(data, fields.title, input.title)
|
|
522
|
-
setPath(data, fields.content,
|
|
570
|
+
setPath(data, fields.content, content)
|
|
523
571
|
setPath(data, fields.excerpt, input.excerpt ?? extractExcerpt(markdown))
|
|
524
572
|
setPath(data, fields.slug, input.slug)
|
|
525
573
|
setPath(data, fields.metaTitle, input.metaTitle)
|