@next-buildr/payload 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/adapter/adapter.test.d.ts +2 -0
- package/dist/adapter/adapter.test.d.ts.map +1 -0
- package/dist/adapter/adapter.test.js +282 -0
- package/dist/adapter/canvas-data-source.d.ts +9 -0
- package/dist/adapter/canvas-data-source.d.ts.map +1 -0
- package/dist/adapter/canvas-data-source.js +24 -0
- package/dist/adapter/document-adapter.d.ts +20 -0
- package/dist/adapter/document-adapter.d.ts.map +1 -0
- package/dist/adapter/document-adapter.js +183 -0
- package/dist/adapter/http.d.ts +34 -0
- package/dist/adapter/http.d.ts.map +1 -0
- package/dist/adapter/http.js +72 -0
- package/dist/adapter/index.d.ts +4 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +5 -0
- package/dist/admin/index.d.ts +4 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +2 -0
- package/dist/admin/layout-field-connected.d.ts +6 -0
- package/dist/admin/layout-field-connected.d.ts.map +1 -0
- package/dist/admin/layout-field-connected.js +16 -0
- package/dist/admin/layout-field-view.d.ts +16 -0
- package/dist/admin/layout-field-view.d.ts.map +1 -0
- package/dist/admin/layout-field-view.js +14 -0
- package/dist/admin/layout-field.d.ts +6 -0
- package/dist/admin/layout-field.d.ts.map +1 -0
- package/dist/admin/layout-field.js +13 -0
- package/dist/admin/layout-field.test.d.ts +2 -0
- package/dist/admin/layout-field.test.d.ts.map +1 -0
- package/dist/admin/layout-field.test.js +41 -0
- package/dist/contract.d.ts +289 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +176 -0
- package/dist/data/build-context.d.ts +20 -0
- package/dist/data/build-context.d.ts.map +1 -0
- package/dist/data/build-context.js +25 -0
- package/dist/data/data.test.d.ts +2 -0
- package/dist/data/data.test.d.ts.map +1 -0
- package/dist/data/data.test.js +261 -0
- package/dist/data/index.d.ts +10 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +5 -0
- package/dist/data/normalize.d.ts +14 -0
- package/dist/data/normalize.d.ts.map +1 -0
- package/dist/data/normalize.js +128 -0
- package/dist/data/payload-data-source.d.ts +42 -0
- package/dist/data/payload-data-source.d.ts.map +1 -0
- package/dist/data/payload-data-source.js +179 -0
- package/dist/data/payload-data-source.test.d.ts +2 -0
- package/dist/data/payload-data-source.test.d.ts.map +1 -0
- package/dist/data/payload-data-source.test.js +160 -0
- package/dist/data/resolve-layout.d.ts +45 -0
- package/dist/data/resolve-layout.d.ts.map +1 -0
- package/dist/data/resolve-layout.js +111 -0
- package/dist/data/schema-from-fields.d.ts +63 -0
- package/dist/data/schema-from-fields.d.ts.map +1 -0
- package/dist/data/schema-from-fields.js +178 -0
- package/dist/data/where.d.ts +53 -0
- package/dist/data/where.d.ts.map +1 -0
- package/dist/data/where.js +210 -0
- package/dist/mcp/http-backend.d.ts +35 -0
- package/dist/mcp/http-backend.d.ts.map +1 -0
- package/dist/mcp/http-backend.js +337 -0
- package/dist/mcp/http-backend.test.d.ts +2 -0
- package/dist/mcp/http-backend.test.d.ts.map +1 -0
- package/dist/mcp/http-backend.test.js +178 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/route.d.ts +63 -0
- package/dist/mcp/route.d.ts.map +1 -0
- package/dist/mcp/route.js +230 -0
- package/dist/next/get-document.d.ts +64 -0
- package/dist/next/get-document.d.ts.map +1 -0
- package/dist/next/get-document.js +208 -0
- package/dist/next/get-document.test.d.ts +2 -0
- package/dist/next/get-document.test.d.ts.map +1 -0
- package/dist/next/get-document.test.js +155 -0
- package/dist/next/index.d.ts +9 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +4 -0
- package/dist/next/next-helpers.test.d.ts +2 -0
- package/dist/next/next-helpers.test.d.ts.map +1 -0
- package/dist/next/next-helpers.test.js +122 -0
- package/dist/next/revalidate.d.ts +44 -0
- package/dist/next/revalidate.d.ts.map +1 -0
- package/dist/next/revalidate.js +45 -0
- package/dist/next/seo.d.ts +43 -0
- package/dist/next/seo.d.ts.map +1 -0
- package/dist/next/seo.js +53 -0
- package/dist/next/tags.d.ts +23 -0
- package/dist/next/tags.d.ts.map +1 -0
- package/dist/next/tags.js +31 -0
- package/dist/plugin/access.d.ts +19 -0
- package/dist/plugin/access.d.ts.map +1 -0
- package/dist/plugin/access.js +32 -0
- package/dist/plugin/collections/form-submissions.d.ts +10 -0
- package/dist/plugin/collections/form-submissions.d.ts.map +1 -0
- package/dist/plugin/collections/form-submissions.js +37 -0
- package/dist/plugin/collections/templates.d.ts +13 -0
- package/dist/plugin/collections/templates.d.ts.map +1 -0
- package/dist/plugin/collections/templates.js +118 -0
- package/dist/plugin/endpoints/access.test.d.ts +2 -0
- package/dist/plugin/endpoints/access.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/access.test.js +91 -0
- package/dist/plugin/endpoints/adapter-contract.test.d.ts +2 -0
- package/dist/plugin/endpoints/adapter-contract.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/adapter-contract.test.js +45 -0
- package/dist/plugin/endpoints/context.d.ts +65 -0
- package/dist/plugin/endpoints/context.d.ts.map +1 -0
- package/dist/plugin/endpoints/context.js +113 -0
- package/dist/plugin/endpoints/data-context.d.ts +8 -0
- package/dist/plugin/endpoints/data-context.d.ts.map +1 -0
- package/dist/plugin/endpoints/data-context.js +63 -0
- package/dist/plugin/endpoints/data-endpoints.test.d.ts +2 -0
- package/dist/plugin/endpoints/data-endpoints.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/data-endpoints.test.js +125 -0
- package/dist/plugin/endpoints/data-query.test.d.ts +2 -0
- package/dist/plugin/endpoints/data-query.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/data-query.test.js +96 -0
- package/dist/plugin/endpoints/data-schema.d.ts +5 -0
- package/dist/plugin/endpoints/data-schema.d.ts.map +1 -0
- package/dist/plugin/endpoints/data-schema.js +21 -0
- package/dist/plugin/endpoints/data.d.ts +10 -0
- package/dist/plugin/endpoints/data.d.ts.map +1 -0
- package/dist/plugin/endpoints/data.js +96 -0
- package/dist/plugin/endpoints/document.d.ts +11 -0
- package/dist/plugin/endpoints/document.d.ts.map +1 -0
- package/dist/plugin/endpoints/document.js +119 -0
- package/dist/plugin/endpoints/documents.d.ts +10 -0
- package/dist/plugin/endpoints/documents.d.ts.map +1 -0
- package/dist/plugin/endpoints/documents.js +145 -0
- package/dist/plugin/endpoints/endpoints.test-kit.d.ts +42 -0
- package/dist/plugin/endpoints/endpoints.test-kit.d.ts.map +1 -0
- package/dist/plugin/endpoints/endpoints.test-kit.js +141 -0
- package/dist/plugin/endpoints/endpoints.test.d.ts +2 -0
- package/dist/plugin/endpoints/endpoints.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/endpoints.test.js +209 -0
- package/dist/plugin/endpoints/forms.d.ts +16 -0
- package/dist/plugin/endpoints/forms.d.ts.map +1 -0
- package/dist/plugin/endpoints/forms.js +199 -0
- package/dist/plugin/endpoints/forms.test.d.ts +2 -0
- package/dist/plugin/endpoints/forms.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/forms.test.js +346 -0
- package/dist/plugin/endpoints/guards.d.ts +9 -0
- package/dist/plugin/endpoints/guards.d.ts.map +1 -0
- package/dist/plugin/endpoints/guards.js +35 -0
- package/dist/plugin/endpoints/index.d.ts +6 -0
- package/dist/plugin/endpoints/index.d.ts.map +1 -0
- package/dist/plugin/endpoints/index.js +32 -0
- package/dist/plugin/endpoints/locales-no-fallback.test.d.ts +2 -0
- package/dist/plugin/endpoints/locales-no-fallback.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/locales-no-fallback.test.js +30 -0
- package/dist/plugin/endpoints/locales.test.d.ts +2 -0
- package/dist/plugin/endpoints/locales.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/locales.test.js +128 -0
- package/dist/plugin/endpoints/manifest.d.ts +5 -0
- package/dist/plugin/endpoints/manifest.d.ts.map +1 -0
- package/dist/plugin/endpoints/manifest.js +18 -0
- package/dist/plugin/endpoints/mcp-http-backend.test.d.ts +2 -0
- package/dist/plugin/endpoints/mcp-http-backend.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/mcp-http-backend.test.js +84 -0
- package/dist/plugin/endpoints/mcp-off.test.d.ts +2 -0
- package/dist/plugin/endpoints/mcp-off.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/mcp-off.test.js +33 -0
- package/dist/plugin/endpoints/mcp-publish.test.d.ts +2 -0
- package/dist/plugin/endpoints/mcp-publish.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/mcp-publish.test.js +42 -0
- package/dist/plugin/endpoints/mcp-route.test.d.ts +2 -0
- package/dist/plugin/endpoints/mcp-route.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/mcp-route.test.js +132 -0
- package/dist/plugin/endpoints/mcp.test.d.ts +2 -0
- package/dist/plugin/endpoints/mcp.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/mcp.test.js +231 -0
- package/dist/plugin/endpoints/media.d.ts +7 -0
- package/dist/plugin/endpoints/media.d.ts.map +1 -0
- package/dist/plugin/endpoints/media.js +115 -0
- package/dist/plugin/endpoints/media.test.d.ts +2 -0
- package/dist/plugin/endpoints/media.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/media.test.js +95 -0
- package/dist/plugin/endpoints/publish-a11y.test.d.ts +2 -0
- package/dist/plugin/endpoints/publish-a11y.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/publish-a11y.test.js +27 -0
- package/dist/plugin/endpoints/publish.d.ts +9 -0
- package/dist/plugin/endpoints/publish.d.ts.map +1 -0
- package/dist/plugin/endpoints/publish.js +81 -0
- package/dist/plugin/endpoints/rename.d.ts +9 -0
- package/dist/plugin/endpoints/rename.d.ts.map +1 -0
- package/dist/plugin/endpoints/rename.js +47 -0
- package/dist/plugin/endpoints/respond.d.ts +9 -0
- package/dist/plugin/endpoints/respond.d.ts.map +1 -0
- package/dist/plugin/endpoints/respond.js +10 -0
- package/dist/plugin/endpoints/samples.d.ts +5 -0
- package/dist/plugin/endpoints/samples.d.ts.map +1 -0
- package/dist/plugin/endpoints/samples.js +42 -0
- package/dist/plugin/endpoints/save.d.ts +9 -0
- package/dist/plugin/endpoints/save.d.ts.map +1 -0
- package/dist/plugin/endpoints/save.js +58 -0
- package/dist/plugin/endpoints/session.d.ts +5 -0
- package/dist/plugin/endpoints/session.d.ts.map +1 -0
- package/dist/plugin/endpoints/session.js +30 -0
- package/dist/plugin/endpoints/templates.test.d.ts +2 -0
- package/dist/plugin/endpoints/templates.test.d.ts.map +1 -0
- package/dist/plugin/endpoints/templates.test.js +191 -0
- package/dist/plugin/fields.d.ts +28 -0
- package/dist/plugin/fields.d.ts.map +1 -0
- package/dist/plugin/fields.js +62 -0
- package/dist/plugin/forms/forms.test.d.ts +2 -0
- package/dist/plugin/forms/forms.test.d.ts.map +1 -0
- package/dist/plugin/forms/forms.test.js +88 -0
- package/dist/plugin/forms/notify.d.ts +6 -0
- package/dist/plugin/forms/notify.d.ts.map +1 -0
- package/dist/plugin/forms/notify.js +13 -0
- package/dist/plugin/forms/rate-limit.d.ts +24 -0
- package/dist/plugin/forms/rate-limit.d.ts.map +1 -0
- package/dist/plugin/forms/rate-limit.js +36 -0
- package/dist/plugin/forms/validate.d.ts +22 -0
- package/dist/plugin/forms/validate.d.ts.map +1 -0
- package/dist/plugin/forms/validate.js +79 -0
- package/dist/plugin/hooks/layout-hook.d.ts +9 -0
- package/dist/plugin/hooks/layout-hook.d.ts.map +1 -0
- package/dist/plugin/hooks/layout-hook.js +33 -0
- package/dist/plugin/hooks/layout-hook.test.d.ts +2 -0
- package/dist/plugin/hooks/layout-hook.test.d.ts.map +1 -0
- package/dist/plugin/hooks/layout-hook.test.js +191 -0
- package/dist/plugin/hooks/process-layout.d.ts +32 -0
- package/dist/plugin/hooks/process-layout.d.ts.map +1 -0
- package/dist/plugin/hooks/process-layout.js +68 -0
- package/dist/plugin/hooks/read-layout.d.ts +17 -0
- package/dist/plugin/hooks/read-layout.d.ts.map +1 -0
- package/dist/plugin/hooks/read-layout.js +39 -0
- package/dist/plugin/index.d.ts +18 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +105 -0
- package/dist/plugin/locales.d.ts +32 -0
- package/dist/plugin/locales.d.ts.map +1 -0
- package/dist/plugin/locales.js +56 -0
- package/dist/plugin/options.d.ts +77 -0
- package/dist/plugin/options.d.ts.map +1 -0
- package/dist/plugin/options.js +123 -0
- package/dist/plugin/plugin.test.d.ts +2 -0
- package/dist/plugin/plugin.test.d.ts.map +1 -0
- package/dist/plugin/plugin.test.js +186 -0
- package/dist/plugin/write-guard.d.ts +14 -0
- package/dist/plugin/write-guard.d.ts.map +1 -0
- package/dist/plugin/write-guard.js +18 -0
- package/package.json +107 -3
- package/README.md +0 -3
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createMemoryDataSource, MAX_QUERY_LIMIT, } from '@next-buildr/core';
|
|
2
|
+
import { normalizeDoc, normalizeMedia } from './normalize.js';
|
|
3
|
+
import { isExposedCollection } from './schema-from-fields.js';
|
|
4
|
+
import { compileWhere, QueryFieldError, queryFieldOf, } from './where.js';
|
|
5
|
+
const DEFAULT_SCAN_LIMIT = 1000;
|
|
6
|
+
/** A query that cannot be answered: not allowlisted, malformed, or too broad. */
|
|
7
|
+
export class DataQueryError extends Error {
|
|
8
|
+
}
|
|
9
|
+
/** The Payload collections as the mapping sees them. */
|
|
10
|
+
const collectionsOf = (payload) => payload.config.collections;
|
|
11
|
+
/**
|
|
12
|
+
* The server `DataSource` (ADR-018): queries and media served from Payload's Local API, held to
|
|
13
|
+
* the same contract suite as `MemoryDataSource`. Every call runs with the access of the request's
|
|
14
|
+
* user (`overrideAccess: false`); production reads published documents only.
|
|
15
|
+
*
|
|
16
|
+
* Conditions Payload evaluates like the contract (typed scalars, missing values) go to the
|
|
17
|
+
* database, so paging and totals are the database's. What it cannot (a case-sensitive `contains`,
|
|
18
|
+
* lists, an optional sort key: missing values must sort last in both directions) is decided by the
|
|
19
|
+
* memory source over at most `scanLimit` candidates the database already narrowed.
|
|
20
|
+
*/
|
|
21
|
+
export function createPayloadDataSource(options) {
|
|
22
|
+
const { payload } = options;
|
|
23
|
+
const collections = collectionsOf(payload);
|
|
24
|
+
const scanLimit = options.scanLimit ?? DEFAULT_SCAN_LIMIT;
|
|
25
|
+
const scope = (ctx) => ({
|
|
26
|
+
...(options.req === undefined ? {} : { req: options.req }),
|
|
27
|
+
overrideAccess: false,
|
|
28
|
+
...(payload.config.localization && ctx.locale !== ''
|
|
29
|
+
? {
|
|
30
|
+
locale: ctx.locale,
|
|
31
|
+
...(payload.config.localization.fallback === false
|
|
32
|
+
? { fallbackLocale: false }
|
|
33
|
+
: {}),
|
|
34
|
+
}
|
|
35
|
+
: {}),
|
|
36
|
+
});
|
|
37
|
+
const idKind = (slug) => {
|
|
38
|
+
const custom = payload.collections[slug]?.customIDType;
|
|
39
|
+
return { kind: (custom ?? payload.db.defaultIDType) === 'number' ? 'number' : 'text' };
|
|
40
|
+
};
|
|
41
|
+
async function query(spec, ctx) {
|
|
42
|
+
const queryable = Object.hasOwn(options.queryable, spec.source)
|
|
43
|
+
? options.queryable[spec.source]
|
|
44
|
+
: undefined;
|
|
45
|
+
const collection = payload.collections[spec.source];
|
|
46
|
+
const own = collections.find((candidate) => candidate.slug === spec.source);
|
|
47
|
+
if (queryable === undefined ||
|
|
48
|
+
collection === undefined ||
|
|
49
|
+
own === undefined ||
|
|
50
|
+
!isExposedCollection(own)) {
|
|
51
|
+
throw new DataQueryError(`The collection "${spec.source}" cannot be queried.`);
|
|
52
|
+
}
|
|
53
|
+
if (!Number.isInteger(spec.limit) || spec.limit < 1 || spec.limit > MAX_QUERY_LIMIT) {
|
|
54
|
+
throw new DataQueryError(`The limit must be between 1 and ${MAX_QUERY_LIMIT}.`);
|
|
55
|
+
}
|
|
56
|
+
if (!Number.isInteger(spec.page) || spec.page < 1) {
|
|
57
|
+
throw new DataQueryError('The page must be 1 or more.');
|
|
58
|
+
}
|
|
59
|
+
const id = idKind(spec.source);
|
|
60
|
+
const context = {
|
|
61
|
+
collections,
|
|
62
|
+
source: spec.source,
|
|
63
|
+
allowed: queryable.fields,
|
|
64
|
+
id,
|
|
65
|
+
};
|
|
66
|
+
let compiled = { c: 'true', post: false };
|
|
67
|
+
let sortNeedsScan = false;
|
|
68
|
+
const sortKeys = [];
|
|
69
|
+
try {
|
|
70
|
+
if (spec.where !== undefined)
|
|
71
|
+
compiled = compileWhere(spec.where, context);
|
|
72
|
+
for (const { field, dir } of spec.sort ?? []) {
|
|
73
|
+
if (!queryable.sort.includes(field)) {
|
|
74
|
+
throw new QueryFieldError(`The field "${field}" of "${spec.source}" cannot be sorted by.`);
|
|
75
|
+
}
|
|
76
|
+
const resolved = queryFieldOf({ ...context, allowed: [field] }, field);
|
|
77
|
+
sortNeedsScan ||= resolved.nullable || resolved.kind === 'unsafe';
|
|
78
|
+
sortKeys.push(`${dir === 'desc' ? '-' : ''}${resolved.payloadPath}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof QueryFieldError)
|
|
83
|
+
throw new DataQueryError(error.message);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
if (compiled.c === 'false') {
|
|
87
|
+
return { items: [], total: 0, page: spec.page, totalPages: 0 };
|
|
88
|
+
}
|
|
89
|
+
const conditions = compiled.c === 'where' ? [compiled.where] : [];
|
|
90
|
+
if (spec.excludeId !== undefined) {
|
|
91
|
+
const excluded = String(spec.excludeId);
|
|
92
|
+
if (id.kind === 'text' || /^\d+$/.test(excluded)) {
|
|
93
|
+
conditions.push({
|
|
94
|
+
id: { not_equals: id.kind === 'number' ? Number(excluded) : excluded },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const preview = ctx.mode !== 'production';
|
|
99
|
+
const hasDrafts = Boolean(collection.config.versions && collection.config.versions.drafts);
|
|
100
|
+
if (hasDrafts && !preview)
|
|
101
|
+
conditions.push({ _status: { equals: 'published' } });
|
|
102
|
+
const where = conditions.length === 0
|
|
103
|
+
? undefined
|
|
104
|
+
: conditions.length === 1
|
|
105
|
+
? conditions[0]
|
|
106
|
+
: { and: conditions };
|
|
107
|
+
const depth = Math.max(1, options.depth ?? 1);
|
|
108
|
+
const normalize = (doc) => {
|
|
109
|
+
const record = doc;
|
|
110
|
+
const item = normalizeDoc({ collections }, own.fields, record, options.contextNames ?? {});
|
|
111
|
+
const path = options.itemPath?.(spec.source, record, ctx.locale);
|
|
112
|
+
return (path === undefined ? item : { ...item, path });
|
|
113
|
+
};
|
|
114
|
+
const common = {
|
|
115
|
+
collection: spec.source,
|
|
116
|
+
depth,
|
|
117
|
+
draft: preview,
|
|
118
|
+
...(where === undefined ? {} : { where }),
|
|
119
|
+
...scope(ctx),
|
|
120
|
+
};
|
|
121
|
+
if (!compiled.post && !sortNeedsScan) {
|
|
122
|
+
const sort = [
|
|
123
|
+
...sortKeys,
|
|
124
|
+
...(sortKeys.some((key) => key.replace('-', '') === 'id') ? [] : ['id']),
|
|
125
|
+
];
|
|
126
|
+
const found = await payload.find({
|
|
127
|
+
...common,
|
|
128
|
+
sort,
|
|
129
|
+
page: spec.page,
|
|
130
|
+
limit: spec.limit,
|
|
131
|
+
pagination: true,
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
items: found.docs.map(normalize),
|
|
135
|
+
total: found.totalDocs,
|
|
136
|
+
page: spec.page,
|
|
137
|
+
totalPages: Math.ceil(found.totalDocs / spec.limit),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const candidates = await payload.find({
|
|
141
|
+
...common,
|
|
142
|
+
sort: 'id',
|
|
143
|
+
page: 1,
|
|
144
|
+
limit: scanLimit + 1,
|
|
145
|
+
pagination: true,
|
|
146
|
+
});
|
|
147
|
+
if (candidates.totalDocs > scanLimit) {
|
|
148
|
+
throw new DataQueryError(`The query needs to look at more than ${scanLimit} documents; narrow the filter.`);
|
|
149
|
+
}
|
|
150
|
+
return createMemoryDataSource({
|
|
151
|
+
collections: { [spec.source]: candidates.docs.map(normalize) },
|
|
152
|
+
}).query(spec, ctx);
|
|
153
|
+
}
|
|
154
|
+
async function getMedia(ids, ctx) {
|
|
155
|
+
const slug = options.mediaCollection;
|
|
156
|
+
if (slug === undefined)
|
|
157
|
+
throw new DataQueryError('No media collection is configured.');
|
|
158
|
+
const kind = idKind(slug).kind;
|
|
159
|
+
const valid = [...new Set(ids)].filter((value) => kind === 'number' ? /^\d+$/.test(value) : value !== '');
|
|
160
|
+
const found = {};
|
|
161
|
+
if (valid.length === 0)
|
|
162
|
+
return found;
|
|
163
|
+
const result = await payload.find({
|
|
164
|
+
collection: slug,
|
|
165
|
+
where: { id: { in: kind === 'number' ? valid.map(Number) : valid } },
|
|
166
|
+
limit: valid.length,
|
|
167
|
+
pagination: false,
|
|
168
|
+
depth: 0,
|
|
169
|
+
...scope(ctx),
|
|
170
|
+
});
|
|
171
|
+
for (const doc of result.docs) {
|
|
172
|
+
const asset = normalizeMedia(doc);
|
|
173
|
+
if (asset !== null)
|
|
174
|
+
found[asset.id] = asset;
|
|
175
|
+
}
|
|
176
|
+
return found;
|
|
177
|
+
}
|
|
178
|
+
return { query, getMedia };
|
|
179
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload-data-source.test.d.ts","sourceRoot":"","sources":["../../src/data/payload-data-source.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { DATA_SOURCE_FIXTURE, runDataSourceContract, } from '@next-buildr/test-utils/contracts/data-source';
|
|
5
|
+
import { sqliteAdapter } from '@payloadcms/db-sqlite';
|
|
6
|
+
import { buildConfig, getPayload } from 'payload';
|
|
7
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
8
|
+
import { createPayloadDataSource, DataQueryError } from './payload-data-source.js';
|
|
9
|
+
const open = { read: () => true };
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), 'buildr-datasource-'));
|
|
11
|
+
let payload;
|
|
12
|
+
const queryable = {
|
|
13
|
+
posts: {
|
|
14
|
+
fields: ['title', 'views', 'tags', 'author.name', 'live'],
|
|
15
|
+
sort: ['title', 'views', 'author.name'],
|
|
16
|
+
},
|
|
17
|
+
notes: { fields: ['title'], sort: ['title'] },
|
|
18
|
+
};
|
|
19
|
+
const source = (extra = {}) => createPayloadDataSource({ payload, queryable, mediaCollection: 'media', ...extra });
|
|
20
|
+
let seeded;
|
|
21
|
+
async function seed(fixture) {
|
|
22
|
+
for (const item of fixture.collections['posts'] ?? []) {
|
|
23
|
+
const { id: _id, ...data } = item;
|
|
24
|
+
await payload.create({ collection: 'posts', data: data });
|
|
25
|
+
}
|
|
26
|
+
for (const asset of Object.values(fixture.media)) {
|
|
27
|
+
await payload.create({ collection: 'media', data: asset });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
const config = buildConfig({
|
|
32
|
+
secret: 'test-secret',
|
|
33
|
+
collections: [
|
|
34
|
+
{
|
|
35
|
+
slug: 'posts',
|
|
36
|
+
access: open,
|
|
37
|
+
fields: [
|
|
38
|
+
{ name: 'title', type: 'text', required: true },
|
|
39
|
+
{ name: 'views', type: 'number', required: true },
|
|
40
|
+
{ name: 'tags', type: 'text', hasMany: true },
|
|
41
|
+
{ name: 'author', type: 'group', fields: [{ name: 'name', type: 'text' }] },
|
|
42
|
+
{ name: 'live', type: 'checkbox', required: true },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
slug: 'media',
|
|
47
|
+
access: open,
|
|
48
|
+
fields: [
|
|
49
|
+
{ name: 'id', type: 'text' },
|
|
50
|
+
{ name: 'url', type: 'text' },
|
|
51
|
+
{ name: 'alt', type: 'text' },
|
|
52
|
+
{ name: 'width', type: 'number' },
|
|
53
|
+
{ name: 'height', type: 'number' },
|
|
54
|
+
{ name: 'mimeType', type: 'text' },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
slug: 'notes',
|
|
59
|
+
access: open,
|
|
60
|
+
fields: [{ name: 'title', type: 'text', required: true }],
|
|
61
|
+
versions: { drafts: true },
|
|
62
|
+
},
|
|
63
|
+
{ slug: 'guarded', fields: [{ name: 'title', type: 'text' }] },
|
|
64
|
+
],
|
|
65
|
+
db: sqliteAdapter({ client: { url: `file:${join(dir, 'db.sqlite')}` } }),
|
|
66
|
+
typescript: { autoGenerate: false },
|
|
67
|
+
});
|
|
68
|
+
payload = await getPayload({ config, key: 'datasource' });
|
|
69
|
+
seeded = seed(DATA_SOURCE_FIXTURE);
|
|
70
|
+
await seeded;
|
|
71
|
+
});
|
|
72
|
+
afterAll(() => {
|
|
73
|
+
try {
|
|
74
|
+
rmSync(dir, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Windows keeps the database file open a moment longer.
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// The same contract MemoryDataSource is held to (posts and media are seeded once, above).
|
|
81
|
+
runDataSourceContract('PayloadDataSource', async () => {
|
|
82
|
+
await seeded;
|
|
83
|
+
return source();
|
|
84
|
+
});
|
|
85
|
+
const CTX = { locale: 'en', mode: 'production' };
|
|
86
|
+
const spec = (query = {}) => ({
|
|
87
|
+
source: 'posts',
|
|
88
|
+
limit: 50,
|
|
89
|
+
page: 1,
|
|
90
|
+
...query,
|
|
91
|
+
});
|
|
92
|
+
const run = (query, data = source()) => data.query(spec(query), CTX);
|
|
93
|
+
describe('the allowlist', () => {
|
|
94
|
+
it('rejects a field, a sort key or a collection that was not allowed', async () => {
|
|
95
|
+
await expect(run({ where: { field: 'id', op: 'eq', value: '1' } })).rejects.toThrow(DataQueryError);
|
|
96
|
+
await expect(run({ where: { field: 'author', op: 'exists', value: true } })).rejects.toThrow(DataQueryError);
|
|
97
|
+
await expect(run({ sort: [{ field: 'live', dir: 'asc' }] })).rejects.toThrow(DataQueryError);
|
|
98
|
+
await expect(run({ source: 'guarded' })).rejects.toThrow(DataQueryError);
|
|
99
|
+
await expect(run({ source: 'users' })).rejects.toThrow(DataQueryError);
|
|
100
|
+
});
|
|
101
|
+
it('never queries a credential-like field even when it is allowed', async () => {
|
|
102
|
+
const data = source({ queryable: { posts: { fields: ['password'], sort: [] } } });
|
|
103
|
+
await expect(run({ where: { field: 'password', op: 'eq', value: 'x' } }, data)).rejects.toThrow(DataQueryError);
|
|
104
|
+
});
|
|
105
|
+
it('rejects a limit or a page out of range', async () => {
|
|
106
|
+
await expect(run({ limit: 0 })).rejects.toThrow(DataQueryError);
|
|
107
|
+
await expect(run({ limit: 51 })).rejects.toThrow(DataQueryError);
|
|
108
|
+
await expect(run({ page: 0 })).rejects.toThrow(DataQueryError);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('item paths', () => {
|
|
112
|
+
it('gives every item the address the application computes, and none without one', async () => {
|
|
113
|
+
const withPath = source({
|
|
114
|
+
itemPath: (collection, doc, locale) => `/${locale}/${collection}/${String(doc['title'])}`,
|
|
115
|
+
});
|
|
116
|
+
const items = (await run({ limit: 1 }, withPath)).items;
|
|
117
|
+
expect(items[0]?.path).toBe(`/en/posts/${items[0]?.title}`);
|
|
118
|
+
const plain = (await run({ limit: 1 })).items;
|
|
119
|
+
expect(plain[0]).not.toHaveProperty('path');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('access, drafts and the in-memory pass', () => {
|
|
123
|
+
it('reads with the access of the request: a collection closed to visitors fails', async () => {
|
|
124
|
+
await payload.create({ collection: 'guarded', data: { title: 'x' } });
|
|
125
|
+
const data = source({ queryable: { guarded: { fields: ['title'], sort: [] } } });
|
|
126
|
+
await expect(run({ source: 'guarded' }, data)).rejects.toThrow();
|
|
127
|
+
});
|
|
128
|
+
it('serves published documents in production and drafts in preview and canvas', async () => {
|
|
129
|
+
await payload.create({
|
|
130
|
+
collection: 'notes',
|
|
131
|
+
data: { title: 'Live', _status: 'published' },
|
|
132
|
+
});
|
|
133
|
+
await payload.create({
|
|
134
|
+
collection: 'notes',
|
|
135
|
+
data: { title: 'Wip', _status: 'draft' },
|
|
136
|
+
draft: true,
|
|
137
|
+
});
|
|
138
|
+
const titles = async (mode) => {
|
|
139
|
+
const result = await source().query(spec({ source: 'notes' }), { locale: 'en', mode });
|
|
140
|
+
return result.items.map((item) => item.title).sort();
|
|
141
|
+
};
|
|
142
|
+
expect(await titles('production')).toEqual(['Live']);
|
|
143
|
+
expect(await titles('preview')).toEqual(['Live', 'Wip']);
|
|
144
|
+
expect(await titles('canvas')).toEqual(['Live', 'Wip']);
|
|
145
|
+
});
|
|
146
|
+
it('refuses to scan more than scanLimit documents', async () => {
|
|
147
|
+
const data = source({ scanLimit: 3 });
|
|
148
|
+
await expect(run({ where: { field: 'title', op: 'contains', value: 'a' } }, data)).rejects.toThrow(DataQueryError);
|
|
149
|
+
// Narrowed by the database first, the same query fits.
|
|
150
|
+
const narrowed = await run({
|
|
151
|
+
where: {
|
|
152
|
+
and: [
|
|
153
|
+
{ field: 'views', op: 'gte', value: 20 },
|
|
154
|
+
{ field: 'title', op: 'contains', value: 'a' },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
}, data);
|
|
158
|
+
expect(narrowed.items.length).toBeGreaterThan(0);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest } from 'payload';
|
|
2
|
+
/** The collection of reusable layouts (added by the plugin). */
|
|
3
|
+
export declare const TEMPLATES_COLLECTION = "buildr-templates";
|
|
4
|
+
/** Where the layout of a document came from, in the order `resolveLayout` tries them. */
|
|
5
|
+
export type LayoutSource = 'document' | 'template' | 'default-template' | 'builtin';
|
|
6
|
+
export interface ResolvedLayout {
|
|
7
|
+
/** The stored layout as it is (not migrated or validated), or the built-in one. */
|
|
8
|
+
readonly layout: unknown;
|
|
9
|
+
readonly source: LayoutSource;
|
|
10
|
+
/**
|
|
11
|
+
* The document the layout came from as `{collection}:{id}` (the `layoutRef` forms and cache tags
|
|
12
|
+
* use); `null` for the built-in layout, which belongs to no document.
|
|
13
|
+
*/
|
|
14
|
+
readonly layoutRef: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ResolveLayoutInput {
|
|
17
|
+
readonly payload: Payload;
|
|
18
|
+
/** The request whose access applies; without one the read is the visitor's (anonymous). */
|
|
19
|
+
readonly req?: PayloadRequest | undefined;
|
|
20
|
+
readonly collection: string;
|
|
21
|
+
/** The document: at least `id`, `layout` and `template` (an id or a populated template). */
|
|
22
|
+
readonly doc: Readonly<Record<string, unknown>>;
|
|
23
|
+
/** The name the document is bound under (`post`, `page`): the built-in layout binds to it. */
|
|
24
|
+
readonly contextName: string;
|
|
25
|
+
/** Read the latest draft of the templates (the editor, a preview) instead of the published ones. */
|
|
26
|
+
readonly draft?: boolean | undefined;
|
|
27
|
+
/** `true` skips collection access: for a trusted server-side render. Default `false`. */
|
|
28
|
+
readonly overrideAccess?: boolean | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** A layout has content when its root has at least one child; a bare root is what "no layout" looks like. */
|
|
31
|
+
export declare function hasContent(layout: unknown): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The built-in minimal layout (title and body) a document falls back to when neither it nor a
|
|
34
|
+
* template has one: a level-1 heading bound to `{context}.title` and a rich text bound to
|
|
35
|
+
* `{context}.content`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function builtinLayout(contextName: string): unknown;
|
|
38
|
+
/**
|
|
39
|
+
* The layout a document is shown with (docs/payload.md): its own (`layout`, when it has content),
|
|
40
|
+
* else the template it points at, else the collection's default template (`isDefault`), else the
|
|
41
|
+
* built-in minimal one. A template of another collection is never used. Unless `draft`, only
|
|
42
|
+
* published templates count.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveLayout(input: ResolveLayoutInput): Promise<ResolvedLayout>;
|
|
45
|
+
//# sourceMappingURL=resolve-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-layout.d.ts","sourceRoot":"","sources":["../../src/data/resolve-layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAS,MAAM,SAAS,CAAC;AAE9D,gEAAgE;AAChE,eAAO,MAAM,oBAAoB,qBAAqB,CAAC;AAEvD,yFAAyF;AACzF,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,UAAU,GAAG,kBAAkB,GAAG,SAAS,CAAC;AAEpF,MAAM,WAAW,cAAc;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,2FAA2F;IAC3F,QAAQ,CAAC,GAAG,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,4FAA4F;IAC5F,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChD,8FAA8F;IAC9F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,oGAAoG;IACpG,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACrC,yFAAyF;IACzF,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC/C;AAKD,6GAA6G;AAC7G,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAOnD;AAOD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAwB1D;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CA+DtF"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** The collection of reusable layouts (added by the plugin). */
|
|
2
|
+
export const TEMPLATES_COLLECTION = 'buildr-templates';
|
|
3
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
/** A layout has content when its root has at least one child; a bare root is what "no layout" looks like. */
|
|
5
|
+
export function hasContent(layout) {
|
|
6
|
+
if (!isRecord(layout) || !isRecord(layout['nodes']))
|
|
7
|
+
return false;
|
|
8
|
+
const root = layout['nodes']['root'];
|
|
9
|
+
if (!isRecord(root) || !isRecord(root['slots']))
|
|
10
|
+
return false;
|
|
11
|
+
return Object.values(root['slots']).some((children) => Array.isArray(children) && children.length > 0);
|
|
12
|
+
}
|
|
13
|
+
const idOf = (value) => {
|
|
14
|
+
const id = isRecord(value) ? value['id'] : value;
|
|
15
|
+
return typeof id === 'string' || typeof id === 'number' ? id : undefined;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The built-in minimal layout (title and body) a document falls back to when neither it nor a
|
|
19
|
+
* template has one: a level-1 heading bound to `{context}.title` and a rich text bound to
|
|
20
|
+
* `{context}.content`.
|
|
21
|
+
*/
|
|
22
|
+
export function builtinLayout(contextName) {
|
|
23
|
+
return {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
root: 'root',
|
|
26
|
+
nodes: {
|
|
27
|
+
root: { id: 'root', type: 'buildr/page', slots: { default: ['bTitle0001', 'bBody00001'] } },
|
|
28
|
+
bTitle0001: {
|
|
29
|
+
id: 'bTitle0001',
|
|
30
|
+
type: 'buildr/heading',
|
|
31
|
+
name: 'Title',
|
|
32
|
+
props: {
|
|
33
|
+
text: { kind: 'binding', path: `${contextName}.title` },
|
|
34
|
+
level: { kind: 'static', value: 1 },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
bBody00001: {
|
|
38
|
+
id: 'bBody00001',
|
|
39
|
+
type: 'buildr/rich-text',
|
|
40
|
+
name: 'Body',
|
|
41
|
+
props: { content: { kind: 'binding', path: `${contextName}.content` } },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
components: { 'buildr/page': 1, 'buildr/heading': 1, 'buildr/rich-text': 1 },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The layout a document is shown with (docs/payload.md): its own (`layout`, when it has content),
|
|
49
|
+
* else the template it points at, else the collection's default template (`isDefault`), else the
|
|
50
|
+
* built-in minimal one. A template of another collection is never used. Unless `draft`, only
|
|
51
|
+
* published templates count.
|
|
52
|
+
*/
|
|
53
|
+
export async function resolveLayout(input) {
|
|
54
|
+
const { payload, collection, doc } = input;
|
|
55
|
+
const ownId = idOf(doc['id']);
|
|
56
|
+
if (hasContent(doc['layout'])) {
|
|
57
|
+
return {
|
|
58
|
+
layout: doc['layout'],
|
|
59
|
+
source: 'document',
|
|
60
|
+
layoutRef: ownId === undefined ? null : `${collection}:${String(ownId)}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (payload.collections[TEMPLATES_COLLECTION] === undefined) {
|
|
64
|
+
return { layout: builtinLayout(input.contextName), source: 'builtin', layoutRef: null };
|
|
65
|
+
}
|
|
66
|
+
const access = {
|
|
67
|
+
draft: input.draft === true,
|
|
68
|
+
overrideAccess: input.overrideAccess === true,
|
|
69
|
+
depth: 0,
|
|
70
|
+
...(input.req === undefined ? {} : { req: input.req }),
|
|
71
|
+
};
|
|
72
|
+
const asResolved = (template, source) => {
|
|
73
|
+
if (template === undefined || template['targetCollection'] !== collection)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (input.draft !== true && template['_status'] !== 'published')
|
|
76
|
+
return undefined;
|
|
77
|
+
if (!hasContent(template['layout']))
|
|
78
|
+
return undefined;
|
|
79
|
+
return {
|
|
80
|
+
layout: template['layout'],
|
|
81
|
+
source,
|
|
82
|
+
layoutRef: `${TEMPLATES_COLLECTION}:${String(template['id'])}`,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const templateId = idOf(doc['template']);
|
|
86
|
+
if (templateId !== undefined) {
|
|
87
|
+
const found = await payload
|
|
88
|
+
.findByID({ collection: TEMPLATES_COLLECTION, id: templateId, ...access })
|
|
89
|
+
.catch(() => undefined);
|
|
90
|
+
const own = asResolved(found, 'template');
|
|
91
|
+
if (own !== undefined)
|
|
92
|
+
return own;
|
|
93
|
+
}
|
|
94
|
+
const defaults = await payload.find({
|
|
95
|
+
collection: TEMPLATES_COLLECTION,
|
|
96
|
+
where: {
|
|
97
|
+
and: [
|
|
98
|
+
{ targetCollection: { equals: collection } },
|
|
99
|
+
{ isDefault: { equals: true } },
|
|
100
|
+
...(input.draft === true ? [] : [{ _status: { equals: 'published' } }]),
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
limit: 1,
|
|
104
|
+
pagination: false,
|
|
105
|
+
...access,
|
|
106
|
+
});
|
|
107
|
+
const fallback = asResolved(defaults.docs[0], 'default-template');
|
|
108
|
+
if (fallback !== undefined)
|
|
109
|
+
return fallback;
|
|
110
|
+
return { layout: builtinLayout(input.contextName), source: 'builtin', layoutRef: null };
|
|
111
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { DataSchema } from '@next-buildr/core';
|
|
2
|
+
/**
|
|
3
|
+
* The part of a (sanitized) Payload field this module reads. Payload's own `Field` union is large
|
|
4
|
+
* and changes between minor versions; reading through this shape keeps the mapping in one place
|
|
5
|
+
* and the dedicated tests pin the behaviour.
|
|
6
|
+
*/
|
|
7
|
+
export interface FieldLike {
|
|
8
|
+
readonly type: string;
|
|
9
|
+
readonly name?: string;
|
|
10
|
+
readonly hidden?: boolean;
|
|
11
|
+
readonly label?: unknown;
|
|
12
|
+
readonly access?: {
|
|
13
|
+
readonly read?: unknown;
|
|
14
|
+
};
|
|
15
|
+
readonly fields?: readonly FieldLike[];
|
|
16
|
+
readonly tabs?: readonly {
|
|
17
|
+
readonly name?: string;
|
|
18
|
+
readonly fields: readonly FieldLike[];
|
|
19
|
+
}[];
|
|
20
|
+
readonly options?: readonly unknown[];
|
|
21
|
+
readonly hasMany?: boolean;
|
|
22
|
+
readonly relationTo?: unknown;
|
|
23
|
+
readonly required?: boolean;
|
|
24
|
+
readonly defaultValue?: unknown;
|
|
25
|
+
}
|
|
26
|
+
/** A collection as the mapping sees it. */
|
|
27
|
+
export interface CollectionLike {
|
|
28
|
+
readonly slug: string;
|
|
29
|
+
readonly auth?: unknown;
|
|
30
|
+
readonly fields: readonly FieldLike[];
|
|
31
|
+
}
|
|
32
|
+
export interface SchemaSource {
|
|
33
|
+
readonly collections: readonly CollectionLike[];
|
|
34
|
+
readonly globals?: readonly {
|
|
35
|
+
readonly slug: string;
|
|
36
|
+
readonly fields: readonly FieldLike[];
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
export interface SchemaOptions {
|
|
40
|
+
/** The name each configured collection is bound under (`page`, `post`, ...); others keep their slug. */
|
|
41
|
+
readonly contextNames: Readonly<Record<string, string>>;
|
|
42
|
+
/** The global that becomes the `site` scope. */
|
|
43
|
+
readonly siteGlobal?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Whether a collection may take part in the schema and in contexts at all: never the auth ones. */
|
|
46
|
+
export declare const isExposedCollection: (collection: CollectionLike) => boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The named fields of `fields` a binding may reach, layout containers (`row`, `collapsible`, tabs)
|
|
49
|
+
* flattened; a named tab is a `group`. This is the allow-list both the schema and the context use.
|
|
50
|
+
*/
|
|
51
|
+
export declare function exposedFields(fields: readonly FieldLike[]): readonly (FieldLike & {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
})[];
|
|
54
|
+
/** The slug a relation points at when it can be followed (a single, exposed target). */
|
|
55
|
+
export declare function relationTarget(field: FieldLike, source: SchemaSource): string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The `DataSchema` of a collection's documents: the scopes `site` (when the site global exists),
|
|
58
|
+
* the collection's own context name, and `route`; plus one entity per relation target that can be
|
|
59
|
+
* reached (transitively). Auth collections (`users`), hidden fields and anything that looks like a
|
|
60
|
+
* credential are never included; a relation to them is dropped.
|
|
61
|
+
*/
|
|
62
|
+
export declare function schemaFromCollection(source: SchemaSource, collection: string, options: SchemaOptions): DataSchema | undefined;
|
|
63
|
+
//# sourceMappingURL=schema-from-fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-from-fields.d.ts","sourceRoot":"","sources":["../../src/data/schema-from-fields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,UAAU,EAAY,MAAM,mBAAmB,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;IACvC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAA;KAAE,EAAE,CAAC;IAC7F,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,WAAW,EAAE,SAAS,cAAc,EAAE,CAAC;IAChD,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAA;KAAE,EAAE,CAAC;CAChG;AAED,MAAM,WAAW,aAAa;IAC5B,wGAAwG;IACxG,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,gDAAgD;IAChD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAWD,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,eAAgB,cAAc,KAAG,OACL,CAAC;AAe7D;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,SAAS,SAAS,EAAE,GAC3B,SAAS,CAAC,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,EAAE,CAoBpD;AAiBD,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAIzF;AAyFD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,aAAa,GACrB,UAAU,GAAG,SAAS,CAgCxB"}
|