@next-buildr/payload 0.0.0-stage → 2.0.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 (250) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/adapter/adapter.test.d.ts +2 -0
  5. package/dist/adapter/adapter.test.d.ts.map +1 -0
  6. package/dist/adapter/adapter.test.js +282 -0
  7. package/dist/adapter/canvas-data-source.d.ts +9 -0
  8. package/dist/adapter/canvas-data-source.d.ts.map +1 -0
  9. package/dist/adapter/canvas-data-source.js +24 -0
  10. package/dist/adapter/document-adapter.d.ts +20 -0
  11. package/dist/adapter/document-adapter.d.ts.map +1 -0
  12. package/dist/adapter/document-adapter.js +183 -0
  13. package/dist/adapter/http.d.ts +34 -0
  14. package/dist/adapter/http.d.ts.map +1 -0
  15. package/dist/adapter/http.js +72 -0
  16. package/dist/adapter/index.d.ts +4 -0
  17. package/dist/adapter/index.d.ts.map +1 -0
  18. package/dist/adapter/index.js +5 -0
  19. package/dist/admin/index.d.ts +4 -0
  20. package/dist/admin/index.d.ts.map +1 -0
  21. package/dist/admin/index.js +2 -0
  22. package/dist/admin/layout-field-connected.d.ts +6 -0
  23. package/dist/admin/layout-field-connected.d.ts.map +1 -0
  24. package/dist/admin/layout-field-connected.js +16 -0
  25. package/dist/admin/layout-field-view.d.ts +16 -0
  26. package/dist/admin/layout-field-view.d.ts.map +1 -0
  27. package/dist/admin/layout-field-view.js +14 -0
  28. package/dist/admin/layout-field.d.ts +6 -0
  29. package/dist/admin/layout-field.d.ts.map +1 -0
  30. package/dist/admin/layout-field.js +13 -0
  31. package/dist/admin/layout-field.test.d.ts +2 -0
  32. package/dist/admin/layout-field.test.d.ts.map +1 -0
  33. package/dist/admin/layout-field.test.js +41 -0
  34. package/dist/contract.d.ts +289 -0
  35. package/dist/contract.d.ts.map +1 -0
  36. package/dist/contract.js +176 -0
  37. package/dist/data/build-context.d.ts +20 -0
  38. package/dist/data/build-context.d.ts.map +1 -0
  39. package/dist/data/build-context.js +25 -0
  40. package/dist/data/data.test.d.ts +2 -0
  41. package/dist/data/data.test.d.ts.map +1 -0
  42. package/dist/data/data.test.js +261 -0
  43. package/dist/data/index.d.ts +10 -0
  44. package/dist/data/index.d.ts.map +1 -0
  45. package/dist/data/index.js +5 -0
  46. package/dist/data/normalize.d.ts +14 -0
  47. package/dist/data/normalize.d.ts.map +1 -0
  48. package/dist/data/normalize.js +128 -0
  49. package/dist/data/payload-data-source.d.ts +42 -0
  50. package/dist/data/payload-data-source.d.ts.map +1 -0
  51. package/dist/data/payload-data-source.js +179 -0
  52. package/dist/data/payload-data-source.test.d.ts +2 -0
  53. package/dist/data/payload-data-source.test.d.ts.map +1 -0
  54. package/dist/data/payload-data-source.test.js +160 -0
  55. package/dist/data/resolve-layout.d.ts +45 -0
  56. package/dist/data/resolve-layout.d.ts.map +1 -0
  57. package/dist/data/resolve-layout.js +111 -0
  58. package/dist/data/schema-from-fields.d.ts +63 -0
  59. package/dist/data/schema-from-fields.d.ts.map +1 -0
  60. package/dist/data/schema-from-fields.js +178 -0
  61. package/dist/data/where.d.ts +53 -0
  62. package/dist/data/where.d.ts.map +1 -0
  63. package/dist/data/where.js +210 -0
  64. package/dist/mcp/http-backend.d.ts +35 -0
  65. package/dist/mcp/http-backend.d.ts.map +1 -0
  66. package/dist/mcp/http-backend.js +337 -0
  67. package/dist/mcp/http-backend.test.d.ts +2 -0
  68. package/dist/mcp/http-backend.test.d.ts.map +1 -0
  69. package/dist/mcp/http-backend.test.js +178 -0
  70. package/dist/mcp/index.d.ts +2 -0
  71. package/dist/mcp/index.d.ts.map +1 -0
  72. package/dist/mcp/index.js +2 -0
  73. package/dist/mcp/route.d.ts +63 -0
  74. package/dist/mcp/route.d.ts.map +1 -0
  75. package/dist/mcp/route.js +230 -0
  76. package/dist/next/get-document.d.ts +64 -0
  77. package/dist/next/get-document.d.ts.map +1 -0
  78. package/dist/next/get-document.js +208 -0
  79. package/dist/next/get-document.test.d.ts +2 -0
  80. package/dist/next/get-document.test.d.ts.map +1 -0
  81. package/dist/next/get-document.test.js +155 -0
  82. package/dist/next/index.d.ts +9 -0
  83. package/dist/next/index.d.ts.map +1 -0
  84. package/dist/next/index.js +4 -0
  85. package/dist/next/next-helpers.test.d.ts +2 -0
  86. package/dist/next/next-helpers.test.d.ts.map +1 -0
  87. package/dist/next/next-helpers.test.js +122 -0
  88. package/dist/next/revalidate.d.ts +44 -0
  89. package/dist/next/revalidate.d.ts.map +1 -0
  90. package/dist/next/revalidate.js +45 -0
  91. package/dist/next/seo.d.ts +43 -0
  92. package/dist/next/seo.d.ts.map +1 -0
  93. package/dist/next/seo.js +53 -0
  94. package/dist/next/tags.d.ts +23 -0
  95. package/dist/next/tags.d.ts.map +1 -0
  96. package/dist/next/tags.js +31 -0
  97. package/dist/plugin/access.d.ts +19 -0
  98. package/dist/plugin/access.d.ts.map +1 -0
  99. package/dist/plugin/access.js +32 -0
  100. package/dist/plugin/collections/form-submissions.d.ts +10 -0
  101. package/dist/plugin/collections/form-submissions.d.ts.map +1 -0
  102. package/dist/plugin/collections/form-submissions.js +37 -0
  103. package/dist/plugin/collections/templates.d.ts +13 -0
  104. package/dist/plugin/collections/templates.d.ts.map +1 -0
  105. package/dist/plugin/collections/templates.js +118 -0
  106. package/dist/plugin/endpoints/access.test.d.ts +2 -0
  107. package/dist/plugin/endpoints/access.test.d.ts.map +1 -0
  108. package/dist/plugin/endpoints/access.test.js +91 -0
  109. package/dist/plugin/endpoints/adapter-contract.test.d.ts +2 -0
  110. package/dist/plugin/endpoints/adapter-contract.test.d.ts.map +1 -0
  111. package/dist/plugin/endpoints/adapter-contract.test.js +45 -0
  112. package/dist/plugin/endpoints/context.d.ts +65 -0
  113. package/dist/plugin/endpoints/context.d.ts.map +1 -0
  114. package/dist/plugin/endpoints/context.js +113 -0
  115. package/dist/plugin/endpoints/data-context.d.ts +8 -0
  116. package/dist/plugin/endpoints/data-context.d.ts.map +1 -0
  117. package/dist/plugin/endpoints/data-context.js +63 -0
  118. package/dist/plugin/endpoints/data-endpoints.test.d.ts +2 -0
  119. package/dist/plugin/endpoints/data-endpoints.test.d.ts.map +1 -0
  120. package/dist/plugin/endpoints/data-endpoints.test.js +125 -0
  121. package/dist/plugin/endpoints/data-query.test.d.ts +2 -0
  122. package/dist/plugin/endpoints/data-query.test.d.ts.map +1 -0
  123. package/dist/plugin/endpoints/data-query.test.js +96 -0
  124. package/dist/plugin/endpoints/data-schema.d.ts +5 -0
  125. package/dist/plugin/endpoints/data-schema.d.ts.map +1 -0
  126. package/dist/plugin/endpoints/data-schema.js +21 -0
  127. package/dist/plugin/endpoints/data.d.ts +10 -0
  128. package/dist/plugin/endpoints/data.d.ts.map +1 -0
  129. package/dist/plugin/endpoints/data.js +96 -0
  130. package/dist/plugin/endpoints/document.d.ts +11 -0
  131. package/dist/plugin/endpoints/document.d.ts.map +1 -0
  132. package/dist/plugin/endpoints/document.js +119 -0
  133. package/dist/plugin/endpoints/documents.d.ts +10 -0
  134. package/dist/plugin/endpoints/documents.d.ts.map +1 -0
  135. package/dist/plugin/endpoints/documents.js +145 -0
  136. package/dist/plugin/endpoints/endpoints.test-kit.d.ts +42 -0
  137. package/dist/plugin/endpoints/endpoints.test-kit.d.ts.map +1 -0
  138. package/dist/plugin/endpoints/endpoints.test-kit.js +141 -0
  139. package/dist/plugin/endpoints/endpoints.test.d.ts +2 -0
  140. package/dist/plugin/endpoints/endpoints.test.d.ts.map +1 -0
  141. package/dist/plugin/endpoints/endpoints.test.js +209 -0
  142. package/dist/plugin/endpoints/forms.d.ts +16 -0
  143. package/dist/plugin/endpoints/forms.d.ts.map +1 -0
  144. package/dist/plugin/endpoints/forms.js +199 -0
  145. package/dist/plugin/endpoints/forms.test.d.ts +2 -0
  146. package/dist/plugin/endpoints/forms.test.d.ts.map +1 -0
  147. package/dist/plugin/endpoints/forms.test.js +346 -0
  148. package/dist/plugin/endpoints/guards.d.ts +9 -0
  149. package/dist/plugin/endpoints/guards.d.ts.map +1 -0
  150. package/dist/plugin/endpoints/guards.js +35 -0
  151. package/dist/plugin/endpoints/index.d.ts +6 -0
  152. package/dist/plugin/endpoints/index.d.ts.map +1 -0
  153. package/dist/plugin/endpoints/index.js +32 -0
  154. package/dist/plugin/endpoints/locales-no-fallback.test.d.ts +2 -0
  155. package/dist/plugin/endpoints/locales-no-fallback.test.d.ts.map +1 -0
  156. package/dist/plugin/endpoints/locales-no-fallback.test.js +30 -0
  157. package/dist/plugin/endpoints/locales.test.d.ts +2 -0
  158. package/dist/plugin/endpoints/locales.test.d.ts.map +1 -0
  159. package/dist/plugin/endpoints/locales.test.js +128 -0
  160. package/dist/plugin/endpoints/manifest.d.ts +5 -0
  161. package/dist/plugin/endpoints/manifest.d.ts.map +1 -0
  162. package/dist/plugin/endpoints/manifest.js +18 -0
  163. package/dist/plugin/endpoints/mcp-http-backend.test.d.ts +2 -0
  164. package/dist/plugin/endpoints/mcp-http-backend.test.d.ts.map +1 -0
  165. package/dist/plugin/endpoints/mcp-http-backend.test.js +84 -0
  166. package/dist/plugin/endpoints/mcp-off.test.d.ts +2 -0
  167. package/dist/plugin/endpoints/mcp-off.test.d.ts.map +1 -0
  168. package/dist/plugin/endpoints/mcp-off.test.js +33 -0
  169. package/dist/plugin/endpoints/mcp-publish.test.d.ts +2 -0
  170. package/dist/plugin/endpoints/mcp-publish.test.d.ts.map +1 -0
  171. package/dist/plugin/endpoints/mcp-publish.test.js +42 -0
  172. package/dist/plugin/endpoints/mcp-route.test.d.ts +2 -0
  173. package/dist/plugin/endpoints/mcp-route.test.d.ts.map +1 -0
  174. package/dist/plugin/endpoints/mcp-route.test.js +132 -0
  175. package/dist/plugin/endpoints/mcp.test.d.ts +2 -0
  176. package/dist/plugin/endpoints/mcp.test.d.ts.map +1 -0
  177. package/dist/plugin/endpoints/mcp.test.js +231 -0
  178. package/dist/plugin/endpoints/media.d.ts +7 -0
  179. package/dist/plugin/endpoints/media.d.ts.map +1 -0
  180. package/dist/plugin/endpoints/media.js +115 -0
  181. package/dist/plugin/endpoints/media.test.d.ts +2 -0
  182. package/dist/plugin/endpoints/media.test.d.ts.map +1 -0
  183. package/dist/plugin/endpoints/media.test.js +95 -0
  184. package/dist/plugin/endpoints/publish-a11y.test.d.ts +2 -0
  185. package/dist/plugin/endpoints/publish-a11y.test.d.ts.map +1 -0
  186. package/dist/plugin/endpoints/publish-a11y.test.js +27 -0
  187. package/dist/plugin/endpoints/publish.d.ts +9 -0
  188. package/dist/plugin/endpoints/publish.d.ts.map +1 -0
  189. package/dist/plugin/endpoints/publish.js +81 -0
  190. package/dist/plugin/endpoints/rename.d.ts +9 -0
  191. package/dist/plugin/endpoints/rename.d.ts.map +1 -0
  192. package/dist/plugin/endpoints/rename.js +47 -0
  193. package/dist/plugin/endpoints/respond.d.ts +9 -0
  194. package/dist/plugin/endpoints/respond.d.ts.map +1 -0
  195. package/dist/plugin/endpoints/respond.js +10 -0
  196. package/dist/plugin/endpoints/samples.d.ts +5 -0
  197. package/dist/plugin/endpoints/samples.d.ts.map +1 -0
  198. package/dist/plugin/endpoints/samples.js +42 -0
  199. package/dist/plugin/endpoints/save.d.ts +9 -0
  200. package/dist/plugin/endpoints/save.d.ts.map +1 -0
  201. package/dist/plugin/endpoints/save.js +58 -0
  202. package/dist/plugin/endpoints/session.d.ts +5 -0
  203. package/dist/plugin/endpoints/session.d.ts.map +1 -0
  204. package/dist/plugin/endpoints/session.js +30 -0
  205. package/dist/plugin/endpoints/templates.test.d.ts +2 -0
  206. package/dist/plugin/endpoints/templates.test.d.ts.map +1 -0
  207. package/dist/plugin/endpoints/templates.test.js +191 -0
  208. package/dist/plugin/fields.d.ts +28 -0
  209. package/dist/plugin/fields.d.ts.map +1 -0
  210. package/dist/plugin/fields.js +62 -0
  211. package/dist/plugin/forms/forms.test.d.ts +2 -0
  212. package/dist/plugin/forms/forms.test.d.ts.map +1 -0
  213. package/dist/plugin/forms/forms.test.js +88 -0
  214. package/dist/plugin/forms/notify.d.ts +6 -0
  215. package/dist/plugin/forms/notify.d.ts.map +1 -0
  216. package/dist/plugin/forms/notify.js +13 -0
  217. package/dist/plugin/forms/rate-limit.d.ts +24 -0
  218. package/dist/plugin/forms/rate-limit.d.ts.map +1 -0
  219. package/dist/plugin/forms/rate-limit.js +36 -0
  220. package/dist/plugin/forms/validate.d.ts +22 -0
  221. package/dist/plugin/forms/validate.d.ts.map +1 -0
  222. package/dist/plugin/forms/validate.js +79 -0
  223. package/dist/plugin/hooks/layout-hook.d.ts +9 -0
  224. package/dist/plugin/hooks/layout-hook.d.ts.map +1 -0
  225. package/dist/plugin/hooks/layout-hook.js +33 -0
  226. package/dist/plugin/hooks/layout-hook.test.d.ts +2 -0
  227. package/dist/plugin/hooks/layout-hook.test.d.ts.map +1 -0
  228. package/dist/plugin/hooks/layout-hook.test.js +191 -0
  229. package/dist/plugin/hooks/process-layout.d.ts +32 -0
  230. package/dist/plugin/hooks/process-layout.d.ts.map +1 -0
  231. package/dist/plugin/hooks/process-layout.js +68 -0
  232. package/dist/plugin/hooks/read-layout.d.ts +17 -0
  233. package/dist/plugin/hooks/read-layout.d.ts.map +1 -0
  234. package/dist/plugin/hooks/read-layout.js +39 -0
  235. package/dist/plugin/index.d.ts +18 -0
  236. package/dist/plugin/index.d.ts.map +1 -0
  237. package/dist/plugin/index.js +105 -0
  238. package/dist/plugin/locales.d.ts +32 -0
  239. package/dist/plugin/locales.d.ts.map +1 -0
  240. package/dist/plugin/locales.js +56 -0
  241. package/dist/plugin/options.d.ts +77 -0
  242. package/dist/plugin/options.d.ts.map +1 -0
  243. package/dist/plugin/options.js +123 -0
  244. package/dist/plugin/plugin.test.d.ts +2 -0
  245. package/dist/plugin/plugin.test.d.ts.map +1 -0
  246. package/dist/plugin/plugin.test.js +186 -0
  247. package/dist/plugin/write-guard.d.ts +14 -0
  248. package/dist/plugin/write-guard.d.ts.map +1 -0
  249. package/dist/plugin/write-guard.js +18 -0
  250. package/package.json +108 -3
