@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 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.d.ts","sourceRoot":"","sources":["../../src/adapter/adapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createEmptyDocument } from '@next-buildr/core';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { createPayloadAdapter, createPayloadCanvasDataSource } from './index.js';
|
|
6
|
+
/** A `fetch` that answers from `routes` (`"GET /path"`), recording what it was asked. */
|
|
7
|
+
function fakeFetch(routes) {
|
|
8
|
+
const calls = [];
|
|
9
|
+
const fetch = (async (input, init = {}) => {
|
|
10
|
+
const url = new URL(input, 'http://site.test');
|
|
11
|
+
const method = init.method ?? 'GET';
|
|
12
|
+
calls.push({
|
|
13
|
+
method,
|
|
14
|
+
url,
|
|
15
|
+
body: typeof init.body === 'string'
|
|
16
|
+
? JSON.parse(init.body)
|
|
17
|
+
: init.body instanceof FormData
|
|
18
|
+
? init.body
|
|
19
|
+
: undefined,
|
|
20
|
+
headers: (init.headers ?? {}),
|
|
21
|
+
});
|
|
22
|
+
const route = routes[`${method} ${url.pathname}`];
|
|
23
|
+
if (route === undefined)
|
|
24
|
+
throw new Error(`unexpected ${method} ${url.pathname}`);
|
|
25
|
+
const answer = route();
|
|
26
|
+
if (answer instanceof Error)
|
|
27
|
+
throw answer;
|
|
28
|
+
return new Response(answer.body === undefined ? '' : JSON.stringify(answer.body), {
|
|
29
|
+
status: answer.status,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
return { fetch, calls };
|
|
33
|
+
}
|
|
34
|
+
const REF = { collection: 'pages', id: 7 };
|
|
35
|
+
const doc = createEmptyDocument();
|
|
36
|
+
const documentBody = {
|
|
37
|
+
ref: { collection: 'pages', id: 7 },
|
|
38
|
+
title: 'Home',
|
|
39
|
+
slug: 'home',
|
|
40
|
+
status: 'draft',
|
|
41
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
42
|
+
revision: 3,
|
|
43
|
+
document: doc,
|
|
44
|
+
contextRef: 'pages:7',
|
|
45
|
+
previewPath: '/home',
|
|
46
|
+
layoutSource: 'document',
|
|
47
|
+
layoutRef: 'pages:1',
|
|
48
|
+
};
|
|
49
|
+
describe('createPayloadAdapter', () => {
|
|
50
|
+
it('reads the session, with the languages of the site', async () => {
|
|
51
|
+
const { fetch, calls } = fakeFetch({
|
|
52
|
+
'GET /api/buildr/session': () => ({
|
|
53
|
+
status: 200,
|
|
54
|
+
body: {
|
|
55
|
+
user: { id: 5 },
|
|
56
|
+
permissions: { canEdit: true, canPublish: false, canUnlockTemplates: false },
|
|
57
|
+
limits: { maxNodes: 10, maxBytes: 10 },
|
|
58
|
+
locales: {
|
|
59
|
+
locales: ['pl', 'en'],
|
|
60
|
+
default: 'pl',
|
|
61
|
+
fallback: true,
|
|
62
|
+
intl: { pl: 'Polski', en: 'English' },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
const adapter = createPayloadAdapter({ baseUrl: '/api/', fetch });
|
|
68
|
+
expect(await adapter.getSession(REF)).toEqual({
|
|
69
|
+
userId: '5',
|
|
70
|
+
canEdit: true,
|
|
71
|
+
canPublish: false,
|
|
72
|
+
locales: {
|
|
73
|
+
locales: ['pl', 'en'],
|
|
74
|
+
default: 'pl',
|
|
75
|
+
fallback: true,
|
|
76
|
+
intl: { pl: 'Polski', en: 'English' },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
await adapter.getSession(REF);
|
|
80
|
+
expect(calls).toHaveLength(1);
|
|
81
|
+
expect(calls[0]?.url.pathname).toBe('/api/buildr/session');
|
|
82
|
+
});
|
|
83
|
+
it('loads and validates a document; a broken one rejects', async () => {
|
|
84
|
+
const good = fakeFetch({
|
|
85
|
+
'GET /api/buildr/documents/pages/7': () => ({ status: 200, body: documentBody }),
|
|
86
|
+
});
|
|
87
|
+
const loaded = await createPayloadAdapter({ baseUrl: '/api', fetch: good.fetch }).load(REF);
|
|
88
|
+
expect(loaded).toMatchObject({ title: 'Home', slug: 'home', status: 'draft', revision: 3 });
|
|
89
|
+
expect(good.calls[0]?.url.searchParams.get('draft')).toBe('1');
|
|
90
|
+
const bad = fakeFetch({
|
|
91
|
+
'GET /api/buildr/documents/pages/7': () => ({
|
|
92
|
+
status: 200,
|
|
93
|
+
body: { ...documentBody, document: { nope: true } },
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
await expect(createPayloadAdapter({ baseUrl: '/api', fetch: bad.fetch }).load(REF)).rejects.toThrow(/not valid/);
|
|
97
|
+
});
|
|
98
|
+
it('maps 409 and 422 of a save onto results, and everything else onto a rejection', async () => {
|
|
99
|
+
let answer = {
|
|
100
|
+
status: 200,
|
|
101
|
+
body: { revision: 4, updatedAt: 'now' },
|
|
102
|
+
};
|
|
103
|
+
const { fetch, calls } = fakeFetch({ 'PUT /api/buildr/documents/pages/7': () => answer });
|
|
104
|
+
const adapter = createPayloadAdapter({ baseUrl: '/api', fetch });
|
|
105
|
+
const request = { document: doc, baseRevision: 3, autosave: true };
|
|
106
|
+
expect(await adapter.save(REF, request)).toEqual({ ok: true, revision: 4, updatedAt: 'now' });
|
|
107
|
+
expect(calls[0]?.body).toEqual(request);
|
|
108
|
+
expect(calls[0]?.headers['content-type']).toBe('application/json');
|
|
109
|
+
answer = { status: 409, body: { currentRevision: 9 } };
|
|
110
|
+
expect(await adapter.save(REF, request)).toEqual({
|
|
111
|
+
ok: false,
|
|
112
|
+
kind: 'conflict',
|
|
113
|
+
currentRevision: 9,
|
|
114
|
+
});
|
|
115
|
+
const diagnostics = [{ code: 'x', message: 'bad', severity: 'error' }];
|
|
116
|
+
answer = { status: 422, body: { diagnostics } };
|
|
117
|
+
expect(await adapter.save(REF, request)).toEqual({ ok: false, kind: 'invalid', diagnostics });
|
|
118
|
+
for (const status of [401, 403, 404, 500]) {
|
|
119
|
+
answer = { status, body: { error: 'nope' } };
|
|
120
|
+
await expect(adapter.save(REF, request)).rejects.toMatchObject({ status, message: 'nope' });
|
|
121
|
+
}
|
|
122
|
+
answer = { status: 200, body: { unexpected: true } };
|
|
123
|
+
await expect(adapter.save(REF, request)).rejects.toThrow(/contract/);
|
|
124
|
+
});
|
|
125
|
+
it('rejects when there is no network', async () => {
|
|
126
|
+
const { fetch } = fakeFetch({
|
|
127
|
+
'PUT /api/buildr/documents/pages/7': () => new TypeError('offline'),
|
|
128
|
+
});
|
|
129
|
+
await expect(createPayloadAdapter({ baseUrl: '/api', fetch }).save(REF, {
|
|
130
|
+
document: doc,
|
|
131
|
+
baseRevision: 0,
|
|
132
|
+
autosave: false,
|
|
133
|
+
})).rejects.toMatchObject({ name: 'AdapterError', message: 'offline' });
|
|
134
|
+
});
|
|
135
|
+
it('reads only the revision, with who saved it', async () => {
|
|
136
|
+
const { fetch, calls } = fakeFetch({
|
|
137
|
+
'GET /api/buildr/documents/pages/7/revision': () => ({
|
|
138
|
+
status: 200,
|
|
139
|
+
body: { revision: 4, updatedAt: 'u', updatedBy: 'agent@example.com' },
|
|
140
|
+
}),
|
|
141
|
+
});
|
|
142
|
+
expect(await createPayloadAdapter({ baseUrl: '/api', fetch }).getRevision?.(REF)).toEqual({
|
|
143
|
+
revision: 4,
|
|
144
|
+
updatedAt: 'u',
|
|
145
|
+
updatedBy: 'agent@example.com',
|
|
146
|
+
});
|
|
147
|
+
expect(calls).toHaveLength(1);
|
|
148
|
+
});
|
|
149
|
+
it('publishes with the same result mapping', async () => {
|
|
150
|
+
const { fetch } = fakeFetch({
|
|
151
|
+
'POST /api/buildr/documents/pages/7/publish': () => ({
|
|
152
|
+
status: 200,
|
|
153
|
+
body: { status: 'published', publishedAt: 'p', revision: 5, updatedAt: 'u' },
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
expect(await createPayloadAdapter({ baseUrl: '/api', fetch }).publish(REF, { baseRevision: 4 })).toEqual({
|
|
157
|
+
ok: true,
|
|
158
|
+
revision: 5,
|
|
159
|
+
updatedAt: 'u',
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
it('builds the data context from the sample, the language and the session', async () => {
|
|
163
|
+
const { fetch, calls } = fakeFetch({
|
|
164
|
+
'GET /api/buildr/session': () => ({
|
|
165
|
+
status: 200,
|
|
166
|
+
body: {
|
|
167
|
+
user: { id: 1 },
|
|
168
|
+
permissions: { canEdit: true, canPublish: true, canUnlockTemplates: true },
|
|
169
|
+
limits: { maxNodes: 1, maxBytes: 1 },
|
|
170
|
+
locales: {
|
|
171
|
+
locales: ['pl', 'en'],
|
|
172
|
+
default: 'pl',
|
|
173
|
+
fallback: false,
|
|
174
|
+
intl: { pl: 'Polski', en: 'English' },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
'GET /api/buildr/data/context': () => ({
|
|
179
|
+
status: 200,
|
|
180
|
+
body: { scopes: { page: { title: 'T' } } },
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
const adapter = createPayloadAdapter({ baseUrl: '/api', fetch, timeZone: 'Europe/Warsaw' });
|
|
184
|
+
const context = await adapter.getContext(REF, { sampleId: '12' });
|
|
185
|
+
expect(context).toMatchObject({
|
|
186
|
+
scopes: { page: { title: 'T' } },
|
|
187
|
+
locale: 'pl',
|
|
188
|
+
timeZone: 'Europe/Warsaw',
|
|
189
|
+
mode: 'canvas',
|
|
190
|
+
locales: { default: 'pl', fallback: false },
|
|
191
|
+
});
|
|
192
|
+
const query = calls.at(-1)?.url.searchParams;
|
|
193
|
+
expect(query?.get('id')).toBe('12');
|
|
194
|
+
expect(query?.get('collection')).toBe('pages');
|
|
195
|
+
await adapter.getContext(REF, { sampleId: 'posts:5' });
|
|
196
|
+
const qualified = calls.at(-1)?.url.searchParams;
|
|
197
|
+
expect(qualified?.get('collection')).toBe('posts');
|
|
198
|
+
expect(qualified?.get('id')).toBe('5');
|
|
199
|
+
expect(query?.get('locale')).toBe('pl');
|
|
200
|
+
});
|
|
201
|
+
it('searches media with a cursor and a type, and uploads with the alt', async () => {
|
|
202
|
+
const asset = { id: 'm1', url: '/m1.png', mimeType: 'image/png', alt: 'One' };
|
|
203
|
+
const { fetch, calls } = fakeFetch({
|
|
204
|
+
'GET /api/buildr/media': () => ({
|
|
205
|
+
status: 200,
|
|
206
|
+
body: { items: [asset], page: 2, totalPages: 3 },
|
|
207
|
+
}),
|
|
208
|
+
'POST /api/buildr/media': () => ({ status: 201, body: asset }),
|
|
209
|
+
});
|
|
210
|
+
const adapter = createPayloadAdapter({ baseUrl: '/api', fetch });
|
|
211
|
+
const found = await adapter.media.search({
|
|
212
|
+
text: 'cat',
|
|
213
|
+
cursor: '2',
|
|
214
|
+
mimeTypes: ['image/png', 'image/jpeg'],
|
|
215
|
+
});
|
|
216
|
+
expect(found).toEqual({ items: [asset], nextCursor: '3' });
|
|
217
|
+
const query = calls[0]?.url.searchParams;
|
|
218
|
+
expect([query?.get('search'), query?.get('type'), query?.get('page')]).toEqual([
|
|
219
|
+
'cat',
|
|
220
|
+
'image',
|
|
221
|
+
'2',
|
|
222
|
+
]);
|
|
223
|
+
const uploaded = await adapter.media.upload?.(new File(['x'], 'a.png', { type: 'image/png' }), 'One');
|
|
224
|
+
expect(uploaded).toEqual(asset);
|
|
225
|
+
const form = calls[1]?.body;
|
|
226
|
+
expect(form.get('alt')).toBe('One');
|
|
227
|
+
expect(form.get('file').name).toBe('a.png');
|
|
228
|
+
expect(calls[1]?.headers['content-type']).toBeUndefined();
|
|
229
|
+
});
|
|
230
|
+
it('lists samples, and links to the preview and the admin', async () => {
|
|
231
|
+
const { fetch } = fakeFetch({
|
|
232
|
+
'GET /api/buildr/samples/pages': () => ({
|
|
233
|
+
status: 200,
|
|
234
|
+
body: { items: [{ id: '1', title: 'A' }] },
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
const adapter = createPayloadAdapter({ baseUrl: '/api', fetch });
|
|
238
|
+
expect(await adapter.listSamples?.(REF)).toEqual([{ id: 'pages:1', label: 'A' }]);
|
|
239
|
+
expect(adapter.previewUrl(REF, { draft: true })).toBe('/buildr/preview?collection=pages&id=7&draft=1');
|
|
240
|
+
expect(adapter.previewUrl(REF)).toBe('/buildr/preview?collection=pages&id=7');
|
|
241
|
+
expect(adapter.cmsUrl?.(REF)).toBe('/admin/collections/pages/7');
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('createPayloadCanvasDataSource', () => {
|
|
245
|
+
const spec = { source: 'posts', limit: 5, page: 1 };
|
|
246
|
+
const ctx = { locale: 'en', mode: 'canvas' };
|
|
247
|
+
it('runs queries and media through the builder API', async () => {
|
|
248
|
+
const result = { items: [{ id: '1' }], total: 1, page: 1, totalPages: 1 };
|
|
249
|
+
const { fetch, calls } = fakeFetch({
|
|
250
|
+
'POST /api/buildr/data/query': () => ({ status: 200, body: result }),
|
|
251
|
+
'POST /api/buildr/data/media': () => ({
|
|
252
|
+
status: 200,
|
|
253
|
+
body: { m1: { id: 'm1', url: '/m1.png', mimeType: 'image/png' } },
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
const source = createPayloadCanvasDataSource({ baseUrl: '/api', fetch });
|
|
257
|
+
expect(await source.query(spec, ctx)).toEqual(result);
|
|
258
|
+
expect(calls[0]?.body).toEqual({ spec, locale: 'en' });
|
|
259
|
+
expect(Object.keys(await source.getMedia(['m1', 'm1'], ctx))).toEqual(['m1']);
|
|
260
|
+
expect(calls[1]?.body).toEqual({ ids: ['m1'], locale: 'en' });
|
|
261
|
+
expect(await source.getMedia([], ctx)).toEqual({});
|
|
262
|
+
expect(calls).toHaveLength(2);
|
|
263
|
+
});
|
|
264
|
+
it('rejects with the reason the server gave', async () => {
|
|
265
|
+
const { fetch } = fakeFetch({
|
|
266
|
+
'POST /api/buildr/data/query': () => ({
|
|
267
|
+
status: 422,
|
|
268
|
+
body: { error: 'The collection "x" cannot be queried.' },
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
await expect(createPayloadCanvasDataSource({ baseUrl: '/api', fetch }).query(spec, ctx)).rejects.toThrow(/cannot be queried/);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
describe('the adapter boundary', () => {
|
|
275
|
+
it('never imports payload, @payloadcms or next', () => {
|
|
276
|
+
const dir = import.meta.dirname;
|
|
277
|
+
const forbidden = /from\s+['"](payload|@payloadcms\/[^'"]*|next|next\/[^'"]*|@next-buildr\/next)['"]/;
|
|
278
|
+
for (const file of readdirSync(dir).filter((name) => name.endsWith('.ts') && !name.endsWith('.test.ts'))) {
|
|
279
|
+
expect(readFileSync(join(dir, file), 'utf8'), file).not.toMatch(forbidden);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DataSource } from '@next-buildr/core';
|
|
2
|
+
import { type HttpOptions } from './http.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The `DataSource` of the canvas: queries and media go to the builder API, which answers with the
|
|
5
|
+
* access of the editing user (drafts included). A refused query (not allowlisted, too broad) rejects
|
|
6
|
+
* with an `AdapterError` carrying the server's reason.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createPayloadCanvasDataSource(options: HttpOptions): DataSource;
|
|
9
|
+
//# sourceMappingURL=canvas-data-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas-data-source.d.ts","sourceRoot":"","sources":["../../src/adapter/canvas-data-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAOpD,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAErE;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,WAAW,GAAG,UAAU,CAe9E"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { dataMediaResponseSchema, dataQueryResponseSchema, } from '../contract.js';
|
|
2
|
+
import { createHttp, expectBody } from './http.js';
|
|
3
|
+
/**
|
|
4
|
+
* The `DataSource` of the canvas: queries and media go to the builder API, which answers with the
|
|
5
|
+
* access of the editing user (drafts included). A refused query (not allowlisted, too broad) rejects
|
|
6
|
+
* with an `AdapterError` carrying the server's reason.
|
|
7
|
+
*/
|
|
8
|
+
export function createPayloadCanvasDataSource(options) {
|
|
9
|
+
const http = createHttp(options);
|
|
10
|
+
return {
|
|
11
|
+
async query(spec, ctx) {
|
|
12
|
+
const body = { spec, locale: ctx.locale };
|
|
13
|
+
const reply = await http.send('POST', '/buildr/data/query', { body });
|
|
14
|
+
return expectBody(reply, dataQueryResponseSchema);
|
|
15
|
+
},
|
|
16
|
+
async getMedia(ids, ctx) {
|
|
17
|
+
if (ids.length === 0)
|
|
18
|
+
return {};
|
|
19
|
+
const body = { ids: [...new Set(ids)], locale: ctx.locale };
|
|
20
|
+
const reply = await http.send('POST', '/buildr/data/media', { body });
|
|
21
|
+
return expectBody(reply, dataMediaResponseSchema);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DocumentAdapter } from '@next-buildr/editor';
|
|
2
|
+
import { type HttpOptions } from './http.ts';
|
|
3
|
+
export interface PayloadAdapterOptions extends HttpOptions {
|
|
4
|
+
/** The language the editor is showing now; defaults to the site's default language. */
|
|
5
|
+
readonly locale?: (() => string | undefined) | undefined;
|
|
6
|
+
/** The IANA time zone of the site's dates (default: the browser's). */
|
|
7
|
+
readonly timeZone?: string | undefined;
|
|
8
|
+
/** The route the site uses to open a draft (`routes.preview` of the plugin; default `/buildr/preview`). */
|
|
9
|
+
readonly previewRoute?: string | undefined;
|
|
10
|
+
/** Payload's admin route (default `/admin`); `cmsUrl` links into it. */
|
|
11
|
+
readonly adminRoute?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The `DocumentAdapter` of the editor (docs/editor.md#persistence-pb-087) over the builder API of
|
|
15
|
+
* the Payload plugin. Responses are checked against the contract; a document is parsed with core's
|
|
16
|
+
* `parseDocument`. A refused save (`409`, `422`) is a result; anything the adapter could not tell
|
|
17
|
+
* (no network, `5xx`, `401`, `403`, `404`) rejects with an `AdapterError`, which the editor retries.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createPayloadAdapter(options: PayloadAdapterOptions): DocumentAdapter;
|
|
20
|
+
//# sourceMappingURL=document-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/document-adapter.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,eAAe,EAOhB,MAAM,qBAAqB,CAAC;AAgB7B,OAAO,EAKL,KAAK,WAAW,EAEjB,MAAM,WAAW,CAAC;AAEnB,MAAM,WAAW,qBAAsB,SAAQ,WAAW;IACxD,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IACzD,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,2GAA2G;IAC3G,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,wEAAwE;IACxE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAwCD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,eAAe,CA0JpF"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { mediaAssetSchema, parseDocument, } from '@next-buildr/core';
|
|
2
|
+
import { conflictResponseSchema, dataContextResponseSchema, dataSchemaResponseSchema, documentResponseSchema, invalidResponseSchema, mediaListResponseSchema, publishResponseSchema, renameResponseSchema, revisionResponseSchema, samplesResponseSchema, saveResponseSchema, sessionResponseSchema, } from '../contract.js';
|
|
3
|
+
import { AdapterError, createHttp, errorOf, expectBody, } from './http.js';
|
|
4
|
+
const seg = (value) => encodeURIComponent(String(value));
|
|
5
|
+
const documentPath = (ref) => `/buildr/documents/${seg(ref.collection)}/${seg(ref.id)}`;
|
|
6
|
+
/** The server's diagnostics as core's (`details` is JSON on the wire). */
|
|
7
|
+
const diagnosticsOf = (list) => list;
|
|
8
|
+
/** A `409` or `422` of a save/publish is a value; anything else that is not `200` rejects. */
|
|
9
|
+
function refusal(reply) {
|
|
10
|
+
if (reply.status === 409) {
|
|
11
|
+
const parsed = conflictResponseSchema.safeParse(reply.body);
|
|
12
|
+
if (parsed.success) {
|
|
13
|
+
return { ok: false, kind: 'conflict', currentRevision: parsed.data.currentRevision };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (reply.status === 422) {
|
|
17
|
+
const parsed = invalidResponseSchema.safeParse(reply.body);
|
|
18
|
+
if (parsed.success) {
|
|
19
|
+
return { ok: false, kind: 'invalid', diagnostics: diagnosticsOf(parsed.data.diagnostics) };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const MIME_TYPES = ['image', 'video', 'audio'];
|
|
25
|
+
/** The one media type all of `mimeTypes` share (`image/*`, `image/png`, ...), if there is one. */
|
|
26
|
+
function mediaType(mimeTypes) {
|
|
27
|
+
if (mimeTypes === undefined || mimeTypes.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
const kinds = new Set(mimeTypes.map((mime) => mime.split('/')[0]));
|
|
30
|
+
const [only] = [...kinds];
|
|
31
|
+
return kinds.size === 1 ? MIME_TYPES.find((kind) => kind === only) : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The `DocumentAdapter` of the editor (docs/editor.md#persistence-pb-087) over the builder API of
|
|
35
|
+
* the Payload plugin. Responses are checked against the contract; a document is parsed with core's
|
|
36
|
+
* `parseDocument`. A refused save (`409`, `422`) is a result; anything the adapter could not tell
|
|
37
|
+
* (no network, `5xx`, `401`, `403`, `404`) rejects with an `AdapterError`, which the editor retries.
|
|
38
|
+
*/
|
|
39
|
+
export function createPayloadAdapter(options) {
|
|
40
|
+
const http = createHttp(options);
|
|
41
|
+
let session;
|
|
42
|
+
const loadSession = () => {
|
|
43
|
+
session ??= http
|
|
44
|
+
.send('GET', '/buildr/session')
|
|
45
|
+
.then((reply) => expectBody(reply, sessionResponseSchema))
|
|
46
|
+
.catch((error) => {
|
|
47
|
+
session = undefined;
|
|
48
|
+
throw error;
|
|
49
|
+
});
|
|
50
|
+
return session;
|
|
51
|
+
};
|
|
52
|
+
const localesOf = (current) => current.locales === undefined ? undefined : current.locales;
|
|
53
|
+
return {
|
|
54
|
+
async getSession() {
|
|
55
|
+
const current = await loadSession();
|
|
56
|
+
const locales = localesOf(current);
|
|
57
|
+
return {
|
|
58
|
+
userId: String(current.user.id),
|
|
59
|
+
canEdit: current.permissions.canEdit,
|
|
60
|
+
canPublish: current.permissions.canPublish,
|
|
61
|
+
...(locales === undefined ? {} : { locales }),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
async load(ref) {
|
|
65
|
+
const reply = await http.send('GET', documentPath(ref), {
|
|
66
|
+
query: { draft: '1', locale: options.locale?.() },
|
|
67
|
+
});
|
|
68
|
+
const body = expectBody(reply, documentResponseSchema);
|
|
69
|
+
const parsed = parseDocument(body.document);
|
|
70
|
+
if (!parsed.ok)
|
|
71
|
+
throw new AdapterError('The stored document is not valid.', reply.status);
|
|
72
|
+
return {
|
|
73
|
+
title: body.title,
|
|
74
|
+
...(body.slug === null ? {} : { slug: body.slug }),
|
|
75
|
+
status: body.status,
|
|
76
|
+
updatedAt: body.updatedAt,
|
|
77
|
+
revision: body.revision,
|
|
78
|
+
document: parsed.value,
|
|
79
|
+
contextRef: body.contextRef,
|
|
80
|
+
...(body.readOnly === true ? { readOnly: true } : {}),
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
async getRevision(ref) {
|
|
84
|
+
const reply = await http.send('GET', `${documentPath(ref)}/revision`);
|
|
85
|
+
const body = expectBody(reply, revisionResponseSchema);
|
|
86
|
+
return {
|
|
87
|
+
revision: body.revision,
|
|
88
|
+
updatedAt: body.updatedAt,
|
|
89
|
+
...(body.updatedBy === undefined ? {} : { updatedBy: body.updatedBy }),
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
async save(ref, request) {
|
|
93
|
+
const reply = await http.send('PUT', documentPath(ref), { body: request });
|
|
94
|
+
const refused = refusal(reply);
|
|
95
|
+
if (refused !== undefined)
|
|
96
|
+
return refused;
|
|
97
|
+
const body = expectBody(reply, saveResponseSchema);
|
|
98
|
+
return { ok: true, revision: body.revision, updatedAt: body.updatedAt };
|
|
99
|
+
},
|
|
100
|
+
async rename(ref, title) {
|
|
101
|
+
const reply = await http.send('PATCH', documentPath(ref), { body: { title } });
|
|
102
|
+
return expectBody(reply, renameResponseSchema);
|
|
103
|
+
},
|
|
104
|
+
async publish(ref, request) {
|
|
105
|
+
const reply = await http.send('POST', `${documentPath(ref)}/publish`, { body: request });
|
|
106
|
+
const refused = refusal(reply);
|
|
107
|
+
if (refused !== undefined)
|
|
108
|
+
return refused;
|
|
109
|
+
const body = expectBody(reply, publishResponseSchema);
|
|
110
|
+
return { ok: true, revision: body.revision, updatedAt: body.updatedAt };
|
|
111
|
+
},
|
|
112
|
+
async getDataSchema(ref) {
|
|
113
|
+
const reply = await http.send('GET', `/buildr/data-schema/${seg(ref.collection)}`);
|
|
114
|
+
return expectBody(reply, dataSchemaResponseSchema);
|
|
115
|
+
},
|
|
116
|
+
async getContext(ref, request) {
|
|
117
|
+
const current = await loadSession();
|
|
118
|
+
const locales = localesOf(current);
|
|
119
|
+
const locale = options.locale?.() ?? locales?.default ?? 'en';
|
|
120
|
+
// A sample is `collection:id` (what `listSamples` returns); a bare id belongs to the document's own collection.
|
|
121
|
+
const sample = request?.sampleId;
|
|
122
|
+
const at = sample === undefined ? -1 : sample.indexOf(':');
|
|
123
|
+
const reply = await http.send('GET', '/buildr/data/context', {
|
|
124
|
+
query: {
|
|
125
|
+
collection: sample !== undefined && at > 0 ? sample.slice(0, at) : ref.collection,
|
|
126
|
+
id: sample === undefined ? String(ref.id) : sample.slice(at + 1),
|
|
127
|
+
draft: '1',
|
|
128
|
+
locale,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const body = expectBody(reply, dataContextResponseSchema);
|
|
132
|
+
const fallbackLocales = { default: locale, fallback: false, intl: { [locale]: locale } };
|
|
133
|
+
return {
|
|
134
|
+
scopes: body.scopes,
|
|
135
|
+
locale,
|
|
136
|
+
locales: locales ?? fallbackLocales,
|
|
137
|
+
timeZone: options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
138
|
+
mode: 'canvas',
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
async listSamples(ref) {
|
|
142
|
+
const reply = await http.send('GET', `/buildr/samples/${seg(ref.collection)}`);
|
|
143
|
+
const body = expectBody(reply, samplesResponseSchema);
|
|
144
|
+
// The id is the canvas's `contextRef` (`collection:id`), so choosing a sample needs no translation.
|
|
145
|
+
return body.items.map((item) => ({ id: `${ref.collection}:${item.id}`, label: item.title }));
|
|
146
|
+
},
|
|
147
|
+
media: {
|
|
148
|
+
async search(query) {
|
|
149
|
+
const page = query.cursor === undefined ? 1 : Number(query.cursor);
|
|
150
|
+
const reply = await http.send('GET', '/buildr/media', {
|
|
151
|
+
query: {
|
|
152
|
+
search: query.text === undefined || query.text === '' ? undefined : query.text,
|
|
153
|
+
type: mediaType(query.mimeTypes),
|
|
154
|
+
page: String(Number.isInteger(page) && page >= 1 ? page : 1),
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const body = expectBody(reply, mediaListResponseSchema);
|
|
158
|
+
return {
|
|
159
|
+
items: body.items,
|
|
160
|
+
...(body.page < body.totalPages ? { nextCursor: String(body.page + 1) } : {}),
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
async upload(file, alt) {
|
|
164
|
+
const form = new FormData();
|
|
165
|
+
form.set('file', file);
|
|
166
|
+
form.set('alt', alt);
|
|
167
|
+
const reply = await http.send('POST', '/buildr/media', { body: form });
|
|
168
|
+
if (reply.status === 422)
|
|
169
|
+
throw errorOf(reply);
|
|
170
|
+
return expectBody(reply, mediaAssetSchema, [201]);
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
previewUrl(ref, request) {
|
|
174
|
+
const query = new URLSearchParams({ collection: ref.collection, id: String(ref.id) });
|
|
175
|
+
if (request?.draft === true)
|
|
176
|
+
query.set('draft', '1');
|
|
177
|
+
return `${options.previewRoute ?? '/buildr/preview'}?${query.toString()}`;
|
|
178
|
+
},
|
|
179
|
+
cmsUrl(ref) {
|
|
180
|
+
return `${(options.adminRoute ?? '/admin').replace(/\/+$/, '')}/collections/${seg(ref.collection)}/${seg(ref.id)}`;
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A call that did not come to an answer the caller can act on: no network, a `5xx`, `401`/`403`/
|
|
4
|
+
* `404`, a body that breaks the contract. (A `409` or `422` of a save is a value, not this.)
|
|
5
|
+
*/
|
|
6
|
+
export declare class AdapterError extends Error {
|
|
7
|
+
readonly status: number | undefined;
|
|
8
|
+
constructor(message: string, status?: number);
|
|
9
|
+
}
|
|
10
|
+
export interface HttpOptions {
|
|
11
|
+
/** Payload's API root, e.g. `/api` or `https://cms.example.com/api`. */
|
|
12
|
+
readonly baseUrl: string;
|
|
13
|
+
/** Defaults to the global `fetch` (looked up per call, so tests can replace it). */
|
|
14
|
+
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
15
|
+
/** Cookies travel with a same-origin request by default; use `include` for a cross-origin CMS. */
|
|
16
|
+
readonly credentials?: 'same-origin' | 'include' | 'omit' | undefined;
|
|
17
|
+
}
|
|
18
|
+
export interface Reply {
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly body: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface Http {
|
|
23
|
+
/** `path` is relative to `baseUrl` (`/buildr/session`). Never rejects for an HTTP status, only for the network. */
|
|
24
|
+
send(method: 'GET' | 'POST' | 'PUT' | 'PATCH', path: string, init?: {
|
|
25
|
+
readonly query?: Record<string, string | undefined>;
|
|
26
|
+
readonly body?: unknown;
|
|
27
|
+
}): Promise<Reply>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createHttp(options: HttpOptions): Http;
|
|
30
|
+
/** The message of an `{ error }` body, else a generic one. */
|
|
31
|
+
export declare function errorOf(reply: Reply): AdapterError;
|
|
32
|
+
/** The body of a successful reply, checked against the contract; anything else is an `AdapterError`. */
|
|
33
|
+
export declare function expectBody<S extends z.ZodType>(reply: Reply, schema: S, accepted?: readonly number[]): z.infer<S>;
|
|
34
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/adapter/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B;;;GAGG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,YAAY,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAI3C;CACF;AAED,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,GAAG,SAAS,CAAC;IACrD,kGAAkG;IAClG,QAAQ,CAAC,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;CACvE;AAED,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,IAAI;IACnB,mHAAmH;IACnH,IAAI,CACF,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,EACxC,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GACtF,OAAO,CAAC,KAAK,CAAC,CAAC;CACnB;AAID,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAsCrD;AAED,8DAA8D;AAC9D,wBAAgB,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,YAAY,CAMlD;AAED,wGAAwG;AACxG,wBAAgB,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAC5C,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,CAAC,EACT,QAAQ,GAAE,SAAS,MAAM,EAAU,GAClC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAOZ"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { errorResponseSchema } from '../contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* A call that did not come to an answer the caller can act on: no network, a `5xx`, `401`/`403`/
|
|
4
|
+
* `404`, a body that breaks the contract. (A `409` or `422` of a save is a value, not this.)
|
|
5
|
+
*/
|
|
6
|
+
export class AdapterError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
constructor(message, status) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'AdapterError';
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const trimEnd = (value) => value.replace(/\/+$/, '');
|
|
15
|
+
export function createHttp(options) {
|
|
16
|
+
const base = trimEnd(options.baseUrl);
|
|
17
|
+
return {
|
|
18
|
+
async send(method, path, init = {}) {
|
|
19
|
+
const params = new URLSearchParams();
|
|
20
|
+
for (const [key, value] of Object.entries(init.query ?? {})) {
|
|
21
|
+
if (value !== undefined)
|
|
22
|
+
params.set(key, value);
|
|
23
|
+
}
|
|
24
|
+
const search = params.toString();
|
|
25
|
+
const url = `${base}${path}${search === '' ? '' : `?${search}`}`;
|
|
26
|
+
const body = init.body;
|
|
27
|
+
const isForm = typeof FormData !== 'undefined' && body instanceof FormData;
|
|
28
|
+
let response;
|
|
29
|
+
try {
|
|
30
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
31
|
+
response = await doFetch(url, {
|
|
32
|
+
method,
|
|
33
|
+
credentials: options.credentials ?? 'same-origin',
|
|
34
|
+
headers: body === undefined || isForm ? {} : { 'content-type': 'application/json' },
|
|
35
|
+
...(body === undefined
|
|
36
|
+
? {}
|
|
37
|
+
: { body: isForm ? body : JSON.stringify(body) }),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new AdapterError(error instanceof Error ? error.message : 'The request failed.');
|
|
42
|
+
}
|
|
43
|
+
const text = await response.text().catch(() => '');
|
|
44
|
+
let parsed;
|
|
45
|
+
if (text !== '') {
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
if (response.ok)
|
|
51
|
+
throw new AdapterError('The response is not JSON.', response.status);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { status: response.status, body: parsed };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** The message of an `{ error }` body, else a generic one. */
|
|
59
|
+
export function errorOf(reply) {
|
|
60
|
+
const parsed = errorResponseSchema.safeParse(reply.body);
|
|
61
|
+
return new AdapterError(parsed.success ? parsed.data.error : `The server answered ${reply.status}.`, reply.status);
|
|
62
|
+
}
|
|
63
|
+
/** The body of a successful reply, checked against the contract; anything else is an `AdapterError`. */
|
|
64
|
+
export function expectBody(reply, schema, accepted = [200]) {
|
|
65
|
+
if (!accepted.includes(reply.status))
|
|
66
|
+
throw errorOf(reply);
|
|
67
|
+
const parsed = schema.safeParse(reply.body);
|
|
68
|
+
if (!parsed.success) {
|
|
69
|
+
throw new AdapterError('The response does not match the contract.', reply.status);
|
|
70
|
+
}
|
|
71
|
+
return parsed.data;
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapter/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC"}
|