@payload-onpage/core 0.2.1 → 0.3.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.
Files changed (72) hide show
  1. package/README.md +4 -0
  2. package/dist/autosave.d.ts +2 -2
  3. package/dist/autosave.d.ts.map +1 -1
  4. package/dist/autosave.js +2 -1
  5. package/dist/autosave.js.map +1 -1
  6. package/dist/constants.d.ts +14 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +14 -0
  9. package/dist/constants.js.map +1 -1
  10. package/dist/docref.d.ts +18 -3
  11. package/dist/docref.d.ts.map +1 -1
  12. package/dist/docref.js +13 -4
  13. package/dist/docref.js.map +1 -1
  14. package/dist/encode.d.ts +16 -4
  15. package/dist/encode.d.ts.map +1 -1
  16. package/dist/encode.js +42 -62
  17. package/dist/encode.js.map +1 -1
  18. package/dist/fields.d.ts +39 -8
  19. package/dist/fields.d.ts.map +1 -1
  20. package/dist/fields.js +111 -97
  21. package/dist/fields.js.map +1 -1
  22. package/dist/index.d.ts +16 -12
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +16 -12
  25. package/dist/index.js.map +1 -1
  26. package/dist/path.d.ts +9 -1
  27. package/dist/path.d.ts.map +1 -1
  28. package/dist/path.js +27 -8
  29. package/dist/path.js.map +1 -1
  30. package/dist/payload.d.ts +5 -3
  31. package/dist/payload.d.ts.map +1 -1
  32. package/dist/payload.js +34 -23
  33. package/dist/payload.js.map +1 -1
  34. package/dist/ports.d.ts +43 -28
  35. package/dist/ports.d.ts.map +1 -1
  36. package/dist/ports.js +11 -1
  37. package/dist/ports.js.map +1 -1
  38. package/dist/schema.d.ts +71 -26
  39. package/dist/schema.d.ts.map +1 -1
  40. package/dist/schema.js +1 -6
  41. package/dist/schema.js.map +1 -1
  42. package/dist/seal.d.ts +17 -0
  43. package/dist/seal.d.ts.map +1 -1
  44. package/dist/seal.js +13 -0
  45. package/dist/seal.js.map +1 -1
  46. package/dist/session.d.ts +34 -18
  47. package/dist/session.d.ts.map +1 -1
  48. package/dist/session.js +20 -13
  49. package/dist/session.js.map +1 -1
  50. package/dist/stega.d.ts +6 -2
  51. package/dist/stega.d.ts.map +1 -1
  52. package/dist/stega.js +18 -13
  53. package/dist/stega.js.map +1 -1
  54. package/dist/util.d.ts +9 -0
  55. package/dist/util.d.ts.map +1 -0
  56. package/dist/util.js +11 -0
  57. package/dist/util.js.map +1 -0
  58. package/package.json +14 -3
  59. package/src/autosave.ts +4 -3
  60. package/src/constants.ts +16 -0
  61. package/src/docref.ts +26 -6
  62. package/src/encode.ts +47 -55
  63. package/src/fields.ts +166 -98
  64. package/src/index.ts +125 -12
  65. package/src/path.ts +32 -10
  66. package/src/payload.ts +54 -26
  67. package/src/ports.ts +50 -22
  68. package/src/schema.ts +100 -17
  69. package/src/seal.ts +31 -0
  70. package/src/session.ts +67 -47
  71. package/src/stega.ts +25 -17
  72. 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 { blockFor } from './fields.ts'
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: string }) => boolean
54
+ export type EncodeFilter = (args: { field: SchemaField; path: string; ref: RefKey }) => boolean
53
55
 
54
56
  interface Ctx {
55
- ref: string
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
- function encodeValue(value: unknown, field: SchemaField, path: string, ctx: Ctx): unknown {
76
- if (value == null) return value
77
- switch (field.type) {
78
- case 'text':
79
- case 'textarea':
80
- if (typeof value === 'string' && value && allowed(field, path, ctx))
81
- return mark(value, ctx, path, field.type === 'textarea' ? 'textarea' : 'text')
82
- return value
83
- case 'richText':
84
- return allowed(field, path, ctx) ? encodeRichText(value, ctx, path) : value
85
- case 'upload':
86
- if (!field.hasMany && value && typeof value === 'object' && !Array.isArray(value)) {
87
- const media = value as Record<string, unknown>
88
- return {
89
- ...media,
90
- alt: mark(typeof media.alt === 'string' ? media.alt : '', ctx, path, 'media'),
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
- return value
94
- case 'group':
95
- return encodeFields(value, field.fields ?? [], path, ctx)
96
- case 'array':
97
- return Array.isArray(value)
98
- ? (value as unknown[]).map((row: unknown, i: number) =>
99
- encodeFields(row, field.fields ?? [], `${path}.${i}`, ctx),
100
- )
101
- : value
102
- case 'blocks':
103
- return Array.isArray(value)
104
- ? (value as unknown[]).map((row: unknown, i: number) => {
105
- const block = blockFor(field, row)
106
- return block ? encodeFields(row, block.fields, `${path}.${i}`, ctx) : row
107
- })
108
- : value
109
- default:
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: string,
129
- opts: { only?: string[]; filter?: EncodeFilter } = {},
119
+ ref: RefKey,
120
+ opts: { only?: string[] | undefined; filter?: EncodeFilter | undefined } = {},
130
121
  ): T {
131
- const scope = opts.only ? fields.filter((f) => opts.only!.includes(f.name)) : fields
132
- return encodeFields(doc, scope, '', { ref, filter: opts.filter }) as T
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 { DocumentSchema, SchemaBlock, SchemaField } from './schema.ts'
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: SchemaField, row: unknown): SchemaBlock | undefined =>
20
- field.blocks?.find((b) => b.slug === (row as { blockType?: unknown } | null)?.blockType)
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: SchemaField
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 segs = path.split('.')
44
- const last = segs[segs.length - 1]
45
- if (!last || !/^\d+$/.test(last) || !isEditablePath(schema, path)) return null
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.type !== 'array' && listField.type !== 'blocks')) return null
50
- const rows = getAt<unknown[]>(doc, listPath) ?? []
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: isBlock ? (block?.fields ?? []) : (listField.fields ?? []),
61
- label: isBlock
62
- ? (block?.labels.singular ?? 'Section')
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 (let i = 0; i < segs.length; i++) {
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
- return null
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[]): Record<string, unknown> {
109
- const out: Record<string, unknown> = {}
184
+ export function defaultsFor(fields: SchemaField[]): Data {
185
+ const out: Data = {}
110
186
  for (const f of fields) {
111
- if (f.defaultValue !== undefined) {
112
- out[f.name] = structuredClone(f.defaultValue)
113
- } else if (f.type === 'group') {
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
- } else if (f.type === 'blocks') {
118
- out[f.name] = []
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: SchemaField): Record<string, unknown> {
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): Record<string, unknown> {
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
- if (!row || typeof row !== 'object') return row
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: SchemaField): unknown {
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
- if (value == null) return value
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 * from './docref.ts'
8
- export * from './path.ts'
9
- export * from './schema.ts'
10
- export * from './fields.ts'
11
- export * from './autosave.ts'
12
- export * from './stega.ts'
13
- export * from './encode.ts'
14
- export * from './lexical.ts'
15
- export * from './ports.ts'
16
- export * from './session.ts'
17
- export * from './seal.ts'
18
- export * from './constants.ts'
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'