@@ -0,0 +1,178 @@
1
+ /** Fields the builder itself owns; they are never data for a binding. */
2
+ const INTERNAL_FIELDS = new Set(['layout', 'buildrRevision', '_status']);
3
+ /** A name that says the value is a credential, whatever the field config claims. */
4
+ const SENSITIVE_NAME = /pass(word|phrase)|secret|token|salt|hash|api[-_]?key|otp|2fa|session|credential/i;
5
+ /** Payload's own collections and the plugin's; the auth collections are excluded by `auth`. */
6
+ const isInternalCollection = (slug) => slug.startsWith('payload-') || slug.startsWith('buildr-');
7
+ /** Whether a collection may take part in the schema and in contexts at all: never the auth ones. */
8
+ export const isExposedCollection = (collection) => !collection.auth && !isInternalCollection(collection.slug);
9
+ /** Layout-only wrappers whose children are ordinary siblings of the parent. */
10
+ const CONTAINERS = new Set(['row', 'collapsible']);
11
+ /** A field with a name that the schema (and the context) may carry, or `undefined`. */
12
+ function exposed(field) {
13
+ if (field.name === undefined || field.name === '')
14
+ return undefined;
15
+ if (INTERNAL_FIELDS.has(field.name) || SENSITIVE_NAME.test(field.name))
16
+ return undefined;
17
+ if (field.hidden === true)
18
+ return undefined;
19
+ // A field-level read rule may hide it from this very user; the schema is not user-specific.
20
+ if (field.access?.read !== undefined)
21
+ return undefined;
22
+ return field;
23
+ }
24
+ /**
25
+ * The named fields of `fields` a binding may reach, layout containers (`row`, `collapsible`, tabs)
26
+ * flattened; a named tab is a `group`. This is the allow-list both the schema and the context use.
27
+ */
28
+ export function exposedFields(fields) {
29
+ const result = [];
30
+ for (const field of fields) {
31
+ if (CONTAINERS.has(field.type)) {
32
+ result.push(...exposedFields(field.fields ?? []));
33
+ }
34
+ else if (field.type === 'tabs') {
35
+ for (const tab of field.tabs ?? []) {
36
+ if (tab.name === undefined) {
37
+ result.push(...exposedFields(tab.fields));
38
+ }
39
+ else {
40
+ const named = exposed({ type: 'group', name: tab.name, fields: tab.fields });
41
+ if (named !== undefined)
42
+ result.push(named);
43
+ }
44
+ }
45
+ }
46
+ else {
47
+ const own = exposed(field);
48
+ if (own !== undefined)
49
+ result.push(own);
50
+ }
51
+ }
52
+ return result;
53
+ }
54
+ const stringLabel = (field) => typeof field.label === 'string' ? field.label : undefined;
55
+ const optionValue = (option) => {
56
+ if (typeof option === 'string')
57
+ return option;
58
+ if (typeof option === 'object' && option !== null && 'value' in option) {
59
+ const value = option.value;
60
+ return typeof value === 'string' ? value : undefined;
61
+ }
62
+ return undefined;
63
+ };
64
+ const targetOf = (field) => typeof field.relationTo === 'string' ? field.relationTo : undefined;
65
+ /** The slug a relation points at when it can be followed (a single, exposed target). */
66
+ export function relationTarget(field, source) {
67
+ const slug = targetOf(field);
68
+ const collection = source.collections.find((candidate) => candidate.slug === slug);
69
+ return collection !== undefined && isExposedCollection(collection) ? collection.slug : undefined;
70
+ }
71
+ /** The `DataType` of a field, or `undefined` for a kind the MVP does not expose. */
72
+ function typeOf(field, source, options, reached) {
73
+ const many = (type) => type !== undefined && field.hasMany === true ? { t: 'list', of: type } : type;
74
+ switch (field.type) {
75
+ case 'text':
76
+ case 'textarea':
77
+ case 'email':
78
+ return many({ t: 'string' });
79
+ case 'number':
80
+ return many({ t: 'number' });
81
+ case 'checkbox':
82
+ return { t: 'boolean' };
83
+ case 'date':
84
+ return { t: 'date' };
85
+ case 'select':
86
+ case 'radio': {
87
+ const values = (field.options ?? []).flatMap((option) => optionValue(option) ?? []);
88
+ return many({ t: 'enum', values });
89
+ }
90
+ case 'richText':
91
+ return { t: 'richText' };
92
+ case 'upload':
93
+ return many({ t: 'media' });
94
+ case 'relationship': {
95
+ const slug = relationTarget(field, source);
96
+ if (slug === undefined)
97
+ return undefined;
98
+ reached.add(slug);
99
+ return many({ t: 'ref', entity: options.contextNames[slug] ?? slug });
100
+ }
101
+ case 'group':
102
+ return { t: 'object', fields: fieldsToData(field.fields ?? [], source, options, reached) };
103
+ case 'array':
104
+ return {
105
+ t: 'list',
106
+ of: { t: 'object', fields: fieldsToData(field.fields ?? [], source, options, reached) },
107
+ };
108
+ default:
109
+ // blocks, json, point, code, join, ui, ...: not exposed in the MVP.
110
+ return undefined;
111
+ }
112
+ }
113
+ function fieldsToData(fields, source, options, reached) {
114
+ const result = {};
115
+ for (const field of exposedFields(fields)) {
116
+ const type = typeOf(field, source, options, reached);
117
+ if (type === undefined)
118
+ continue;
119
+ const label = stringLabel(field);
120
+ result[field.name] = { type, ...(label === undefined ? {} : { label }), nullable: true };
121
+ }
122
+ return result;
123
+ }
124
+ const STANDARD = {
125
+ id: { type: { t: 'string' } },
126
+ createdAt: { type: { t: 'date' } },
127
+ updatedAt: { type: { t: 'date' } },
128
+ };
129
+ const objectOf = (fields) => ({
130
+ type: { t: 'object', fields: { ...STANDARD, ...fields } },
131
+ });
132
+ const ROUTE = {
133
+ type: {
134
+ t: 'object',
135
+ fields: {
136
+ path: { type: { t: 'string' } },
137
+ locale: { type: { t: 'string' } },
138
+ params: {
139
+ type: { t: 'object', fields: { page: { type: { t: 'number' }, nullable: true } } },
140
+ },
141
+ },
142
+ },
143
+ };
144
+ /**
145
+ * The `DataSchema` of a collection's documents: the scopes `site` (when the site global exists),
146
+ * the collection's own context name, and `route`; plus one entity per relation target that can be
147
+ * reached (transitively). Auth collections (`users`), hidden fields and anything that looks like a
148
+ * credential are never included; a relation to them is dropped.
149
+ */
150
+ export function schemaFromCollection(source, collection, options) {
151
+ const own = source.collections.find((candidate) => candidate.slug === collection);
152
+ if (own === undefined || !isExposedCollection(own))
153
+ return undefined;
154
+ const reached = new Set();
155
+ const entities = {};
156
+ const scopes = {};
157
+ const site = source.globals?.find((global) => global.slug === options.siteGlobal);
158
+ if (site !== undefined) {
159
+ scopes['site'] = {
160
+ type: { t: 'object', fields: fieldsToData(site.fields, source, options, reached) },
161
+ };
162
+ }
163
+ scopes[options.contextNames[collection] ?? collection] = objectOf(fieldsToData(own.fields, source, options, reached));
164
+ scopes['route'] = ROUTE;
165
+ // Every relation target becomes an entity; a target's own relations extend the worklist.
166
+ const done = new Set();
167
+ while (true) {
168
+ const next = [...reached].find((slug) => !done.has(slug));
169
+ if (next === undefined)
170
+ break;
171
+ done.add(next);
172
+ const target = source.collections.find((candidate) => candidate.slug === next);
173
+ if (target === undefined)
174
+ continue;
175
+ entities[options.contextNames[next] ?? next] = objectOf(fieldsToData(target.fields, source, options, reached));
176
+ }
177
+ return { scopes, entities };
178
+ }
@@ -0,0 +1,53 @@
1
+ import type { FilterNode, JsonValue } from '@next-buildr/core';
2
+ import type { Where } from 'payload';
3
+ import { type CollectionLike } from './schema-from-fields.ts';
4
+ /**
5
+ * How a query field behaves in Payload's `where`, decided from the collection config:
6
+ * - `string` | `number` | `boolean` | `date` | `id`: scalars whose comparisons Payload evaluates
7
+ * the way the `DataSource` contract wants (typed, missing never matches);
8
+ * - `unsafe`: anything else (lists, rich text, ...): Payload cannot decide it, so the condition is
9
+ * evaluated in memory on the normalized items.
10
+ */
11
+ export type FieldKind = 'string' | 'number' | 'boolean' | 'date' | 'id' | 'unsafe';
12
+ export interface QueryField {
13
+ readonly kind: FieldKind;
14
+ /** May the value be missing (an optional field, or one behind an optional relation)? */
15
+ readonly nullable: boolean;
16
+ /** The path in Payload's `where` / `sort`. */
17
+ readonly payloadPath: string;
18
+ }
19
+ export interface IdType {
20
+ readonly kind: 'number' | 'text';
21
+ }
22
+ /**
23
+ * Resolves a query path (`title`, `seo.title`, `author.name`, `author.id`) against the collection.
24
+ * Groups and single relationships to exposed collections are walked; hidden, credential-like and
25
+ * unknown fields do not resolve (`undefined`), so they can never be queried.
26
+ */
27
+ export declare function resolveQueryField(collections: readonly CollectionLike[], slug: string, path: string): QueryField | undefined;
28
+ /** A condition compiled for Payload: always true, never true, or a `where` (`post`: a superset only). */
29
+ export type Compiled = {
30
+ readonly c: 'true';
31
+ readonly post: boolean;
32
+ } | {
33
+ readonly c: 'false';
34
+ readonly post: false;
35
+ } | {
36
+ readonly c: 'where';
37
+ readonly where: Where;
38
+ readonly post: boolean;
39
+ };
40
+ /** Thrown for a field the site owner did not allow (or that cannot be queried at all). */
41
+ export declare class QueryFieldError extends Error {
42
+ }
43
+ export interface WhereContext {
44
+ readonly collections: readonly CollectionLike[];
45
+ readonly source: string;
46
+ readonly allowed: readonly string[];
47
+ readonly id: IdType;
48
+ }
49
+ /** The field of a query path, or a `QueryFieldError` when it is not allowlisted or does not resolve. */
50
+ export declare function queryFieldOf(context: WhereContext, path: string): QueryField;
51
+ /** Compiles a filter tree; `false` results short-circuit, unsafe conditions widen and flag `post`. */
52
+ export declare function compileWhere(node: FilterNode<JsonValue>, context: WhereContext): Compiled;
53
+ //# sourceMappingURL=where.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"where.d.ts","sourceRoot":"","sources":["../../src/data/where.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EACL,KAAK,cAAc,EAIpB,MAAM,yBAAyB,CAAC;AAEjC;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,GAAG,QAAQ,CAAC;AAEnF,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;CAClC;AA6BD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,SAAS,cAAc,EAAE,EACtC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,UAAU,GAAG,SAAS,CAgDxB;AAED,yGAAyG;AACzG,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC9C;IAAE,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GAC7C;IAAE,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAgG3E,0FAA0F;AAC1F,qBAAa,eAAgB,SAAQ,KAAK;CAAG;AAE7C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,wGAAwG;AACxG,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAS5E;AAED,sGAAsG;AACtG,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,YAAY,GAAG,QAAQ,CAsBzF"}
@@ -0,0 +1,210 @@
1
+ import { exposedFields, isExposedCollection, } from './schema-from-fields.js';
2
+ /** The field named `name` among `fields`, layout containers and unnamed tabs looked through. */
3
+ const findField = (fields, name) => exposedFields(fields).find((field) => field.name === name);
4
+ function classify(field) {
5
+ if (field.hasMany === true)
6
+ return 'unsafe';
7
+ switch (field.type) {
8
+ case 'text':
9
+ case 'textarea':
10
+ case 'email':
11
+ case 'select':
12
+ case 'radio':
13
+ return 'string';
14
+ case 'number':
15
+ return 'number';
16
+ case 'checkbox':
17
+ return 'boolean';
18
+ case 'date':
19
+ return 'date';
20
+ default:
21
+ return 'unsafe';
22
+ }
23
+ }
24
+ const isRequired = (field) => field.required === true || field.defaultValue !== undefined;
25
+ /**
26
+ * Resolves a query path (`title`, `seo.title`, `author.name`, `author.id`) against the collection.
27
+ * Groups and single relationships to exposed collections are walked; hidden, credential-like and
28
+ * unknown fields do not resolve (`undefined`), so they can never be queried.
29
+ */
30
+ export function resolveQueryField(collections, slug, path) {
31
+ const own = collections.find((collection) => collection.slug === slug);
32
+ if (own === undefined || !isExposedCollection(own))
33
+ return undefined;
34
+ const segments = path.split('.');
35
+ let fields = own.fields;
36
+ let nullable = false;
37
+ const payloadPath = [];
38
+ for (let index = 0; index < segments.length; index++) {
39
+ const segment = segments[index];
40
+ const last = index === segments.length - 1;
41
+ if (index === 0 && last && segment === 'id') {
42
+ return { kind: 'id', nullable: false, payloadPath: 'id' };
43
+ }
44
+ if (index === 0 && last && (segment === 'createdAt' || segment === 'updatedAt')) {
45
+ return { kind: 'date', nullable: false, payloadPath: segment };
46
+ }
47
+ if (segment === 'id' && index > 0 && last) {
48
+ // `author.id`: Payload's `where` addresses a relationship by the field itself.
49
+ return { kind: 'id', nullable, payloadPath: payloadPath.join('.') };
50
+ }
51
+ const field = findField(fields, segment);
52
+ if (field === undefined)
53
+ return undefined;
54
+ payloadPath.push(segment);
55
+ if (!isRequired(field))
56
+ nullable = true;
57
+ if (last) {
58
+ // A relationship is queried by `author.id` or a field behind it, never as a whole.
59
+ if (field.type === 'relationship' && field.hasMany !== true)
60
+ return undefined;
61
+ return { kind: classify(field), nullable, payloadPath: payloadPath.join('.') };
62
+ }
63
+ if (field.type === 'group') {
64
+ fields = field.fields ?? [];
65
+ }
66
+ else if (field.type === 'relationship' && field.hasMany !== true) {
67
+ const target = collections.find((collection) => collection.slug === field.relationTo);
68
+ if (target === undefined || !isExposedCollection(target))
69
+ return undefined;
70
+ fields = target.fields;
71
+ nullable = true;
72
+ }
73
+ else if (field.type === 'array') {
74
+ // Through a list: the values are many per document; leave it to the in-memory evaluation.
75
+ return { kind: 'unsafe', nullable: true, payloadPath: payloadPath.join('.') };
76
+ }
77
+ else {
78
+ return undefined;
79
+ }
80
+ }
81
+ return undefined;
82
+ }
83
+ const TRUE = { c: 'true', post: false };
84
+ const FALSE = { c: 'false', post: false };
85
+ /** A condition Payload cannot decide: everything passes the database, the memory pass decides. */
86
+ const SUPERSET = { c: 'true', post: true };
87
+ const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
88
+ /** A date operand is safe for Payload only in the canonical form that sorts like the instant. */
89
+ const isCanonicalDate = (value) => typeof value === 'string' && ISO.test(value) && !Number.isNaN(Date.parse(value));
90
+ /** Whether `value` is an operand the kind can equal (typed, like the contract). */
91
+ function accepts(kind, value, id) {
92
+ switch (kind) {
93
+ case 'string':
94
+ return typeof value === 'string';
95
+ case 'number':
96
+ return typeof value === 'number' && Number.isFinite(value);
97
+ case 'boolean':
98
+ return typeof value === 'boolean';
99
+ case 'date':
100
+ return isCanonicalDate(value);
101
+ case 'id':
102
+ // Normalized items carry ids as strings, so only strings can equal one.
103
+ return typeof value === 'string' && (id.kind === 'text' || /^\d+$/.test(value));
104
+ case 'unsafe':
105
+ return false;
106
+ }
107
+ }
108
+ const idValue = (kind, value, id) => kind === 'id' && id.kind === 'number' && typeof value === 'string' ? Number(value) : value;
109
+ const atPath = (path, condition) => ({ [path]: condition });
110
+ const RANGE = {
111
+ gt: 'greater_than',
112
+ gte: 'greater_than_equal',
113
+ lt: 'less_than',
114
+ lte: 'less_than_equal',
115
+ };
116
+ function compileLeaf(field, op, value, id) {
117
+ const { kind, payloadPath: path } = field;
118
+ const wrap = (where, post = false) => ({ c: 'where', where, post });
119
+ const orMissing = (where) => field.nullable ? { or: [where, atPath(path, { exists: false })] } : where;
120
+ if (kind === 'unsafe')
121
+ return SUPERSET;
122
+ switch (op) {
123
+ case 'eq':
124
+ return accepts(kind, value, id)
125
+ ? wrap(atPath(path, { equals: idValue(kind, value, id) }))
126
+ : FALSE;
127
+ case 'neq':
128
+ return accepts(kind, value, id)
129
+ ? wrap(orMissing(atPath(path, { not_equals: idValue(kind, value, id) })))
130
+ : TRUE;
131
+ case 'in': {
132
+ if (!Array.isArray(value))
133
+ return FALSE;
134
+ const usable = value.filter((option) => accepts(kind, option, id));
135
+ return usable.length === 0
136
+ ? FALSE
137
+ : wrap(atPath(path, { in: usable.map((option) => idValue(kind, option, id)) }));
138
+ }
139
+ case 'nin': {
140
+ if (!Array.isArray(value))
141
+ return TRUE;
142
+ const usable = value.filter((option) => accepts(kind, option, id));
143
+ return usable.length === 0
144
+ ? TRUE
145
+ : wrap(orMissing(atPath(path, { not_in: usable.map((option) => idValue(kind, option, id)) })));
146
+ }
147
+ case 'gt':
148
+ case 'gte':
149
+ case 'lt':
150
+ case 'lte':
151
+ if (kind === 'boolean' || kind === 'id')
152
+ return SUPERSET;
153
+ return accepts(kind, value, id) ? wrap(atPath(path, { [RANGE[op]]: value })) : FALSE;
154
+ case 'exists':
155
+ if (value !== false)
156
+ return field.nullable ? wrap(atPath(path, { exists: true })) : TRUE;
157
+ return field.nullable ? wrap(atPath(path, { exists: false })) : FALSE;
158
+ case 'contains':
159
+ if (kind === 'number' || kind === 'boolean')
160
+ return FALSE;
161
+ if (typeof value !== 'string')
162
+ return FALSE;
163
+ // Payload's `contains` ignores case; the contract does not, so it only narrows here.
164
+ return kind === 'string' ? wrap(atPath(path, { contains: value }), true) : SUPERSET;
165
+ default:
166
+ return FALSE;
167
+ }
168
+ }
169
+ /** Thrown for a field the site owner did not allow (or that cannot be queried at all). */
170
+ export class QueryFieldError extends Error {
171
+ }
172
+ /** The field of a query path, or a `QueryFieldError` when it is not allowlisted or does not resolve. */
173
+ export function queryFieldOf(context, path) {
174
+ if (!context.allowed.includes(path)) {
175
+ throw new QueryFieldError(`The field "${path}" of "${context.source}" cannot be queried.`);
176
+ }
177
+ const field = resolveQueryField(context.collections, context.source, path);
178
+ if (field === undefined) {
179
+ throw new QueryFieldError(`The field "${path}" of "${context.source}" cannot be queried.`);
180
+ }
181
+ return field;
182
+ }
183
+ /** Compiles a filter tree; `false` results short-circuit, unsafe conditions widen and flag `post`. */
184
+ export function compileWhere(node, context) {
185
+ if ('and' in node) {
186
+ const parts = node.and.map((child) => compileWhere(child, context));
187
+ if (parts.some((part) => part.c === 'false'))
188
+ return FALSE;
189
+ const wheres = parts.flatMap((part) => (part.c === 'where' ? [part.where] : []));
190
+ const post = parts.some((part) => part.post);
191
+ if (wheres.length === 0)
192
+ return { c: 'true', post };
193
+ return {
194
+ c: 'where',
195
+ where: wheres.length === 1 ? wheres[0] : { and: wheres },
196
+ post,
197
+ };
198
+ }
199
+ if ('or' in node) {
200
+ const parts = node.or.map((child) => compileWhere(child, context));
201
+ const post = parts.some((part) => part.post);
202
+ if (parts.some((part) => part.c === 'true'))
203
+ return { c: 'true', post };
204
+ const wheres = parts.flatMap((part) => (part.c === 'where' ? [part.where] : []));
205
+ if (wheres.length === 0)
206
+ return FALSE;
207
+ return { c: 'where', where: wheres.length === 1 ? wheres[0] : { or: wheres }, post };
208
+ }
209
+ return compileLeaf(queryFieldOf(context, node.field), node.op, node.value, context.id);
210
+ }
@@ -0,0 +1,35 @@
1
+ import { type Theme } from '@next-buildr/core';
2
+ import { type McpBackend } from '@next-buildr/mcp';
3
+ export interface PayloadMcpBackendOptions {
4
+ /** Payload's API root of the site, e.g. `https://example.com/api`. */
5
+ readonly baseUrl: string;
6
+ /** A Payload API key of a dedicated, low-privilege user. It is only ever sent as a header. */
7
+ readonly apiKey: string;
8
+ /** The auth collection the key belongs to (default `users`). */
9
+ readonly collection?: string;
10
+ /**
11
+ * The builder collections to list when `listDocuments` is called without a `collection`
12
+ * (the API has no collection index). Without it such a call is refused as `invalid`.
13
+ */
14
+ readonly collections?: readonly string[];
15
+ /** The theme of the site; the builder API does not serve one, so the default theme unless set. */
16
+ readonly theme?: Theme;
17
+ /** Origin that `previewUrl` prefixes to the document's public path (default: the origin of `baseUrl`). */
18
+ readonly siteUrl?: string;
19
+ /** Per-request timeout (default 15 s). */
20
+ readonly timeoutMs?: number;
21
+ /** Retries of idempotent requests (GET) after a network error, 429, 502, 503 or 504 (default 2). */
22
+ readonly retries?: number;
23
+ /** Defaults to the global `fetch` (looked up per call). */
24
+ readonly fetch?: typeof globalThis.fetch;
25
+ /** Waits between retries; injectable for tests. */
26
+ readonly sleep?: (ms: number) => Promise<void>;
27
+ }
28
+ /**
29
+ * The `McpBackend` of `@next-buildr/mcp` over the builder API of a site running the Payload plugin
30
+ * (docs/mcp.md#http-backend). Authenticates with a Payload API key; every response is checked
31
+ * against the contract and documents with `parseDocument`. Only GETs are retried. The API key is
32
+ * sent as a header only and is scrubbed from every message this backend produces.
33
+ */
34
+ export declare function createPayloadMcpBackend(options: PayloadMcpBackendOptions): McpBackend;
35
+ //# sourceMappingURL=http-backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-backend.d.ts","sourceRoot":"","sources":["../../src/mcp/http-backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,KAAK,EACX,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAWL,KAAK,UAAU,EAIhB,MAAM,kBAAkB,CAAC;AAkB1B,MAAM,WAAW,wBAAwB;IACvC,sEAAsE;IACtE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,kGAAkG;IAClG,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,0GAA0G;IAC1G,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,0CAA0C;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,oGAAoG;IACpG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IACzC,mDAAmD;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAqCD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,UAAU,CA+UrF"}