@nitrogenbuilder/connector-payload 1.1.0 → 1.1.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.
@@ -25,10 +25,6 @@ export const NitrogenComponentUsage = {
25
25
  type: 'text',
26
26
  required: true,
27
27
  },
28
- {
29
- name: 'depth',
30
- type: 'number',
31
- },
32
28
  {
33
29
  name: 'sourceCollection',
34
30
  type: 'text',
@@ -56,26 +52,12 @@ export const NitrogenComponentUsage = {
56
52
  name: 'sourceStatus',
57
53
  type: 'text',
58
54
  },
55
+ // Usage answers "which components appear in which documents". A module's
56
+ // position in the tree is deliberately not stored: nothing read it, and
57
+ // keeping it meant every reorder rewrote every row of the document.
59
58
  {
60
59
  name: 'moduleId',
61
60
  type: 'text',
62
61
  },
63
- {
64
- name: 'modulePath',
65
- type: 'text',
66
- required: true,
67
- },
68
- {
69
- name: 'parentComponentName',
70
- type: 'text',
71
- },
72
- {
73
- name: 'parentModuleId',
74
- type: 'text',
75
- },
76
- {
77
- name: 'parentModulePath',
78
- type: 'text',
79
- },
80
62
  ],
81
63
  };
@@ -548,7 +548,7 @@ export default function NitrogenComponentInventoryClient({ adminRoute, component
548
548
  display: 'block',
549
549
  padding: 12,
550
550
  textDecoration: 'none',
551
- }, children: [_jsx("div", { style: { fontWeight: 600 }, children: usage.sourceTitle || usage.sourceSlug || usage.sourceDocumentId }), _jsxs("div", { style: { color: payloadTheme.mutedText, fontSize: 12, marginTop: 2 }, children: [usage.sourceCollection, " - ", usage.modulePath] })] }, usage.usageKey))) })) : (_jsx("p", { style: { color: payloadTheme.mutedText, margin: 0 }, children: "No indexed usages yet." }))] }), _jsxs("div", { children: [_jsx("h3", { style: { color: payloadTheme.text, margin: '0 0 10px', fontSize: 15 }, children: "Props" }), definition ? (Object.values(definition.categories).map((category) => renderCategory(category))) : (_jsx("p", { style: { color: payloadTheme.mutedText, margin: 0 }, children: "No manifest definition available." }))] }), _jsxs("div", { children: [_jsx("h3", { style: { color: payloadTheme.text, margin: '0 0 10px', fontSize: 15 }, children: "Update Prop Across Site" }), editableProps.length ? (_jsxs("div", { style: {
551
+ }, children: [_jsx("div", { style: { fontWeight: 600 }, children: usage.sourceTitle || usage.sourceSlug || usage.sourceDocumentId }), _jsxs("div", { style: { color: payloadTheme.mutedText, fontSize: 12, marginTop: 2 }, children: [usage.sourceCollection, usage.sourceSlug ? ` - ${usage.sourceSlug}` : ''] })] }, usage.usageKey))) })) : (_jsx("p", { style: { color: payloadTheme.mutedText, margin: 0 }, children: "No indexed usages yet." }))] }), _jsxs("div", { children: [_jsx("h3", { style: { color: payloadTheme.text, margin: '0 0 10px', fontSize: 15 }, children: "Props" }), definition ? (Object.values(definition.categories).map((category) => renderCategory(category))) : (_jsx("p", { style: { color: payloadTheme.mutedText, margin: 0 }, children: "No manifest definition available." }))] }), _jsxs("div", { children: [_jsx("h3", { style: { color: payloadTheme.text, margin: '0 0 10px', fontSize: 15 }, children: "Update Prop Across Site" }), editableProps.length ? (_jsxs("div", { style: {
552
552
  border: `1px solid ${payloadTheme.border}`,
553
553
  borderRadius: 8,
554
554
  display: 'grid',
@@ -6,12 +6,7 @@ export declare const NITROGEN_COMPONENT_CATALOG_COLLECTION = "nitrogen-component
6
6
  export declare const NITROGEN_COMPONENT_USAGE_COLLECTION = "nitrogen-component-usage";
7
7
  type ComponentUsageRecord = {
8
8
  componentName: string;
9
- depth: number;
10
9
  moduleId: string;
11
- modulePath: string;
12
- parentComponentName?: string;
13
- parentModuleId?: string;
14
- parentModulePath?: string;
15
10
  };
16
11
  type ReindexResult = {
17
12
  catalogCount: number;
@@ -10,50 +10,41 @@ function moduleNameFromNode(node) {
10
10
  }
11
11
  return null;
12
12
  }
13
- function collectUsageFromTree(modules, pathPrefix, results, parentContext, depth = 0) {
13
+ function collectUsageFromTree(modules, pathPrefix, results) {
14
14
  if (!Array.isArray(modules))
15
15
  return;
16
16
  modules.forEach((maybeNode, index) => {
17
17
  if (!maybeNode || typeof maybeNode !== 'object')
18
18
  return;
19
19
  const node = maybeNode;
20
- const modulePath = `${pathPrefix}[${index}]`;
21
20
  const componentName = moduleNameFromNode(node);
21
+ // The tree path is no longer stored, but it is still the identity of last
22
+ // resort for a node that carries no id of its own. Such a node reverts to
23
+ // the old positional behaviour and churns when the tree is reordered.
24
+ const modulePath = `${pathPrefix}[${index}]`;
22
25
  const moduleId = typeof node.id === 'string' || typeof node.id === 'number'
23
26
  ? String(node.id)
24
27
  : modulePath;
25
- const currentContext = componentName
26
- ? {
27
- componentName,
28
- moduleId,
29
- modulePath,
30
- }
31
- : parentContext;
32
28
  if (componentName) {
33
29
  results.push({
34
30
  componentName,
35
- depth,
36
31
  moduleId,
37
- modulePath,
38
- parentComponentName: parentContext?.componentName,
39
- parentModuleId: parentContext?.moduleId,
40
- parentModulePath: parentContext?.modulePath,
41
32
  });
42
33
  }
43
34
  const props = node.props;
44
35
  if (props && typeof props === 'object') {
45
36
  const children = props.children;
46
37
  if (Array.isArray(children)) {
47
- collectUsageFromTree(children, `${modulePath}.props.children`, results, currentContext, depth + 1);
38
+ collectUsageFromTree(children, `${modulePath}.props.children`, results);
48
39
  }
49
40
  else if (children && typeof children === 'object') {
50
41
  for (const [slotName, slotChildren] of Object.entries(children)) {
51
- collectUsageFromTree(slotChildren, `${modulePath}.props.children.${slotName}`, results, currentContext, depth + 1);
42
+ collectUsageFromTree(slotChildren, `${modulePath}.props.children.${slotName}`, results);
52
43
  }
53
44
  }
54
45
  }
55
46
  if (Array.isArray(node.children)) {
56
- collectUsageFromTree(node.children, `${modulePath}.children`, results, currentContext, depth + 1);
47
+ collectUsageFromTree(node.children, `${modulePath}.children`, results);
57
48
  }
58
49
  });
59
50
  }
@@ -84,62 +75,77 @@ export async function findAllDocs(payload, collection, options, req) {
84
75
  }
85
76
  return docs;
86
77
  }
87
- async function deleteUsageDocs(payload, docs, req) {
88
- for (const doc of docs) {
89
- await payload.delete({
90
- collection: NITROGEN_COMPONENT_USAGE_COLLECTION,
91
- id: doc.id,
92
- overrideAccess: true,
93
- req,
94
- });
95
- }
78
+ /**
79
+ * Delete usage rows in one bulk statement rather than paginating the ids and
80
+ * issuing a `payload.delete` per row. Usage is rebuilt wholesale on every save,
81
+ * so a document with N modules previously cost N deletes (plus the finds to
82
+ * discover them) on the critical path of the save transaction.
83
+ */
84
+ async function deleteUsageDocsWhere(payload, where, req) {
85
+ await payload.delete({
86
+ collection: NITROGEN_COMPONENT_USAGE_COLLECTION,
87
+ overrideAccess: true,
88
+ req,
89
+ where,
90
+ });
96
91
  }
97
- async function findUsageDocsForSource(payload, sourceCollection, sourceDocumentId, req) {
98
- return findAllDocs(payload, NITROGEN_COMPONENT_USAGE_COLLECTION, {
99
- limit: 100,
100
- select: {
101
- id: true,
102
- },
103
- where: {
104
- and: [
105
- {
106
- sourceCollection: {
107
- equals: sourceCollection,
108
- },
92
+ function usageDocsForSourceWhere(sourceCollection, sourceDocumentId) {
93
+ return {
94
+ and: [
95
+ {
96
+ sourceCollection: {
97
+ equals: sourceCollection,
109
98
  },
110
- {
111
- sourceDocumentId: {
112
- equals: sourceDocumentId,
113
- },
99
+ },
100
+ {
101
+ sourceDocumentId: {
102
+ equals: sourceDocumentId,
114
103
  },
115
- ],
116
- },
117
- }, req);
104
+ },
105
+ ],
106
+ };
107
+ }
108
+ function usageKeyFor(sourceCollection, sourceDocumentId, record) {
109
+ return `${sourceCollection}:${sourceDocumentId}:${record.moduleId}:${record.componentName}`;
110
+ }
111
+ function usageSourceFields(doc) {
112
+ return {
113
+ sourceTitle: doc.title || '',
114
+ sourceSlug: doc.slug || '',
115
+ sourceStatus: doc.status || doc._status || '',
116
+ };
117
+ }
118
+ /**
119
+ * `componentName`, `moduleId`, `sourceCollection` and `sourceDocumentId` are all
120
+ * encoded in the usage key, so a key match already implies they agree; the
121
+ * source fields are reconciled separately by a single bulk update. That leaves
122
+ * only `sourceType`, which is checked defensively so a row written with the
123
+ * wrong value can still heal.
124
+ */
125
+ function usageRowIsCurrent(row, sourceType) {
126
+ return row.sourceType === sourceType;
127
+ }
128
+ function usageRowData(sourceCollection, sourceType, sourceDocumentId, sourceFields, record) {
129
+ return {
130
+ usageKey: usageKeyFor(sourceCollection, sourceDocumentId, record),
131
+ componentName: record.componentName,
132
+ sourceCollection,
133
+ sourceType,
134
+ sourceDocumentId,
135
+ ...sourceFields,
136
+ moduleId: record.moduleId,
137
+ };
118
138
  }
119
139
  async function createUsageDocs(payload, sourceCollection, sourceType, doc, req) {
120
140
  const sourceDocumentId = String(doc.id);
141
+ const sourceFields = usageSourceFields(doc);
121
142
  const usageRecords = extractComponentUsageRecords(doc.nitrogenData);
122
143
  for (const record of usageRecords) {
123
144
  await payload.create({
124
145
  collection: NITROGEN_COMPONENT_USAGE_COLLECTION,
125
146
  overrideAccess: true,
126
147
  req,
127
- data: {
128
- usageKey: `${sourceCollection}:${sourceDocumentId}:${record.moduleId}:${record.modulePath}:${record.componentName}`,
129
- componentName: record.componentName,
130
- depth: record.depth,
131
- sourceCollection,
132
- sourceType,
133
- sourceDocumentId,
134
- sourceTitle: doc.title || '',
135
- sourceSlug: doc.slug || '',
136
- sourceStatus: doc.status || doc._status || '',
137
- moduleId: record.moduleId,
138
- modulePath: record.modulePath,
139
- parentComponentName: record.parentComponentName || '',
140
- parentModuleId: record.parentModuleId || '',
141
- parentModulePath: record.parentModulePath || '',
142
- },
148
+ data: usageRowData(sourceCollection, sourceType, sourceDocumentId, sourceFields, record),
143
149
  });
144
150
  }
145
151
  return usageRecords.length;
@@ -193,23 +199,72 @@ export async function syncComponentCatalog(payload, manifestInput, req) {
193
199
  }
194
200
  export async function reindexDocumentUsage(payload, sourceCollection, doc, sourceType = 'document', req) {
195
201
  const sourceDocumentId = String(doc.id);
196
- const docsForSource = await findUsageDocsForSource(payload, sourceCollection, sourceDocumentId, req);
197
- await deleteUsageDocs(payload, docsForSource, req);
198
- return createUsageDocs(payload, sourceCollection, sourceType, doc, req);
202
+ const sourceWhere = usageDocsForSourceWhere(sourceCollection, sourceDocumentId);
203
+ const sourceFields = usageSourceFields(doc);
204
+ const usageRecords = extractComponentUsageRecords(doc.nitrogenData);
205
+ // Usage rows describe tree *structure* only — nothing derived from module
206
+ // props or content. Editing copy therefore leaves every row identical, so
207
+ // diffing turns the common autosave case into zero writes instead of a full
208
+ // delete-and-recreate of every row on the save's critical path.
209
+ const existingRows = await findAllDocs(payload, NITROGEN_COMPONENT_USAGE_COLLECTION, { limit: 200, where: sourceWhere }, req);
210
+ const desiredByKey = new Map();
211
+ for (const record of usageRecords) {
212
+ desiredByKey.set(usageKeyFor(sourceCollection, sourceDocumentId, record), record);
213
+ }
214
+ const existingByKey = new Map();
215
+ const staleKeys = [];
216
+ for (const row of existingRows) {
217
+ if (desiredByKey.has(row.usageKey))
218
+ existingByKey.set(row.usageKey, row);
219
+ else
220
+ staleKeys.push(row.usageKey);
221
+ }
222
+ if (staleKeys.length > 0) {
223
+ await deleteUsageDocsWhere(payload, { usageKey: { in: staleKeys } }, req);
224
+ }
225
+ // Title/slug/status are identical across every row of the document, so when
226
+ // they change one bulk update fixes the whole set rather than N row updates.
227
+ const sourceFieldsStale = existingRows.some((row) => (row.sourceTitle || '') !== sourceFields.sourceTitle ||
228
+ (row.sourceSlug || '') !== sourceFields.sourceSlug ||
229
+ (row.sourceStatus || '') !== sourceFields.sourceStatus);
230
+ if (sourceFieldsStale && existingByKey.size > 0) {
231
+ await payload.update({
232
+ collection: NITROGEN_COMPONENT_USAGE_COLLECTION,
233
+ overrideAccess: true,
234
+ req,
235
+ where: sourceWhere,
236
+ data: sourceFields,
237
+ });
238
+ }
239
+ for (const [usageKey, record] of desiredByKey) {
240
+ const row = existingByKey.get(usageKey);
241
+ const data = usageRowData(sourceCollection, sourceType, sourceDocumentId, sourceFields, record);
242
+ if (!row) {
243
+ await payload.create({
244
+ collection: NITROGEN_COMPONENT_USAGE_COLLECTION,
245
+ overrideAccess: true,
246
+ req,
247
+ data,
248
+ });
249
+ }
250
+ else if (!usageRowIsCurrent(row, sourceType)) {
251
+ await payload.update({
252
+ collection: NITROGEN_COMPONENT_USAGE_COLLECTION,
253
+ id: row.id,
254
+ overrideAccess: true,
255
+ req,
256
+ data,
257
+ });
258
+ }
259
+ }
260
+ return usageRecords.length;
199
261
  }
200
262
  export async function deleteDocumentUsage(payload, sourceCollection, sourceDocumentId, req) {
201
- const docsForSource = await findUsageDocsForSource(payload, sourceCollection, String(sourceDocumentId), req);
202
- await deleteUsageDocs(payload, docsForSource, req);
263
+ await deleteUsageDocsWhere(payload, usageDocsForSourceWhere(sourceCollection, String(sourceDocumentId)), req);
203
264
  }
204
265
  export async function reindexAllComponentInventory(payload, collections, manifestInput, req) {
205
266
  const entries = await syncComponentCatalog(payload, manifestInput, req);
206
- const usageDocs = await findAllDocs(payload, NITROGEN_COMPONENT_USAGE_COLLECTION, {
207
- limit: 200,
208
- select: {
209
- id: true,
210
- },
211
- }, req);
212
- await deleteUsageDocs(payload, usageDocs, req);
267
+ await deleteUsageDocsWhere(payload, { id: { exists: true } }, req);
213
268
  let documentCount = 0;
214
269
  let usageCount = 0;
215
270
  for (const collection of collections) {
package/dist/types.d.ts CHANGED
@@ -152,7 +152,6 @@ export interface NitrogenComponentUsageDoc {
152
152
  id: string | number;
153
153
  usageKey: string;
154
154
  componentName: string;
155
- depth?: number;
156
155
  sourceCollection: string;
157
156
  sourceType: string;
158
157
  sourceDocumentId: string;
@@ -160,10 +159,6 @@ export interface NitrogenComponentUsageDoc {
160
159
  sourceSlug?: string;
161
160
  sourceStatus?: string;
162
161
  moduleId?: string;
163
- modulePath: string;
164
- parentComponentName?: string;
165
- parentModuleId?: string;
166
- parentModulePath?: string;
167
162
  }
168
163
  export interface NitrogenInventorySourceDoc {
169
164
  id: string | number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitrogenbuilder/connector-payload",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Nitrogen page builder connector plugin for Payload CMS 3.x",
5
5
  "author": "Leonardo Dentzien <leo@torchmedia.ca>",
6
6
  "type": "module",