@payload-onpage/core 0.2.0 → 0.3.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/dist/autosave.d.ts +2 -2
- package/dist/autosave.d.ts.map +1 -1
- package/dist/autosave.js +2 -1
- package/dist/autosave.js.map +1 -1
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -0
- package/dist/constants.js.map +1 -1
- package/dist/docref.d.ts +18 -3
- package/dist/docref.d.ts.map +1 -1
- package/dist/docref.js +13 -4
- package/dist/docref.js.map +1 -1
- package/dist/encode.d.ts +16 -4
- package/dist/encode.d.ts.map +1 -1
- package/dist/encode.js +42 -62
- package/dist/encode.js.map +1 -1
- package/dist/fields.d.ts +39 -8
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +111 -97
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +16 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -12
- package/dist/index.js.map +1 -1
- package/dist/path.d.ts +9 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +27 -8
- package/dist/path.js.map +1 -1
- package/dist/payload.d.ts +5 -3
- package/dist/payload.d.ts.map +1 -1
- package/dist/payload.js +34 -23
- package/dist/payload.js.map +1 -1
- package/dist/ports.d.ts +43 -28
- package/dist/ports.d.ts.map +1 -1
- package/dist/ports.js +11 -1
- package/dist/ports.js.map +1 -1
- package/dist/schema.d.ts +71 -26
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -6
- package/dist/schema.js.map +1 -1
- package/dist/seal.d.ts +17 -0
- package/dist/seal.d.ts.map +1 -1
- package/dist/seal.js +13 -0
- package/dist/seal.js.map +1 -1
- package/dist/session.d.ts +34 -18
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +20 -13
- package/dist/session.js.map +1 -1
- package/dist/stega.d.ts +6 -2
- package/dist/stega.d.ts.map +1 -1
- package/dist/stega.js +18 -13
- package/dist/stega.js.map +1 -1
- package/dist/util.d.ts +9 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +11 -0
- package/dist/util.js.map +1 -0
- package/package.json +1 -1
- package/src/autosave.ts +4 -3
- package/src/constants.ts +16 -0
- package/src/docref.ts +26 -6
- package/src/encode.ts +47 -55
- package/src/fields.ts +166 -98
- package/src/index.ts +125 -12
- package/src/path.ts +32 -10
- package/src/payload.ts +54 -26
- package/src/ports.ts +50 -22
- package/src/schema.ts +100 -17
- package/src/seal.ts +31 -0
- package/src/session.ts +67 -47
- package/src/stega.ts +25 -17
- package/src/util.ts +12 -0
package/src/encode.ts
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* maps an image). Never tagged: selects, numbers, dates, relationships, JSON,
|
|
10
10
|
* code, and any field whose name suggests machine use (url, slug, email…).
|
|
11
11
|
*/
|
|
12
|
+
import type { RefKey } from './docref.ts'
|
|
12
13
|
import { stegaEncode, type Marker } from './stega.ts'
|
|
13
14
|
import type { SchemaField } from './schema.ts'
|
|
14
|
-
import {
|
|
15
|
+
import { mapFields, type FieldVisitor } from './fields.ts'
|
|
15
16
|
import { mapLexical } from './lexical.ts'
|
|
17
|
+
import { assertNever } from './util.ts'
|
|
16
18
|
|
|
17
19
|
/** Name segments that mark a field as holding a machine value (never marked). */
|
|
18
20
|
const MACHINE_WORDS = new Set([
|
|
@@ -49,11 +51,11 @@ export function isMachineFieldName(name: string): boolean {
|
|
|
49
51
|
/** Field editors that edit text: a field using any other editor (color, …) holds a machine value and is never marked. */
|
|
50
52
|
const TEXT_EDITORS = new Set(['text', 'textarea', 'email', 'richText'])
|
|
51
53
|
|
|
52
|
-
export type EncodeFilter = (args: { field: SchemaField; path: string; ref:
|
|
54
|
+
export type EncodeFilter = (args: { field: SchemaField; path: string; ref: RefKey }) => boolean
|
|
53
55
|
|
|
54
56
|
interface Ctx {
|
|
55
|
-
ref:
|
|
56
|
-
filter?: EncodeFilter
|
|
57
|
+
ref: RefKey
|
|
58
|
+
filter?: EncodeFilter | undefined
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
const allowed = (field: SchemaField, path: string, ctx: Ctx) =>
|
|
@@ -72,62 +74,52 @@ function encodeRichText(value: unknown, ctx: Ctx, path: string): unknown {
|
|
|
72
74
|
)
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
/** What each leaf field kind gets: text and rich text are marked, a populated upload's `alt` carries its path. */
|
|
78
|
+
const encoding = (ctx: Ctx): FieldVisitor => ({
|
|
79
|
+
leaf: (value, field, path) => {
|
|
80
|
+
switch (field.type) {
|
|
81
|
+
case 'text':
|
|
82
|
+
case 'textarea':
|
|
83
|
+
return typeof value === 'string' && value && allowed(field, path, ctx)
|
|
84
|
+
? mark(value, ctx, path, field.type)
|
|
85
|
+
: value
|
|
86
|
+
case 'richText':
|
|
87
|
+
return allowed(field, path, ctx) ? encodeRichText(value, ctx, path) : value
|
|
88
|
+
case 'upload':
|
|
89
|
+
if (!field.hasMany && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
90
|
+
const media = value as Record<string, unknown>
|
|
91
|
+
return {
|
|
92
|
+
...media,
|
|
93
|
+
alt: mark(typeof media.alt === 'string' ? media.alt : '', ctx, path, 'media'),
|
|
94
|
+
}
|
|
91
95
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return value
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function encodeFields(data: unknown, fields: SchemaField[], prefix: string, ctx: Ctx): unknown {
|
|
115
|
-
if (!data || typeof data !== 'object' || Array.isArray(data)) return data
|
|
116
|
-
const out: Record<string, unknown> = { ...(data as Record<string, unknown>) }
|
|
117
|
-
for (const f of fields) {
|
|
118
|
-
if (!(f.name in out)) continue
|
|
119
|
-
out[f.name] = encodeValue(out[f.name], f, prefix ? `${prefix}.${f.name}` : f.name, ctx)
|
|
120
|
-
}
|
|
121
|
-
return out
|
|
122
|
-
}
|
|
96
|
+
return value
|
|
97
|
+
// Machine values: never marked.
|
|
98
|
+
case 'email':
|
|
99
|
+
case 'code':
|
|
100
|
+
case 'json':
|
|
101
|
+
case 'checkbox':
|
|
102
|
+
case 'point':
|
|
103
|
+
case 'number':
|
|
104
|
+
case 'date':
|
|
105
|
+
case 'select':
|
|
106
|
+
case 'radio':
|
|
107
|
+
case 'relationship':
|
|
108
|
+
return value
|
|
109
|
+
default:
|
|
110
|
+
return assertNever(field, 'field type')
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
})
|
|
123
114
|
|
|
124
115
|
/** Return a copy of `doc` with markers on the given (editable) top-level fields. */
|
|
125
116
|
export function encodeDocument<T extends Record<string, unknown>>(
|
|
126
117
|
doc: T,
|
|
127
118
|
fields: SchemaField[],
|
|
128
|
-
ref:
|
|
129
|
-
opts: { only?: string[]; filter?: EncodeFilter } = {},
|
|
119
|
+
ref: RefKey,
|
|
120
|
+
opts: { only?: string[] | undefined; filter?: EncodeFilter | undefined } = {},
|
|
130
121
|
): T {
|
|
131
|
-
const
|
|
132
|
-
|
|
122
|
+
const { only } = opts
|
|
123
|
+
const scope = only ? fields.filter((f) => only.includes(f.name)) : fields
|
|
124
|
+
return mapFields(doc, scope, encoding({ ref, filter: opts.filter })) as T
|
|
133
125
|
}
|
package/src/fields.ts
CHANGED
|
@@ -3,21 +3,102 @@
|
|
|
3
3
|
* new blocks/rows from defaults, clone rows with fresh ids, and normalize data
|
|
4
4
|
* for saving (populated relations back to ids).
|
|
5
5
|
*/
|
|
6
|
-
import { getAt, newRowId, parsePath, type PathSegment } from './path.ts'
|
|
6
|
+
import { getAt, newRowId, parsePath, splitItemPath, type PathSegment } from './path.ts'
|
|
7
7
|
import { mapLexical } from './lexical.ts'
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
ArrayField,
|
|
10
|
+
BlocksField,
|
|
11
|
+
DocumentSchema,
|
|
12
|
+
GroupField,
|
|
13
|
+
ListField,
|
|
14
|
+
RelationField,
|
|
15
|
+
SchemaBlock,
|
|
16
|
+
SchemaField,
|
|
17
|
+
} from './schema.ts'
|
|
18
|
+
import { isListField } from './schema.ts'
|
|
19
|
+
import { assertNever } from './util.ts'
|
|
9
20
|
|
|
10
21
|
export interface FieldAtPath {
|
|
11
22
|
field: SchemaField
|
|
12
23
|
/** The block whose fields contain this field, when inside a blocks row. */
|
|
13
|
-
block?: SchemaBlock
|
|
24
|
+
block?: SchemaBlock | undefined
|
|
14
25
|
/** Path of the nearest blocks row ("sections.3"), when inside one. */
|
|
15
|
-
blockPath?: string
|
|
26
|
+
blockPath?: string | undefined
|
|
16
27
|
}
|
|
17
28
|
|
|
29
|
+
type Data = Record<string, unknown>
|
|
30
|
+
|
|
31
|
+
const isData = (value: unknown): value is Data =>
|
|
32
|
+
!!value && typeof value === 'object' && !Array.isArray(value)
|
|
33
|
+
|
|
18
34
|
/** The block a blocks-field row uses (by its `blockType`). */
|
|
19
|
-
export const blockFor = (field:
|
|
20
|
-
field.blocks
|
|
35
|
+
export const blockFor = (field: BlocksField, row: unknown): SchemaBlock | undefined =>
|
|
36
|
+
isData(row) ? field.blocks.find((b) => b.slug === row.blockType) : undefined
|
|
37
|
+
|
|
38
|
+
/** The fields of one row of a list field: the block's fields, or the array's. */
|
|
39
|
+
export const rowFieldsOf = (field: ListField, row: unknown): SchemaField[] | undefined =>
|
|
40
|
+
field.type === 'blocks' ? blockFor(field, row)?.fields : field.fields
|
|
41
|
+
|
|
42
|
+
/** A field that holds other fields' data. */
|
|
43
|
+
type Container = GroupField | ArrayField | BlocksField
|
|
44
|
+
/** Any other field: its value is handled as one unit. */
|
|
45
|
+
export type LeafField = Exclude<SchemaField, Container>
|
|
46
|
+
|
|
47
|
+
export interface FieldVisitor {
|
|
48
|
+
/** Every field that isn't a group, array or blocks field. Return the value unchanged to keep it. */
|
|
49
|
+
leaf: (value: unknown, field: LeafField, path: string) => unknown
|
|
50
|
+
/** Each array/blocks row (a copy), before its fields are visited: e.g. give it a new id. */
|
|
51
|
+
row?: (row: Data, path: string) => Data
|
|
52
|
+
/** A blocks row with an unknown `blockType`. Default: kept as is. */
|
|
53
|
+
unknownBlock?: (row: unknown) => unknown
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Rebuild document data field by field. Groups, array rows and block rows are
|
|
58
|
+
* walked here, once, for every schema-aware transform (cloning, normalizing for
|
|
59
|
+
* a save, encoding markers); `visit` only says what to do with leaf fields.
|
|
60
|
+
* Values of fields missing from `data` stay missing; untouched values are shared.
|
|
61
|
+
*/
|
|
62
|
+
export function mapFields(
|
|
63
|
+
data: unknown,
|
|
64
|
+
fields: SchemaField[],
|
|
65
|
+
visit: FieldVisitor,
|
|
66
|
+
prefix = '',
|
|
67
|
+
): unknown {
|
|
68
|
+
if (!isData(data)) return data
|
|
69
|
+
const out: Data = { ...data }
|
|
70
|
+
for (const f of fields)
|
|
71
|
+
if (f.name in out)
|
|
72
|
+
out[f.name] = mapField(out[f.name], f, visit, prefix ? `${prefix}.${f.name}` : f.name)
|
|
73
|
+
return out
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One field's value (see `mapFields`). */
|
|
77
|
+
export function mapField(
|
|
78
|
+
value: unknown,
|
|
79
|
+
field: SchemaField,
|
|
80
|
+
visit: FieldVisitor,
|
|
81
|
+
path: string,
|
|
82
|
+
): unknown {
|
|
83
|
+
if (value == null) return value
|
|
84
|
+
switch (field.type) {
|
|
85
|
+
case 'group':
|
|
86
|
+
return mapFields(value, field.fields, visit, path)
|
|
87
|
+
case 'array':
|
|
88
|
+
case 'blocks':
|
|
89
|
+
return Array.isArray(value)
|
|
90
|
+
? value.map((row: unknown, i) => {
|
|
91
|
+
const fields = rowFieldsOf(field, row)
|
|
92
|
+
if (!fields) return visit.unknownBlock ? visit.unknownBlock(row) : row
|
|
93
|
+
const rowPath = `${path}.${i}`
|
|
94
|
+
const base = isData(row) && visit.row ? visit.row({ ...row }, rowPath) : row
|
|
95
|
+
return mapFields(base, fields, visit, rowPath)
|
|
96
|
+
})
|
|
97
|
+
: value
|
|
98
|
+
default:
|
|
99
|
+
return visit.leaf(value, field, path)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
21
102
|
|
|
22
103
|
/** May the editor change this path? (Its top-level field must be editable.) */
|
|
23
104
|
export const isEditablePath = (schema: DocumentSchema, path: string): boolean =>
|
|
@@ -30,7 +111,7 @@ export interface RowInfo {
|
|
|
30
111
|
listPath: string
|
|
31
112
|
index: number
|
|
32
113
|
length: number
|
|
33
|
-
listField:
|
|
114
|
+
listField: ListField
|
|
34
115
|
/** Fields of this row: the block's fields, or the array's. */
|
|
35
116
|
rowFields: SchemaField[]
|
|
36
117
|
/** Singular label of one row ("Hero", "Feature"). */
|
|
@@ -40,15 +121,13 @@ export interface RowInfo {
|
|
|
40
121
|
|
|
41
122
|
/** Everything the editor needs about one row of an array or blocks field, or null if `path` isn't an editable row. */
|
|
42
123
|
export function resolveRow(schema: DocumentSchema, doc: unknown, path: string): RowInfo | null {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const listPath = segs.slice(0, -1).join('.')
|
|
47
|
-
const index = Number(last)
|
|
124
|
+
const split = splitItemPath(path)
|
|
125
|
+
if (!split || !isEditablePath(schema, path)) return null
|
|
126
|
+
const [listPath, index] = split
|
|
48
127
|
const listField = fieldAtPath(schema.fields, doc, listPath)?.field
|
|
49
|
-
if (!listField || (listField
|
|
50
|
-
const rows = getAt
|
|
51
|
-
if (index >= rows.length) return null
|
|
128
|
+
if (!listField || !isListField(listField)) return null
|
|
129
|
+
const rows = getAt(doc, listPath)
|
|
130
|
+
if (!Array.isArray(rows) || index >= rows.length) return null
|
|
52
131
|
const isBlock = listField.type === 'blocks'
|
|
53
132
|
const block = isBlock ? blockFor(listField, rows[index]) : undefined
|
|
54
133
|
return {
|
|
@@ -57,10 +136,10 @@ export function resolveRow(schema: DocumentSchema, doc: unknown, path: string):
|
|
|
57
136
|
index,
|
|
58
137
|
length: rows.length,
|
|
59
138
|
listField,
|
|
60
|
-
rowFields:
|
|
61
|
-
label:
|
|
62
|
-
|
|
63
|
-
: (listField.labels?.singular ?? listField.label),
|
|
139
|
+
rowFields: rowFieldsOf(listField, rows[index]) ?? [],
|
|
140
|
+
label:
|
|
141
|
+
block?.labels.singular ??
|
|
142
|
+
(isBlock ? 'Section' : (listField.labels?.singular ?? listField.label)),
|
|
64
143
|
isBlock,
|
|
65
144
|
}
|
|
66
145
|
}
|
|
@@ -80,85 +159,63 @@ export function fieldAtPath(
|
|
|
80
159
|
let current: SchemaField | undefined
|
|
81
160
|
let block: SchemaBlock | undefined
|
|
82
161
|
let blockPath: string | undefined
|
|
83
|
-
for (
|
|
84
|
-
const seg = segs[i]
|
|
162
|
+
for (const [i, seg] of segs.entries()) {
|
|
85
163
|
if (typeof seg === 'number') {
|
|
86
|
-
if (!current) return null
|
|
164
|
+
if (!current || !isListField(current)) return null
|
|
87
165
|
if (current.type === 'blocks') {
|
|
88
166
|
const rowPath = segs.slice(0, i + 1)
|
|
89
167
|
block = blockFor(current, getAt(doc, rowPath))
|
|
90
168
|
if (!block) return null
|
|
91
169
|
blockPath = rowPath.join('.')
|
|
92
170
|
level = block.fields
|
|
93
|
-
} else if (current.type === 'array') {
|
|
94
|
-
level = current.fields ?? []
|
|
95
171
|
} else {
|
|
96
|
-
|
|
172
|
+
level = current.fields
|
|
97
173
|
}
|
|
98
174
|
continue
|
|
99
175
|
}
|
|
100
176
|
current = level.find((f) => f.name === seg)
|
|
101
177
|
if (!current) return null
|
|
102
|
-
if (current.type === 'group') level = current.fields
|
|
178
|
+
if (current.type === 'group') level = current.fields
|
|
103
179
|
}
|
|
104
180
|
return current ? { field: current, block, blockPath } : null
|
|
105
181
|
}
|
|
106
182
|
|
|
107
183
|
/** Defaults for a set of fields: static `defaultValue`s, nested groups, `minRows` rows. */
|
|
108
|
-
export function defaultsFor(fields: SchemaField[]):
|
|
109
|
-
const out:
|
|
184
|
+
export function defaultsFor(fields: SchemaField[]): Data {
|
|
185
|
+
const out: Data = {}
|
|
110
186
|
for (const f of fields) {
|
|
111
|
-
if (f.defaultValue !== undefined)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
out[f.name] = defaultsFor(f.fields ?? [])
|
|
115
|
-
} else if (f.type === 'array') {
|
|
187
|
+
if (f.defaultValue !== undefined) out[f.name] = structuredClone(f.defaultValue)
|
|
188
|
+
else if (f.type === 'group') out[f.name] = defaultsFor(f.fields)
|
|
189
|
+
else if (f.type === 'array')
|
|
116
190
|
out[f.name] = Array.from({ length: f.minRows ?? 0 }, () => newArrayRow(f))
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
} else if (f.required && (f.type === 'text' || f.type === 'textarea')) {
|
|
120
|
-
out[f.name] = f.label
|
|
121
|
-
}
|
|
191
|
+
else if (f.type === 'blocks') out[f.name] = []
|
|
192
|
+
else if (f.required && (f.type === 'text' || f.type === 'textarea')) out[f.name] = f.label
|
|
122
193
|
}
|
|
123
194
|
return out
|
|
124
195
|
}
|
|
125
196
|
|
|
126
|
-
export function newArrayRow(arrayField:
|
|
127
|
-
return { id: newRowId(), ...defaultsFor(arrayField.fields
|
|
197
|
+
export function newArrayRow(arrayField: ArrayField): Data {
|
|
198
|
+
return { id: newRowId(), ...defaultsFor(arrayField.fields) }
|
|
128
199
|
}
|
|
129
200
|
|
|
130
|
-
export function newBlockRow(block: SchemaBlock):
|
|
201
|
+
export function newBlockRow(block: SchemaBlock): Data {
|
|
131
202
|
return { id: newRowId(), blockType: block.slug, ...defaultsFor(block.fields) }
|
|
132
203
|
}
|
|
133
204
|
|
|
205
|
+
/** Cloning: every row gets a new id; related documents' ids are kept; other objects are deep-copied. */
|
|
206
|
+
const cloning: FieldVisitor = {
|
|
207
|
+
leaf: (value) => (typeof value === 'object' ? structuredClone(value) : value),
|
|
208
|
+
row: (row) => ({ ...row, id: newRowId() }),
|
|
209
|
+
unknownBlock: (row) => structuredClone(row),
|
|
210
|
+
}
|
|
211
|
+
|
|
134
212
|
/**
|
|
135
213
|
* Deep-clone a row (array item or block) giving it, and every nested row, a new
|
|
136
214
|
* id. Ids of related documents (uploads, relationships) are left alone, so a
|
|
137
215
|
* duplicated card still points at the same image.
|
|
138
216
|
*/
|
|
139
217
|
export function cloneRow(row: unknown, rowFields: SchemaField[]): unknown {
|
|
140
|
-
|
|
141
|
-
const copy: Record<string, unknown> = { ...(row as Record<string, unknown>), id: newRowId() }
|
|
142
|
-
for (const f of rowFields) copy[f.name] = cloneValue(copy[f.name], f)
|
|
143
|
-
return copy
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function cloneValue(value: unknown, field: SchemaField): unknown {
|
|
147
|
-
if (value == null) return value
|
|
148
|
-
if (field.type === 'group') {
|
|
149
|
-
const out: Record<string, unknown> = { ...(value as Record<string, unknown>) }
|
|
150
|
-
for (const f of field.fields ?? []) out[f.name] = cloneValue(out[f.name], f)
|
|
151
|
-
return out
|
|
152
|
-
}
|
|
153
|
-
if (field.type === 'array' && Array.isArray(value))
|
|
154
|
-
return (value as unknown[]).map((r: unknown) => cloneRow(r, field.fields ?? []))
|
|
155
|
-
if (field.type === 'blocks' && Array.isArray(value)) {
|
|
156
|
-
return (value as unknown[]).map((r: unknown) => {
|
|
157
|
-
const block = blockFor(field, r)
|
|
158
|
-
return block ? cloneRow(r, block.fields) : structuredClone(r)
|
|
159
|
-
})
|
|
160
|
-
}
|
|
161
|
-
return typeof value === 'object' ? structuredClone(value) : value
|
|
218
|
+
return isData(row) ? mapFields({ ...row, id: newRowId() }, rowFields, cloning) : row
|
|
162
219
|
}
|
|
163
220
|
|
|
164
221
|
/** The id of a relationship value: a populated doc, `{ relationTo, value }`, or an id. */
|
|
@@ -171,6 +228,23 @@ export function relationId(v: unknown): string | number | undefined {
|
|
|
171
228
|
return o.id as string | number | undefined
|
|
172
229
|
}
|
|
173
230
|
|
|
231
|
+
/**
|
|
232
|
+
* A field value as a list: a hasMany value as its items, a single value as a
|
|
233
|
+
* one-item list, nothing as []. `item` keeps or converts each entry (return
|
|
234
|
+
* undefined to drop it).
|
|
235
|
+
*/
|
|
236
|
+
export function asList<T = unknown>(
|
|
237
|
+
value: unknown,
|
|
238
|
+
many: boolean | undefined,
|
|
239
|
+
item: (v: unknown) => T | undefined = (v) => v as T,
|
|
240
|
+
): T[] {
|
|
241
|
+
const raw = many ? (Array.isArray(value) ? value : []) : value == null ? [] : [value]
|
|
242
|
+
return raw.flatMap((v) => {
|
|
243
|
+
const out = item(v)
|
|
244
|
+
return out === undefined ? [] : [out]
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
174
248
|
/** A display title for a relationship value (`titleField` when known, else common title fields, else the id). */
|
|
175
249
|
export function relationTitle(v: unknown, titleField?: string): string {
|
|
176
250
|
if (v && typeof v === 'object') {
|
|
@@ -185,7 +259,7 @@ export function relationTitle(v: unknown, titleField?: string): string {
|
|
|
185
259
|
|
|
186
260
|
const idOf = (v: unknown): unknown => (v && typeof v === 'object' && 'id' in v ? relationId(v) : v)
|
|
187
261
|
|
|
188
|
-
function normalizeRelation(value: unknown, field:
|
|
262
|
+
function normalizeRelation(value: unknown, field: RelationField): unknown {
|
|
189
263
|
if (value == null) return value
|
|
190
264
|
const one = (v: unknown) => {
|
|
191
265
|
// Polymorphic: { relationTo, value }
|
|
@@ -198,38 +272,39 @@ function normalizeRelation(value: unknown, field: SchemaField): unknown {
|
|
|
198
272
|
return Array.isArray(value) ? value.map(one) : one(value)
|
|
199
273
|
}
|
|
200
274
|
|
|
275
|
+
/** Normalizing for a save: populated relations become ids, rich text drops populated docs. */
|
|
276
|
+
const normalizing: FieldVisitor = {
|
|
277
|
+
leaf: (value, field) => {
|
|
278
|
+
switch (field.type) {
|
|
279
|
+
case 'upload':
|
|
280
|
+
case 'relationship':
|
|
281
|
+
return normalizeRelation(value, field)
|
|
282
|
+
case 'richText':
|
|
283
|
+
return normalizeRichText(value)
|
|
284
|
+
case 'text':
|
|
285
|
+
case 'textarea':
|
|
286
|
+
case 'email':
|
|
287
|
+
case 'code':
|
|
288
|
+
case 'json':
|
|
289
|
+
case 'checkbox':
|
|
290
|
+
case 'point':
|
|
291
|
+
case 'number':
|
|
292
|
+
case 'date':
|
|
293
|
+
case 'select':
|
|
294
|
+
case 'radio':
|
|
295
|
+
return value
|
|
296
|
+
default:
|
|
297
|
+
return assertNever(field, 'field type')
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
|
|
201
302
|
/**
|
|
202
303
|
* Turn rendered (populated) data back into what Payload expects on write:
|
|
203
304
|
* uploads and relationships become ids, everything else passes through.
|
|
204
305
|
*/
|
|
205
306
|
export function normalizeValue(value: unknown, field: SchemaField): unknown {
|
|
206
|
-
|
|
207
|
-
switch (field.type) {
|
|
208
|
-
case 'upload':
|
|
209
|
-
case 'relationship':
|
|
210
|
-
return normalizeRelation(value, field)
|
|
211
|
-
case 'richText':
|
|
212
|
-
return normalizeRichText(value)
|
|
213
|
-
case 'group': {
|
|
214
|
-
const out: Record<string, unknown> = { ...(value as Record<string, unknown>) }
|
|
215
|
-
for (const f of field.fields ?? [])
|
|
216
|
-
if (f.name in out) out[f.name] = normalizeValue(out[f.name], f)
|
|
217
|
-
return out
|
|
218
|
-
}
|
|
219
|
-
case 'array':
|
|
220
|
-
return Array.isArray(value)
|
|
221
|
-
? (value as unknown[]).map((row: unknown) => normalizeRow(row, field.fields ?? []))
|
|
222
|
-
: value
|
|
223
|
-
case 'blocks':
|
|
224
|
-
return Array.isArray(value)
|
|
225
|
-
? (value as unknown[]).map((row: unknown) => {
|
|
226
|
-
const block = blockFor(field, row)
|
|
227
|
-
return block ? normalizeRow(row, block.fields) : row
|
|
228
|
-
})
|
|
229
|
-
: value
|
|
230
|
-
default:
|
|
231
|
-
return value
|
|
232
|
-
}
|
|
307
|
+
return mapField(value, field, normalizing, field.name)
|
|
233
308
|
}
|
|
234
309
|
|
|
235
310
|
/**
|
|
@@ -258,13 +333,6 @@ export function normalizeRichText(value: unknown): unknown {
|
|
|
258
333
|
const isPopulated = (v: unknown): v is { id: string | number } =>
|
|
259
334
|
!!v && typeof v === 'object' && 'id' in v
|
|
260
335
|
|
|
261
|
-
function normalizeRow(row: unknown, rowFields: SchemaField[]): unknown {
|
|
262
|
-
if (!row || typeof row !== 'object') return row
|
|
263
|
-
const out: Record<string, unknown> = { ...(row as Record<string, unknown>) }
|
|
264
|
-
for (const f of rowFields) if (f.name in out) out[f.name] = normalizeValue(out[f.name], f)
|
|
265
|
-
return out
|
|
266
|
-
}
|
|
267
|
-
|
|
268
336
|
/** Normalize the given top-level keys of a document for a PATCH body. */
|
|
269
337
|
export function buildPatch(
|
|
270
338
|
doc: Record<string, unknown>,
|
package/src/index.ts
CHANGED
|
@@ -3,16 +3,129 @@
|
|
|
3
3
|
* schema, path and field operations, the stega codec, autosave, the editor
|
|
4
4
|
* session, and the port interfaces adapters implement. The default Payload REST
|
|
5
5
|
* adapters are in `@payload-onpage/core/payload`.
|
|
6
|
+
*
|
|
7
|
+
* The public API is listed explicitly: anything not exported here is internal
|
|
8
|
+
* and may change in any release.
|
|
6
9
|
*/
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
export {
|
|
11
|
+
isRefKey,
|
|
12
|
+
parseRefKey,
|
|
13
|
+
refKey,
|
|
14
|
+
targetKey,
|
|
15
|
+
type DocRef,
|
|
16
|
+
type RefKey,
|
|
17
|
+
type TargetKey,
|
|
18
|
+
} from './docref.ts'
|
|
19
|
+
export {
|
|
20
|
+
getAt,
|
|
21
|
+
insertItem,
|
|
22
|
+
moveItem,
|
|
23
|
+
newRowId,
|
|
24
|
+
parsePath,
|
|
25
|
+
pathnameOf,
|
|
26
|
+
removeItem,
|
|
27
|
+
rowPrefixes,
|
|
28
|
+
safeLinkUrl,
|
|
29
|
+
safePath,
|
|
30
|
+
samePath,
|
|
31
|
+
setAt,
|
|
32
|
+
splitItemPath,
|
|
33
|
+
updateArray,
|
|
34
|
+
type PathSegment,
|
|
35
|
+
} from './path.ts'
|
|
36
|
+
export {
|
|
37
|
+
isListField,
|
|
38
|
+
type ArrayField,
|
|
39
|
+
type BlocksField,
|
|
40
|
+
type ChoiceField,
|
|
41
|
+
type DateField,
|
|
42
|
+
type DocumentSchema,
|
|
43
|
+
type FieldOf,
|
|
44
|
+
type GroupField,
|
|
45
|
+
type ListField,
|
|
46
|
+
type NumberField,
|
|
47
|
+
type RelationField,
|
|
48
|
+
type ScalarField,
|
|
49
|
+
type ScalarFieldType,
|
|
50
|
+
type SchemaBlock,
|
|
51
|
+
type SchemaField,
|
|
52
|
+
type SchemaFieldType,
|
|
53
|
+
type SchemaLabels,
|
|
54
|
+
type SchemaOption,
|
|
55
|
+
} from './schema.ts'
|
|
56
|
+
export {
|
|
57
|
+
asList,
|
|
58
|
+
blockFor,
|
|
59
|
+
buildPatch,
|
|
60
|
+
changedKeys,
|
|
61
|
+
cloneRow,
|
|
62
|
+
defaultsFor,
|
|
63
|
+
fieldAtPath,
|
|
64
|
+
isEditablePath,
|
|
65
|
+
newArrayRow,
|
|
66
|
+
newBlockRow,
|
|
67
|
+
normalizeRichText,
|
|
68
|
+
normalizeValue,
|
|
69
|
+
relationId,
|
|
70
|
+
relationTitle,
|
|
71
|
+
resolveRow,
|
|
72
|
+
rowFieldsOf,
|
|
73
|
+
toText,
|
|
74
|
+
type FieldAtPath,
|
|
75
|
+
type RowInfo,
|
|
76
|
+
} from './fields.ts'
|
|
77
|
+
export { AutosaveQueue, type AutosaveOptions, type SaveState, type SaveStatus } from './autosave.ts'
|
|
78
|
+
export {
|
|
79
|
+
encodeMarker,
|
|
80
|
+
hasStegaMarker,
|
|
81
|
+
stegaClean,
|
|
82
|
+
stegaCleanDeep,
|
|
83
|
+
stegaDecode,
|
|
84
|
+
stegaEncode,
|
|
85
|
+
type Marker,
|
|
86
|
+
type MarkerKind,
|
|
87
|
+
} from './stega.ts'
|
|
88
|
+
export { encodeDocument, isMachineFieldName, type EncodeFilter } from './encode.ts'
|
|
89
|
+
export {
|
|
90
|
+
eachLexicalNode,
|
|
91
|
+
isLexicalState,
|
|
92
|
+
mapLexical,
|
|
93
|
+
type LexicalNode,
|
|
94
|
+
type LexicalState,
|
|
95
|
+
} from './lexical.ts'
|
|
96
|
+
export {
|
|
97
|
+
isOnPageMode,
|
|
98
|
+
isTargetKind,
|
|
99
|
+
type DataSource,
|
|
100
|
+
type Doc,
|
|
101
|
+
type Locator,
|
|
102
|
+
type MediaItem,
|
|
103
|
+
type MediaLibrary,
|
|
104
|
+
type OnPageMode,
|
|
105
|
+
type PreviewLink,
|
|
106
|
+
type Renderer,
|
|
107
|
+
type SaveResult,
|
|
108
|
+
type SearchHit,
|
|
109
|
+
type SessionAdapter,
|
|
110
|
+
type Target,
|
|
111
|
+
type TargetKind,
|
|
112
|
+
} from './ports.ts'
|
|
113
|
+
export {
|
|
114
|
+
EditorSession,
|
|
115
|
+
isReady,
|
|
116
|
+
type DocumentState,
|
|
117
|
+
type EditorSessionOptions,
|
|
118
|
+
type PublishResult,
|
|
119
|
+
type ReadyDocumentState,
|
|
120
|
+
type SessionSnapshot,
|
|
121
|
+
} from './session.ts'
|
|
122
|
+
export {
|
|
123
|
+
isLivePreviewGrant,
|
|
124
|
+
seal,
|
|
125
|
+
SEAL_PURPOSE,
|
|
126
|
+
sha256,
|
|
127
|
+
unseal,
|
|
128
|
+
type PreviewGrant,
|
|
129
|
+
} from './seal.ts'
|
|
130
|
+
export { CONTEXT_KEY, COOKIE, DEFAULTS, HEADER, ROUTES } from './constants.ts'
|
|
131
|
+
export { assertNever, errorText } from './util.ts'
|