@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.
- package/LICENSE +21 -0
- package/README.md +4 -2
- 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 +108 -3
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { mediaListQuerySchema, mediaListResponseSchema } from '../../contract.js';
|
|
2
|
+
import { normalizeMedia } from '../../data/index.js';
|
|
3
|
+
import { chooseLocale } from '../locales.js';
|
|
4
|
+
import { allowed } from './context.js';
|
|
5
|
+
import { mutationGuard } from './guards.js';
|
|
6
|
+
import { fail, json, unauthorized } from './respond.js';
|
|
7
|
+
const PAGE_SIZE = 24;
|
|
8
|
+
/** Uploads are buffered by the server; a serverless host may impose a lower limit of its own. */
|
|
9
|
+
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
const TYPE_PREFIX = { image: 'image/', video: 'video/', audio: 'audio/' };
|
|
11
|
+
/** The media collection, or the response to send when the builder has none or the caller may not edit. */
|
|
12
|
+
async function mediaOf(env, req) {
|
|
13
|
+
if (req.user === null || req.user === undefined)
|
|
14
|
+
return { ok: false, response: unauthorized() };
|
|
15
|
+
const slug = env.options.media?.collection;
|
|
16
|
+
if (slug === undefined || req.payload.collections[slug] === undefined) {
|
|
17
|
+
return { ok: false, response: fail(404, 'No media collection is configured.') };
|
|
18
|
+
}
|
|
19
|
+
if (!(await allowed(env, 'edit', req))) {
|
|
20
|
+
return { ok: false, response: fail(403, 'You may not edit with the builder.') };
|
|
21
|
+
}
|
|
22
|
+
return { ok: true, slug };
|
|
23
|
+
}
|
|
24
|
+
/** `GET /api/buildr/media?search&type&page`: the picker's listing, newest first. */
|
|
25
|
+
export const mediaListEndpoint = (env) => ({
|
|
26
|
+
path: '/buildr/media',
|
|
27
|
+
method: 'get',
|
|
28
|
+
handler: async (req) => {
|
|
29
|
+
const media = await mediaOf(env, req);
|
|
30
|
+
if (!media.ok)
|
|
31
|
+
return media.response;
|
|
32
|
+
const parsed = mediaListQuerySchema.safeParse({
|
|
33
|
+
search: req.searchParams?.get('search') ?? undefined,
|
|
34
|
+
type: req.searchParams?.get('type') ?? undefined,
|
|
35
|
+
page: req.searchParams?.get('page') ?? undefined,
|
|
36
|
+
locale: req.searchParams?.get('locale') ?? undefined,
|
|
37
|
+
});
|
|
38
|
+
if (!parsed.success)
|
|
39
|
+
return fail(400, 'The query does not match the contract.');
|
|
40
|
+
const { search, type, page } = parsed.data;
|
|
41
|
+
const locale = chooseLocale(req, parsed.data.locale);
|
|
42
|
+
if (!locale.ok)
|
|
43
|
+
return locale.response;
|
|
44
|
+
const conditions = [
|
|
45
|
+
...(search === undefined || search === ''
|
|
46
|
+
? []
|
|
47
|
+
: [{ or: [{ alt: { like: search } }, { filename: { like: search } }] }]),
|
|
48
|
+
...(type === undefined ? [] : [{ mimeType: { like: TYPE_PREFIX[type] } }]),
|
|
49
|
+
];
|
|
50
|
+
const result = await req.payload.find({
|
|
51
|
+
collection: media.slug,
|
|
52
|
+
...(conditions.length === 0 ? {} : { where: { and: conditions } }),
|
|
53
|
+
sort: '-createdAt',
|
|
54
|
+
page,
|
|
55
|
+
limit: PAGE_SIZE,
|
|
56
|
+
depth: 0,
|
|
57
|
+
...locale.args,
|
|
58
|
+
req,
|
|
59
|
+
overrideAccess: false,
|
|
60
|
+
});
|
|
61
|
+
return json(mediaListResponseSchema.parse({
|
|
62
|
+
items: result.docs.flatMap((doc) => {
|
|
63
|
+
const asset = normalizeMedia(doc);
|
|
64
|
+
return asset === null ? [] : [asset];
|
|
65
|
+
}),
|
|
66
|
+
page: result.page ?? page,
|
|
67
|
+
totalPages: result.totalPages,
|
|
68
|
+
}));
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
/** `POST /api/buildr/media` (multipart `file`, `alt`): uploads through the Local API; `alt` is required. */
|
|
72
|
+
export const mediaUploadEndpoint = (env) => ({
|
|
73
|
+
path: '/buildr/media',
|
|
74
|
+
method: 'post',
|
|
75
|
+
handler: async (req) => {
|
|
76
|
+
const media = await mediaOf(env, req);
|
|
77
|
+
if (!media.ok)
|
|
78
|
+
return media.response;
|
|
79
|
+
const rejected = mutationGuard(req, 'multipart');
|
|
80
|
+
if (rejected !== undefined)
|
|
81
|
+
return rejected;
|
|
82
|
+
let form;
|
|
83
|
+
try {
|
|
84
|
+
form = await req.formData();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return fail(400, 'The request body must be multipart form data.');
|
|
88
|
+
}
|
|
89
|
+
const file = form.get('file');
|
|
90
|
+
const alt = form.get('alt');
|
|
91
|
+
if (!(file instanceof File) || file.size === 0)
|
|
92
|
+
return fail(400, 'A file is required.');
|
|
93
|
+
if (file.size > MAX_UPLOAD_BYTES)
|
|
94
|
+
return fail(413, 'The file is too large.');
|
|
95
|
+
if (typeof alt !== 'string' || alt.trim() === '') {
|
|
96
|
+
return fail(422, 'The alternative text (alt) is required.');
|
|
97
|
+
}
|
|
98
|
+
const doc = await req.payload.create({
|
|
99
|
+
collection: media.slug,
|
|
100
|
+
data: { alt: alt.trim() },
|
|
101
|
+
file: {
|
|
102
|
+
data: Buffer.from(await file.arrayBuffer()),
|
|
103
|
+
mimetype: file.type,
|
|
104
|
+
name: file.name,
|
|
105
|
+
size: file.size,
|
|
106
|
+
},
|
|
107
|
+
req,
|
|
108
|
+
overrideAccess: false,
|
|
109
|
+
});
|
|
110
|
+
const asset = normalizeMedia(doc);
|
|
111
|
+
if (asset === null)
|
|
112
|
+
return fail(500, 'The uploaded file has no URL.');
|
|
113
|
+
return json(asset, 201);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.test.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/media.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { mkdtempSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
5
|
+
import { mediaListResponseSchema } from '../../contract.js';
|
|
6
|
+
import { boot } from './endpoints.test-kit.js';
|
|
7
|
+
let h;
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
h = await boot('media', { media: { collection: 'media' } }, {
|
|
10
|
+
collections: [
|
|
11
|
+
{
|
|
12
|
+
slug: 'media',
|
|
13
|
+
upload: { staticDir: mkdtempSync(join(tmpdir(), 'buildr-media-files-')) },
|
|
14
|
+
fields: [{ name: 'alt', type: 'text', required: true }],
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
afterAll(() => h.close());
|
|
20
|
+
const PNG = Uint8Array.from(atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=='), (char) => char.charCodeAt(0));
|
|
21
|
+
const upload = async (fields, token) => {
|
|
22
|
+
const form = new FormData();
|
|
23
|
+
if (fields.file !== undefined)
|
|
24
|
+
form.set('file', fields.file);
|
|
25
|
+
if (fields.alt !== undefined)
|
|
26
|
+
form.set('alt', fields.alt);
|
|
27
|
+
const { handleEndpoints } = await import('payload');
|
|
28
|
+
const auth = token === undefined ? h.token : token;
|
|
29
|
+
const response = await handleEndpoints({
|
|
30
|
+
config: h.payload.config,
|
|
31
|
+
request: new Request('http://localhost/api/buildr/media', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: auth === null ? {} : { authorization: `JWT ${auth}` },
|
|
34
|
+
body: form,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
const text = await response.text();
|
|
38
|
+
return { status: response.status, body: text === '' ? undefined : JSON.parse(text) };
|
|
39
|
+
};
|
|
40
|
+
describe('POST /buildr/media', () => {
|
|
41
|
+
it('uploads a file with its alt and answers a MediaAsset', async () => {
|
|
42
|
+
const { status, body } = await upload({
|
|
43
|
+
file: new File([PNG], 'pixel.png', { type: 'image/png' }),
|
|
44
|
+
alt: ' A pixel ',
|
|
45
|
+
});
|
|
46
|
+
expect(status).toBe(201);
|
|
47
|
+
expect(body).toMatchObject({ alt: 'A pixel', mimeType: 'image/png' });
|
|
48
|
+
expect(typeof body.url).toBe('string');
|
|
49
|
+
});
|
|
50
|
+
it('refuses an upload with no alt (422), no file (400) or no login (401)', async () => {
|
|
51
|
+
const file = new File([PNG], 'x.png', { type: 'image/png' });
|
|
52
|
+
expect((await upload({ file })).status).toBe(422);
|
|
53
|
+
expect((await upload({ file, alt: ' ' })).status).toBe(422);
|
|
54
|
+
expect((await upload({ alt: 'x' })).status).toBe(400);
|
|
55
|
+
expect((await upload({ file, alt: 'x' }, null)).status).toBe(401);
|
|
56
|
+
});
|
|
57
|
+
it('refuses a foreign origin and a JSON body', async () => {
|
|
58
|
+
const foreign = await h.call('POST', '/buildr/media', {
|
|
59
|
+
body: {},
|
|
60
|
+
headers: { origin: 'https://evil.test' },
|
|
61
|
+
});
|
|
62
|
+
expect(foreign.status).toBe(415);
|
|
63
|
+
const { handleEndpoints } = await import('payload');
|
|
64
|
+
const form = new FormData();
|
|
65
|
+
form.set('alt', 'x');
|
|
66
|
+
const response = await handleEndpoints({
|
|
67
|
+
config: h.payload.config,
|
|
68
|
+
request: new Request('http://localhost/api/buildr/media', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { authorization: `JWT ${h.token}`, origin: 'https://evil.test' },
|
|
71
|
+
body: form,
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
expect(response.status).toBe(403);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('GET /buildr/media', () => {
|
|
78
|
+
it('lists, searches and filters by type', async () => {
|
|
79
|
+
await upload({ file: new File([PNG], 'cat.png', { type: 'image/png' }), alt: 'A cat' });
|
|
80
|
+
await upload({ file: new File(['%PDF'], 'doc.pdf', { type: 'application/pdf' }), alt: 'Doc' });
|
|
81
|
+
const all = mediaListResponseSchema.parse((await h.call('GET', '/buildr/media')).body);
|
|
82
|
+
expect(all.items.length).toBeGreaterThanOrEqual(3);
|
|
83
|
+
expect(all.page).toBe(1);
|
|
84
|
+
const images = mediaListResponseSchema.parse((await h.call('GET', '/buildr/media?type=image')).body);
|
|
85
|
+
expect(images.items.every((item) => item.mimeType.startsWith('image/'))).toBe(true);
|
|
86
|
+
expect(images.items.length).toBe(all.items.length - 1);
|
|
87
|
+
const found = mediaListResponseSchema.parse((await h.call('GET', '/buildr/media?search=cat')).body);
|
|
88
|
+
expect(found.items.map((item) => item.alt)).toEqual(['A cat']);
|
|
89
|
+
});
|
|
90
|
+
it('validates the query and the caller: 400, 401', async () => {
|
|
91
|
+
expect((await h.call('GET', '/buildr/media?type=exe')).status).toBe(400);
|
|
92
|
+
expect((await h.call('GET', '/buildr/media?page=0')).status).toBe(400);
|
|
93
|
+
expect((await h.call('GET', '/buildr/media', { token: null })).status).toBe(401);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-a11y.test.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/publish-a11y.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { boot, title, withChild } from './endpoints.test-kit.js';
|
|
3
|
+
let h;
|
|
4
|
+
beforeAll(async () => {
|
|
5
|
+
h = await boot('publish-a11y', { a11y: { publish: 'block' } });
|
|
6
|
+
});
|
|
7
|
+
afterAll(() => h.close());
|
|
8
|
+
describe('publishing with the a11y policy "block"', () => {
|
|
9
|
+
it('refuses a layout with accessibility errors, and publishes a fixed one', async () => {
|
|
10
|
+
const doc = await h.payload.create({
|
|
11
|
+
collection: 'pages',
|
|
12
|
+
data: { title: 'Home', slug: 'home' },
|
|
13
|
+
draft: true,
|
|
14
|
+
});
|
|
15
|
+
const path = `/buildr/documents/pages/${doc.id}`;
|
|
16
|
+
const put = (document, baseRevision) => h.call('PUT', path, { body: { document, baseRevision, autosave: false } });
|
|
17
|
+
const broken = withChild({ alt: { kind: 'static', value: '' } }, { type: 'buildr/image' });
|
|
18
|
+
expect((await put(broken, 0)).status).toBe(200);
|
|
19
|
+
const refused = await h.call('POST', `${path}/publish`, { body: { baseRevision: 1 } });
|
|
20
|
+
expect(refused.status).toBe(422);
|
|
21
|
+
expect(refused.body.diagnostics.map((d) => d.code)).toContain('a11y.image-alt');
|
|
22
|
+
const fixed = withChild({ alt: title('A cat').title }, { type: 'buildr/image' });
|
|
23
|
+
expect((await put(fixed, 1)).status).toBe(200);
|
|
24
|
+
const published = await h.call('POST', `${path}/publish`, { body: { baseRevision: 2 } });
|
|
25
|
+
expect(published.status).toBe(200);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload';
|
|
2
|
+
import { type EndpointEnv } from './context.ts';
|
|
3
|
+
/**
|
|
4
|
+
* `POST /api/buildr/documents/:collection/:id/publish`: publishes the latest draft, all of it
|
|
5
|
+
* (fields edited in the admin included). The revision must be the stored one (`409`); the layout is
|
|
6
|
+
* validated again and, with `a11y.publish: 'block'`, accessibility errors refuse the publish (`422`).
|
|
7
|
+
*/
|
|
8
|
+
export declare const publishEndpoint: (env: EndpointEnv) => Endpoint;
|
|
9
|
+
//# sourceMappingURL=publish.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/publish.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAIxC,OAAO,EAGL,KAAK,WAAW,EAOjB,MAAM,cAAc,CAAC;AAItB;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAS,WAAW,KAAG,QAiEjD,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { runA11y } from '@next-buildr/core';
|
|
2
|
+
import { publishRequestSchema, publishResponseSchema } from '../../contract.js';
|
|
3
|
+
import { processLayout } from '../hooks/process-layout.js';
|
|
4
|
+
import { configuredLocales } from '../locales.js';
|
|
5
|
+
import { allowed, bodyOf, latestOf, revisionOf, targetOf, updatedBy, writeContext, writeLimit, } from './context.js';
|
|
6
|
+
import { mutationGuard } from './guards.js';
|
|
7
|
+
import { conflict, fail, invalid, json } from './respond.js';
|
|
8
|
+
/**
|
|
9
|
+
* `POST /api/buildr/documents/:collection/:id/publish`: publishes the latest draft, all of it
|
|
10
|
+
* (fields edited in the admin included). The revision must be the stored one (`409`); the layout is
|
|
11
|
+
* validated again and, with `a11y.publish: 'block'`, accessibility errors refuse the publish (`422`).
|
|
12
|
+
*/
|
|
13
|
+
export const publishEndpoint = (env) => ({
|
|
14
|
+
path: '/buildr/documents/:collection/:id/publish',
|
|
15
|
+
method: 'post',
|
|
16
|
+
handler: async (req) => {
|
|
17
|
+
const target = targetOf(env, req);
|
|
18
|
+
if (!target.ok)
|
|
19
|
+
return target.response;
|
|
20
|
+
const rejected = mutationGuard(req);
|
|
21
|
+
if (rejected !== undefined)
|
|
22
|
+
return rejected;
|
|
23
|
+
if (!(await allowed(env, 'publish', req))) {
|
|
24
|
+
return fail(403, 'You may not publish with the builder.');
|
|
25
|
+
}
|
|
26
|
+
const limited = await writeLimit(env, req);
|
|
27
|
+
if (limited !== undefined)
|
|
28
|
+
return limited;
|
|
29
|
+
const body = await bodyOf(req);
|
|
30
|
+
if (!body.ok)
|
|
31
|
+
return body.response;
|
|
32
|
+
const parsed = publishRequestSchema.safeParse(body.value);
|
|
33
|
+
if (!parsed.success)
|
|
34
|
+
return fail(400, 'The request does not match the contract.');
|
|
35
|
+
const found = await latestOf(req, target.value);
|
|
36
|
+
if (!found.ok)
|
|
37
|
+
return found.response;
|
|
38
|
+
const current = revisionOf(found.value);
|
|
39
|
+
if (parsed.data.baseRevision !== current)
|
|
40
|
+
return conflict(current);
|
|
41
|
+
const layout = processLayout(found.value['layout'], {
|
|
42
|
+
...env.options,
|
|
43
|
+
locales: configuredLocales(req.payload.config),
|
|
44
|
+
});
|
|
45
|
+
if (!layout.ok)
|
|
46
|
+
return invalid(layout.diagnostics);
|
|
47
|
+
const registry = env.options.registry;
|
|
48
|
+
if (registry !== undefined && env.options.a11y.publish === 'block') {
|
|
49
|
+
const own = env.options.collections[target.value.collection];
|
|
50
|
+
const errors = runA11y(layout.doc, registry.meta, {
|
|
51
|
+
config: { expectH1: own?.expectH1 === true ? 'document' : 'layout' },
|
|
52
|
+
})
|
|
53
|
+
.filter((issue) => issue.severity === 'error')
|
|
54
|
+
.map((issue) => ({
|
|
55
|
+
code: `a11y.${issue.ruleId}`,
|
|
56
|
+
message: issue.message,
|
|
57
|
+
severity: 'error',
|
|
58
|
+
path: ['nodes', issue.nodeId],
|
|
59
|
+
}));
|
|
60
|
+
if (errors.length > 0)
|
|
61
|
+
return invalid(errors);
|
|
62
|
+
}
|
|
63
|
+
const published = await req.payload.update({
|
|
64
|
+
collection: target.value.collection,
|
|
65
|
+
id: target.value.id,
|
|
66
|
+
data: { _status: 'published', ...updatedBy(env, req) },
|
|
67
|
+
draft: false,
|
|
68
|
+
depth: 0,
|
|
69
|
+
req,
|
|
70
|
+
overrideAccess: false,
|
|
71
|
+
context: writeContext(req),
|
|
72
|
+
});
|
|
73
|
+
const updatedAt = String(published['updatedAt']);
|
|
74
|
+
return json(publishResponseSchema.parse({
|
|
75
|
+
status: 'published',
|
|
76
|
+
publishedAt: updatedAt,
|
|
77
|
+
revision: current,
|
|
78
|
+
updatedAt,
|
|
79
|
+
}));
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload';
|
|
2
|
+
import { type EndpointEnv } from './context.ts';
|
|
3
|
+
/**
|
|
4
|
+
* `PATCH /api/buildr/documents/:collection/:id`: renames a document. The name is the field the
|
|
5
|
+
* collection shows as its title (`admin.useAsTitle`, else `title`); the layout and its revision are
|
|
6
|
+
* left alone, so a rename never conflicts with the editor's own saves.
|
|
7
|
+
*/
|
|
8
|
+
export declare const renameEndpoint: (env: EndpointEnv) => Endpoint;
|
|
9
|
+
//# sourceMappingURL=rename.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rename.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/rename.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,EAGL,KAAK,WAAW,EAMjB,MAAM,cAAc,CAAC;AAItB;;;;GAIG;AACH,eAAO,MAAM,cAAc,QAAS,WAAW,KAAG,QAiChD,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { renameRequestSchema, renameResponseSchema } from '../../contract.js';
|
|
2
|
+
import { allowed, bodyOf, latestOf, targetOf, updatedBy, writeContext, writeLimit, } from './context.js';
|
|
3
|
+
import { mutationGuard } from './guards.js';
|
|
4
|
+
import { fail, json } from './respond.js';
|
|
5
|
+
/**
|
|
6
|
+
* `PATCH /api/buildr/documents/:collection/:id`: renames a document. The name is the field the
|
|
7
|
+
* collection shows as its title (`admin.useAsTitle`, else `title`); the layout and its revision are
|
|
8
|
+
* left alone, so a rename never conflicts with the editor's own saves.
|
|
9
|
+
*/
|
|
10
|
+
export const renameEndpoint = (env) => ({
|
|
11
|
+
path: '/buildr/documents/:collection/:id',
|
|
12
|
+
method: 'patch',
|
|
13
|
+
handler: async (req) => {
|
|
14
|
+
const target = targetOf(env, req);
|
|
15
|
+
if (!target.ok)
|
|
16
|
+
return target.response;
|
|
17
|
+
const rejected = mutationGuard(req);
|
|
18
|
+
if (rejected !== undefined)
|
|
19
|
+
return rejected;
|
|
20
|
+
if (!(await allowed(env, 'edit', req)))
|
|
21
|
+
return fail(403, 'You may not edit with the builder.');
|
|
22
|
+
const limited = await writeLimit(env, req);
|
|
23
|
+
if (limited !== undefined)
|
|
24
|
+
return limited;
|
|
25
|
+
const body = await bodyOf(req);
|
|
26
|
+
if (!body.ok)
|
|
27
|
+
return body.response;
|
|
28
|
+
const parsed = renameRequestSchema.safeParse(body.value);
|
|
29
|
+
if (!parsed.success)
|
|
30
|
+
return fail(400, 'The request does not match the contract.');
|
|
31
|
+
const found = await latestOf(req, target.value);
|
|
32
|
+
if (!found.ok)
|
|
33
|
+
return found.response;
|
|
34
|
+
const titleField = req.payload.collections[target.value.collection]?.config.admin?.useAsTitle ?? 'title';
|
|
35
|
+
await req.payload.update({
|
|
36
|
+
collection: target.value.collection,
|
|
37
|
+
id: target.value.id,
|
|
38
|
+
data: { [titleField]: parsed.data.title, ...updatedBy(env, req) },
|
|
39
|
+
draft: true,
|
|
40
|
+
depth: 0,
|
|
41
|
+
req,
|
|
42
|
+
overrideAccess: false,
|
|
43
|
+
context: writeContext(req),
|
|
44
|
+
});
|
|
45
|
+
return json(renameResponseSchema.parse({ title: parsed.data.title }));
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Diagnostic } from '@next-buildr/core';
|
|
2
|
+
export declare const json: (body: unknown, status?: number) => Response;
|
|
3
|
+
export declare const fail: (status: number, error: string) => Response;
|
|
4
|
+
export declare const unauthorized: () => Response;
|
|
5
|
+
export declare const notFound: (what: string) => Response;
|
|
6
|
+
export declare const conflict: (currentRevision: number) => Response;
|
|
7
|
+
/** The contract carries plain JSON; a diagnostic detail that is not JSON never gets here. */
|
|
8
|
+
export declare const invalid: (diagnostics: readonly Diagnostic[]) => Response;
|
|
9
|
+
//# sourceMappingURL=respond.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"respond.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/respond.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,eAAO,MAAM,IAAI,SAAU,OAAO,sBAAiB,QAI/C,CAAC;AAEL,eAAO,MAAM,IAAI,WAAY,MAAM,SAAS,MAAM,KAAG,QACJ,CAAC;AAElD,eAAO,MAAM,YAAY,QAAO,QAAoD,CAAC;AACrF,eAAO,MAAM,QAAQ,SAAU,MAAM,KAAG,QAA+C,CAAC;AACxF,eAAO,MAAM,QAAQ,oBAAqB,MAAM,KAAG,QAA0C,CAAC;AAE9F,6FAA6F;AAC7F,eAAO,MAAM,OAAO,gBAAiB,SAAS,UAAU,EAAE,KAAG,QACd,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const json = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
2
|
+
status,
|
|
3
|
+
headers: { 'content-type': 'application/json' },
|
|
4
|
+
});
|
|
5
|
+
export const fail = (status, error) => json({ error }, status);
|
|
6
|
+
export const unauthorized = () => fail(401, 'Authentication is required.');
|
|
7
|
+
export const notFound = (what) => fail(404, `${what} was not found.`);
|
|
8
|
+
export const conflict = (currentRevision) => json({ currentRevision }, 409);
|
|
9
|
+
/** The contract carries plain JSON; a diagnostic detail that is not JSON never gets here. */
|
|
10
|
+
export const invalid = (diagnostics) => json({ diagnostics }, 422);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload';
|
|
2
|
+
import { type EndpointEnv } from './context.ts';
|
|
3
|
+
/** `GET /api/buildr/samples/:collection?search`: documents to preview a template with. */
|
|
4
|
+
export declare const samplesEndpoint: (env: EndpointEnv) => Endpoint;
|
|
5
|
+
//# sourceMappingURL=samples.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"samples.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/samples.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGxC,OAAO,EAAyB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAKvE,0FAA0F;AAC1F,eAAO,MAAM,eAAe,QAAS,WAAW,KAAG,QAmCjD,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { samplesResponseSchema } from '../../contract.js';
|
|
2
|
+
import { chooseLocale } from '../locales.js';
|
|
3
|
+
import { allowed, collectionOf } from './context.js';
|
|
4
|
+
import { fail, json } from './respond.js';
|
|
5
|
+
const SAMPLE_LIMIT = 20;
|
|
6
|
+
/** `GET /api/buildr/samples/:collection?search`: documents to preview a template with. */
|
|
7
|
+
export const samplesEndpoint = (env) => ({
|
|
8
|
+
path: '/buildr/samples/:collection',
|
|
9
|
+
method: 'get',
|
|
10
|
+
handler: async (req) => {
|
|
11
|
+
const collection = collectionOf(env, req);
|
|
12
|
+
if (!collection.ok)
|
|
13
|
+
return collection.response;
|
|
14
|
+
if (!(await allowed(env, 'edit', req)))
|
|
15
|
+
return fail(403, 'You may not edit with the builder.');
|
|
16
|
+
const titleField = req.payload.collections[collection.value]?.config.admin?.useAsTitle ?? 'title';
|
|
17
|
+
const locale = chooseLocale(req, req.searchParams?.get('locale'));
|
|
18
|
+
if (!locale.ok)
|
|
19
|
+
return locale.response;
|
|
20
|
+
const search = req.searchParams?.get('search')?.trim() ?? '';
|
|
21
|
+
const result = await req.payload.find({
|
|
22
|
+
collection: collection.value,
|
|
23
|
+
...(search === '' ? {} : { where: { [titleField]: { like: search } } }),
|
|
24
|
+
limit: SAMPLE_LIMIT,
|
|
25
|
+
depth: 0,
|
|
26
|
+
draft: true,
|
|
27
|
+
...locale.args,
|
|
28
|
+
req,
|
|
29
|
+
overrideAccess: false,
|
|
30
|
+
});
|
|
31
|
+
return json(samplesResponseSchema.parse({
|
|
32
|
+
items: result.docs.map((doc) => {
|
|
33
|
+
const record = doc;
|
|
34
|
+
const title = record[titleField];
|
|
35
|
+
return {
|
|
36
|
+
id: String(record['id']),
|
|
37
|
+
title: typeof title === 'string' && title !== '' ? title : String(record['id']),
|
|
38
|
+
};
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload';
|
|
2
|
+
import { type EndpointEnv } from './context.ts';
|
|
3
|
+
/**
|
|
4
|
+
* `PUT /api/buildr/documents/:collection/:id`: a save. The revision the editor built on must still
|
|
5
|
+
* be the stored one (`409` otherwise); the document is migrated and validated (`422` with the
|
|
6
|
+
* diagnostics); an autosave updates the current autosave version in place, a save creates a draft.
|
|
7
|
+
*/
|
|
8
|
+
export declare const saveEndpoint: (env: EndpointEnv) => Endpoint;
|
|
9
|
+
//# sourceMappingURL=save.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/save.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAIxC,OAAO,EAGL,KAAK,WAAW,EAOjB,MAAM,cAAc,CAAC;AAItB;;;;GAIG;AACH,eAAO,MAAM,YAAY,QAAS,WAAW,KAAG,QA0C9C,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { saveRequestSchema, saveResponseSchema } from '../../contract.js';
|
|
2
|
+
import { processLayout } from '../hooks/process-layout.js';
|
|
3
|
+
import { configuredLocales } from '../locales.js';
|
|
4
|
+
import { allowed, bodyOf, latestOf, revisionOf, targetOf, updatedBy, writeContext, writeLimit, } from './context.js';
|
|
5
|
+
import { mutationGuard } from './guards.js';
|
|
6
|
+
import { conflict, fail, invalid, json } from './respond.js';
|
|
7
|
+
/**
|
|
8
|
+
* `PUT /api/buildr/documents/:collection/:id`: a save. The revision the editor built on must still
|
|
9
|
+
* be the stored one (`409` otherwise); the document is migrated and validated (`422` with the
|
|
10
|
+
* diagnostics); an autosave updates the current autosave version in place, a save creates a draft.
|
|
11
|
+
*/
|
|
12
|
+
export const saveEndpoint = (env) => ({
|
|
13
|
+
path: '/buildr/documents/:collection/:id',
|
|
14
|
+
method: 'put',
|
|
15
|
+
handler: async (req) => {
|
|
16
|
+
const target = targetOf(env, req);
|
|
17
|
+
if (!target.ok)
|
|
18
|
+
return target.response;
|
|
19
|
+
const rejected = mutationGuard(req);
|
|
20
|
+
if (rejected !== undefined)
|
|
21
|
+
return rejected;
|
|
22
|
+
if (!(await allowed(env, 'edit', req)))
|
|
23
|
+
return fail(403, 'You may not edit with the builder.');
|
|
24
|
+
const limited = await writeLimit(env, req);
|
|
25
|
+
if (limited !== undefined)
|
|
26
|
+
return limited;
|
|
27
|
+
const body = await bodyOf(req);
|
|
28
|
+
if (!body.ok)
|
|
29
|
+
return body.response;
|
|
30
|
+
const parsed = saveRequestSchema.safeParse(body.value);
|
|
31
|
+
if (!parsed.success)
|
|
32
|
+
return fail(400, 'The request does not match the contract.');
|
|
33
|
+
const found = await latestOf(req, target.value);
|
|
34
|
+
if (!found.ok)
|
|
35
|
+
return found.response;
|
|
36
|
+
const current = revisionOf(found.value);
|
|
37
|
+
if (parsed.data.baseRevision !== current)
|
|
38
|
+
return conflict(current);
|
|
39
|
+
const layout = processLayout(parsed.data.document, {
|
|
40
|
+
...env.options,
|
|
41
|
+
locales: configuredLocales(req.payload.config),
|
|
42
|
+
});
|
|
43
|
+
if (!layout.ok)
|
|
44
|
+
return invalid(layout.diagnostics);
|
|
45
|
+
const updated = await req.payload.update({
|
|
46
|
+
collection: target.value.collection,
|
|
47
|
+
id: target.value.id,
|
|
48
|
+
data: { layout: layout.doc, buildrRevision: current + 1, ...updatedBy(env, req) },
|
|
49
|
+
draft: true,
|
|
50
|
+
autosave: parsed.data.autosave,
|
|
51
|
+
depth: 0,
|
|
52
|
+
req,
|
|
53
|
+
overrideAccess: false,
|
|
54
|
+
context: writeContext(req),
|
|
55
|
+
});
|
|
56
|
+
return json(saveResponseSchema.parse({ revision: current + 1, updatedAt: String(updated['updatedAt']) }));
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload';
|
|
2
|
+
import type { EndpointEnv } from './context.ts';
|
|
3
|
+
/** `GET /api/buildr/session`: who is asking, what they may do, and the limits their documents live under. */
|
|
4
|
+
export declare const sessionEndpoint: (env: EndpointEnv) => Endpoint;
|
|
5
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAIxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,6GAA6G;AAC7G,eAAO,MAAM,eAAe,QAAS,WAAW,KAAG,QAuBjD,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { sessionResponseSchema } from '../../contract.js';
|
|
2
|
+
import { permissionsOf } from '../access.js';
|
|
3
|
+
import { configuredLocales } from '../locales.js';
|
|
4
|
+
import { json, unauthorized } from './respond.js';
|
|
5
|
+
/** `GET /api/buildr/session`: who is asking, what they may do, and the limits their documents live under. */
|
|
6
|
+
export const sessionEndpoint = (env) => ({
|
|
7
|
+
path: '/buildr/session',
|
|
8
|
+
method: 'get',
|
|
9
|
+
handler: async (req) => {
|
|
10
|
+
const user = req.user;
|
|
11
|
+
if (user === null || user === undefined || user.id === undefined)
|
|
12
|
+
return unauthorized();
|
|
13
|
+
const body = {
|
|
14
|
+
user: { id: user.id, ...(user.email === undefined ? {} : { email: user.email }) },
|
|
15
|
+
permissions: await permissionsOf(env.options, req),
|
|
16
|
+
limits: { ...env.options.limits },
|
|
17
|
+
publishPolicy: env.options.a11y.publish,
|
|
18
|
+
};
|
|
19
|
+
const locales = configuredLocales(req.payload.config);
|
|
20
|
+
if (locales !== undefined) {
|
|
21
|
+
body.locales = {
|
|
22
|
+
locales: [...locales.locales],
|
|
23
|
+
default: locales.default,
|
|
24
|
+
fallback: locales.fallback,
|
|
25
|
+
intl: { ...locales.intl },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return json(sessionResponseSchema.parse(body));
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.test.d.ts","sourceRoot":"","sources":["../../../src/plugin/endpoints/templates.test.ts"],"names":[],"mappings":""}
|