@byline/richtext-lexical 3.21.0 → 4.0.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.
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { getClientConfig, resolveRoutes } from "@byline/core";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { getClientConfig } from "@byline/core";
5
5
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
6
6
  import { $findMatchingParent, mergeRegister } from "@lexical/utils";
7
7
  import { $getSelection, $isLineBreakNode, $isRangeSelection, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, KEY_ESCAPE_COMMAND, SELECTION_CHANGE_COMMAND } from "lexical";
@@ -20,7 +20,7 @@ function FloatingLinkEditor({ editor, isLink, setIsLink, anchorElem }) {
20
20
  });
21
21
  const [linkModalData, setLinkModalData] = useState(void 0);
22
22
  const [modalOpen, setModalOpen] = useState(false);
23
- const adminRoute = useMemo(()=>resolveRoutes(getClientConfig().routes).admin, []);
23
+ const adminRoute = getClientConfig().routes.admin;
24
24
  const $updateLinkEditor = useCallback(()=>{
25
25
  const selection = $getSelection();
26
26
  if ($isRangeSelection(selection)) {
@@ -1,11 +1,11 @@
1
- import { getClientConfig, resolveRoutes } from "@byline/core";
1
+ import { getClientConfig } from "@byline/core";
2
2
  import { addClassNamesToElement, isHTMLAnchorElement } from "@lexical/utils";
3
3
  import { $applyNodeReplacement, $createTextNode, $getSelection, $isElementNode, $isRangeSelection, ElementNode, createCommand } from "lexical";
4
4
  import { sanitizeUrl } from "../../utils/url.js";
5
5
  function adminHref(attrs) {
6
6
  if (null == attrs || 'internal' !== attrs.linkType) return '#';
7
7
  const internal = attrs;
8
- const { admin } = resolveRoutes(getClientConfig().routes);
8
+ const { admin } = getClientConfig().routes;
9
9
  return `${admin}/collections/${internal.targetCollectionPath}/${internal.targetDocumentId}`;
10
10
  }
11
11
  class LinkNode extends ElementNode {
@@ -15,8 +15,7 @@
15
15
  * Pure / framework-agnostic. No React, no DOM, no Lexical runtime — safe
16
16
  * to import from the package's `server` entry.
17
17
  */
18
- import type { BylineClient } from '@byline/client';
19
- import type { ReadContext } from '@byline/core';
18
+ import type { ReadContext, RichTextReadDocumentsFn } from '@byline/core';
20
19
  /**
21
20
  * Lexical-shaped node — only the fields the visitors need to read or
22
21
  * mutate. Carries an unknown `children` array because nodes nest
@@ -82,8 +81,8 @@ export interface LexicalNodeVisitor {
82
81
  match: (node: LexicalNodeLike) => PendingHydration | null;
83
82
  }
84
83
  interface RunPopulateOptions {
85
- client: BylineClient;
86
84
  readContext: ReadContext;
85
+ readDocuments: RichTextReadDocumentsFn;
87
86
  visitors: LexicalNodeVisitor[];
88
87
  /**
89
88
  * One or more rich-text values to walk. The framework's read pipeline
@@ -99,13 +98,9 @@ interface RunPopulateOptions {
99
98
  * hydrations across all visitors, batch-fetch by source collection, and
100
99
  * apply.
101
100
  *
102
- * `readContext` is accepted (factories thread it through from
103
- * `RichTextPopulateContext` / `RichTextEmbedContext`) but currently unused
104
- * the batch fetch goes straight to `getDocumentsByDocumentIds` and doesn't
105
- * recurse into populate or `afterRead`, so there's no visited-set or
106
- * read-budget state to share. Retained on the options shape so a future
107
- * visitor that performs nested populate can opt back in without another
108
- * contract churn.
101
+ * Callers provide the framework-owned `readDocuments` function, which enforces
102
+ * target abilities and `beforeRead` predicates for both read-time populate and
103
+ * write-time embed refreshes.
109
104
  */
110
105
  export declare function runLexicalPopulate(options: RunPopulateOptions): Promise<void>;
111
106
  export {};
@@ -17,7 +17,7 @@ function* iterAllNodes(node) {
17
17
  if (Array.isArray(node.children)) for (const child of node.children)yield* iterAllNodes(child);
18
18
  }
19
19
  async function runLexicalPopulate(options) {
20
- const { client, visitors, values } = options;
20
+ const { visitors, values, readDocuments } = options;
21
21
  const pending = [];
22
22
  for (const value of values){
23
23
  const root = getLexicalRoot(value);
@@ -39,11 +39,9 @@ async function runLexicalPopulate(options) {
39
39
  const fetched = new Map();
40
40
  await Promise.all(Array.from(idsByCollection.entries()).map(async ([collectionPath, idSet])=>{
41
41
  const ids = Array.from(idSet);
42
- const collectionId = await client.resolveCollectionId(collectionPath);
43
- const rawDocs = await client.db.queries.documents.getDocumentsByDocumentIds({
44
- collection_id: collectionId,
45
- document_ids: ids,
46
- readMode: 'published'
42
+ const rawDocs = await readDocuments({
43
+ collectionPath,
44
+ documentIds: ids
47
45
  });
48
46
  const byId = new Map();
49
47
  for (const raw of rawDocs)if ('string' == typeof raw.document_id) byId.set(raw.document_id, {
package/dist/server.js CHANGED
@@ -10,8 +10,8 @@ function lexicalEditorPopulateServer(options) {
10
10
  ];
11
11
  return async (ctx)=>{
12
12
  await runLexicalPopulate({
13
- client: options.getClient(),
14
13
  readContext: ctx.readContext,
14
+ readDocuments: ctx.readDocuments,
15
15
  visitors,
16
16
  values: [
17
17
  ctx.value
@@ -32,8 +32,8 @@ function lexicalEditorEmbedServer(options) {
32
32
  ];
33
33
  return async (ctx)=>{
34
34
  await runLexicalPopulate({
35
- client: options.getClient(),
36
35
  readContext: ctx.readContext,
36
+ readDocuments: ctx.readDocuments,
37
37
  visitors,
38
38
  values: [
39
39
  ctx.value
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "3.21.0",
6
+ "version": "4.0.0",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -73,10 +73,10 @@
73
73
  "lexical": "0.46.0",
74
74
  "npm-run-all": "^4.1.5",
75
75
  "react-error-boundary": "^6.1.2",
76
- "@byline/admin": "3.21.0",
77
- "@byline/core": "3.21.0",
78
- "@byline/client": "3.21.0",
79
- "@byline/ui": "3.21.0"
76
+ "@byline/admin": "4.0.0",
77
+ "@byline/core": "4.0.0",
78
+ "@byline/ui": "4.0.0",
79
+ "@byline/client": "4.0.0"
80
80
  },
81
81
  "peerDependencies": {
82
82
  "react": "^19.0.0",
@@ -11,9 +11,9 @@
11
11
  */
12
12
 
13
13
  import type * as React from 'react'
14
- import { type Dispatch, useCallback, useEffect, useMemo, useRef, useState } from 'react'
14
+ import { type Dispatch, useCallback, useEffect, useRef, useState } from 'react'
15
15
 
16
- import { getClientConfig, resolveRoutes } from '@byline/core'
16
+ import { getClientConfig } from '@byline/core'
17
17
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
18
18
  import { $findMatchingParent, mergeRegister } from '@lexical/utils'
19
19
  import {
@@ -70,7 +70,7 @@ function FloatingLinkEditor({
70
70
  })
71
71
  const [linkModalData, setLinkModalData] = useState<LinkData | undefined>(undefined)
72
72
  const [modalOpen, setModalOpen] = useState(false)
73
- const adminRoute = useMemo(() => resolveRoutes(getClientConfig().routes).admin, [])
73
+ const adminRoute = getClientConfig().routes.admin
74
74
 
75
75
  const $updateLinkEditor = useCallback(() => {
76
76
  const selection = $getSelection()
@@ -6,7 +6,7 @@
6
6
  *
7
7
  */
8
8
 
9
- import { getClientConfig, resolveRoutes } from '@byline/core'
9
+ import { getClientConfig } from '@byline/core'
10
10
  import { addClassNamesToElement, isHTMLAnchorElement } from '@lexical/utils'
11
11
  import {
12
12
  $applyNodeReplacement,
@@ -33,7 +33,7 @@ import type { InternalLinkAttributes, LinkAttributes, SerializedLinkNode } from
33
33
  function adminHref(attrs: LinkAttributes | null | undefined): string {
34
34
  if (attrs == null || attrs.linkType !== 'internal') return '#'
35
35
  const internal = attrs as InternalLinkAttributes
36
- const { admin } = resolveRoutes(getClientConfig().routes)
36
+ const { admin } = getClientConfig().routes
37
37
  return `${admin}/collections/${internal.targetCollectionPath}/${internal.targetDocumentId}`
38
38
  }
39
39
 
@@ -0,0 +1,34 @@
1
+ import { createReadContext } from '@byline/core'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import { type LexicalNodeVisitor, runLexicalPopulate } from './lexical-populate-shared.js'
5
+
6
+ describe('runLexicalPopulate secure reader', () => {
7
+ it('uses the framework reader instead of direct adapter access', async () => {
8
+ const apply = vi.fn()
9
+ const visitor: LexicalNodeVisitor = {
10
+ match: (node) =>
11
+ node.type === 'target' ? { node, collectionPath: 'media', documentId: 'm1', apply } : null,
12
+ }
13
+ const readDocuments = vi
14
+ .fn()
15
+ .mockResolvedValue([
16
+ { document_id: 'm1', path: 'asset', status: 'published', fields: { title: 'Asset' } },
17
+ ])
18
+
19
+ await runLexicalPopulate({
20
+ readContext: createReadContext(),
21
+ readDocuments,
22
+ visitors: [visitor],
23
+ values: [{ root: { type: 'root', children: [{ type: 'target' }] } }],
24
+ })
25
+
26
+ expect(readDocuments).toHaveBeenCalledWith({
27
+ collectionPath: 'media',
28
+ documentIds: ['m1'],
29
+ })
30
+ expect(apply).toHaveBeenCalledWith(
31
+ expect.objectContaining({ id: 'm1', fields: { title: 'Asset' } })
32
+ )
33
+ })
34
+ })
@@ -17,8 +17,7 @@
17
17
  * to import from the package's `server` entry.
18
18
  */
19
19
 
20
- import type { BylineClient } from '@byline/client'
21
- import type { ReadContext } from '@byline/core'
20
+ import type { ReadContext, RichTextReadDocumentsFn } from '@byline/core'
22
21
 
23
22
  /**
24
23
  * Lexical-shaped node — only the fields the visitors need to read or
@@ -124,8 +123,8 @@ export interface LexicalNodeVisitor {
124
123
  }
125
124
 
126
125
  interface RunPopulateOptions {
127
- client: BylineClient
128
126
  readContext: ReadContext
127
+ readDocuments: RichTextReadDocumentsFn
129
128
  visitors: LexicalNodeVisitor[]
130
129
  /**
131
130
  * One or more rich-text values to walk. The framework's read pipeline
@@ -142,16 +141,12 @@ interface RunPopulateOptions {
142
141
  * hydrations across all visitors, batch-fetch by source collection, and
143
142
  * apply.
144
143
  *
145
- * `readContext` is accepted (factories thread it through from
146
- * `RichTextPopulateContext` / `RichTextEmbedContext`) but currently unused
147
- * the batch fetch goes straight to `getDocumentsByDocumentIds` and doesn't
148
- * recurse into populate or `afterRead`, so there's no visited-set or
149
- * read-budget state to share. Retained on the options shape so a future
150
- * visitor that performs nested populate can opt back in without another
151
- * contract churn.
144
+ * Callers provide the framework-owned `readDocuments` function, which enforces
145
+ * target abilities and `beforeRead` predicates for both read-time populate and
146
+ * write-time embed refreshes.
152
147
  */
153
148
  export async function runLexicalPopulate(options: RunPopulateOptions): Promise<void> {
154
- const { client, visitors, values } = options
149
+ const { visitors, values, readDocuments } = options
155
150
 
156
151
  const pending: PendingHydration[] = []
157
152
  for (const value of values) {
@@ -178,21 +173,11 @@ export async function runLexicalPopulate(options: RunPopulateOptions): Promise<v
178
173
  bucket.add(p.documentId)
179
174
  }
180
175
 
181
- // Fetch directly through the adapter rather than the client's `find()` —
182
- // `parseWhere` has no handler for `id`, so `find({ where: { id: { $in } } })`
183
- // silently dropped the filter and returned arbitrary docs ordered by
184
- // `created_at desc`. This is the same primitive relation populate uses
185
- // (`packages/core/src/services/populate.ts`) when it batches by document id.
186
176
  const fetched = new Map<string, Map<string, Record<string, any>>>()
187
177
  await Promise.all(
188
178
  Array.from(idsByCollection.entries()).map(async ([collectionPath, idSet]) => {
189
179
  const ids = Array.from(idSet)
190
- const collectionId = await client.resolveCollectionId(collectionPath)
191
- const rawDocs = await client.db.queries.documents.getDocumentsByDocumentIds({
192
- collection_id: collectionId,
193
- document_ids: ids,
194
- readMode: 'published',
195
- })
180
+ const rawDocs = await readDocuments({ collectionPath, documentIds: ids })
196
181
  const byId = new Map<string, Record<string, any>>()
197
182
  for (const raw of rawDocs as Array<Record<string, any>>) {
198
183
  if (typeof raw.document_id !== 'string') continue
package/src/server.ts CHANGED
@@ -115,8 +115,8 @@ export function lexicalEditorPopulateServer(options: LexicalServerOptions): Rich
115
115
  const visitors = options.visitors ?? [inlineImageVisitor, linkVisitor]
116
116
  return async (ctx: RichTextPopulateContext): Promise<void> => {
117
117
  await runLexicalPopulate({
118
- client: options.getClient(),
119
118
  readContext: ctx.readContext,
119
+ readDocuments: ctx.readDocuments,
120
120
  visitors,
121
121
  values: [ctx.value],
122
122
  })
@@ -165,8 +165,8 @@ export function lexicalEditorEmbedServer(options: LexicalServerOptions): RichTex
165
165
  const visitors = options.visitors ?? [inlineImageVisitor, linkVisitor]
166
166
  return async (ctx: RichTextEmbedContext): Promise<void> => {
167
167
  await runLexicalPopulate({
168
- client: options.getClient(),
169
168
  readContext: ctx.readContext,
169
+ readDocuments: ctx.readDocuments,
170
170
  visitors,
171
171
  values: [ctx.value],
172
172
  })