@actuate-media/cms-core 0.22.0 → 0.23.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=collections-discovery.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collections-discovery.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/api/collections-discovery.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Regression tests for the `/api/cms/collections` discovery endpoint
3
+ * shipped in PR F (Phase 7 — MCP-parity stylistic cleanup).
4
+ *
5
+ * Purpose: enumerate the configured collections without relying on the
6
+ * OpenAPI generator, which has historically drifted out of sync with
7
+ * the admin-visible surface. The endpoint is unauthenticated by design
8
+ * — the response contains schema-shape metadata that is already public
9
+ * via `/openapi.json` — but we test that contract here so a future
10
+ * change can't silently widen it.
11
+ */
12
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
13
+ import { handleActuateAPI } from '../../api/index.js';
14
+ import { setActuateConfig } from '../../config/runtime.js';
15
+ const SECRET = 'test-secret-collections-discovery-pr-f-12345';
16
+ describe('GET /api/cms/collections (PR F discovery endpoint)', () => {
17
+ // Snapshot every env var any test in this file touches so we can
18
+ // restore them in `afterEach`. Mirrors the pattern in health.test.ts —
19
+ // PR #155 review (Bugbot) caught that the prior implementation set
20
+ // CMS_SECRET in beforeEach but never restored it, leaking state into
21
+ // any other test that runs after this file.
22
+ const original = {
23
+ CMS_SECRET: process.env.CMS_SECRET,
24
+ CMS_SESSION_SECRET: process.env.CMS_SESSION_SECRET,
25
+ };
26
+ beforeEach(() => {
27
+ process.env.CMS_SECRET = SECRET;
28
+ delete process.env.CMS_SESSION_SECRET;
29
+ });
30
+ afterEach(() => {
31
+ for (const [k, v] of Object.entries(original)) {
32
+ if (v === undefined)
33
+ delete process.env[k];
34
+ else
35
+ process.env[k] = v;
36
+ }
37
+ delete globalThis.__actuateConfig;
38
+ });
39
+ function makeHandler() {
40
+ setActuateConfig({
41
+ secret: SECRET,
42
+ collections: {
43
+ posts: {
44
+ slug: 'posts',
45
+ labels: { singular: 'Post', plural: 'Posts' },
46
+ type: 'post',
47
+ urlPrefix: 'blog',
48
+ fields: {
49
+ title: { type: 'text', required: true },
50
+ body: { type: 'richText' },
51
+ author: { type: 'relationship', relationTo: 'users' },
52
+ },
53
+ },
54
+ pages: {
55
+ slug: 'pages',
56
+ labels: { singular: 'Page', plural: 'Pages' },
57
+ type: 'page',
58
+ fields: {
59
+ title: { type: 'text', required: true },
60
+ slug: { type: 'slug', required: true },
61
+ },
62
+ },
63
+ hidden_internal: {
64
+ slug: 'hidden_internal',
65
+ labels: { singular: 'Internal', plural: 'Internal' },
66
+ fields: { name: { type: 'text' } },
67
+ admin: { hidden: true },
68
+ },
69
+ },
70
+ });
71
+ return handleActuateAPI({ prismaClient: {} });
72
+ }
73
+ it('returns one entry per configured collection with field summaries', async () => {
74
+ const handler = makeHandler();
75
+ const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
76
+ expect(response.status).toBe(200);
77
+ const payload = (await response.json());
78
+ expect(payload.data).toHaveLength(3);
79
+ const bySlug = new Map(payload.data.map((c) => [c.slug, c]));
80
+ const posts = bySlug.get('posts');
81
+ expect(posts.labels).toEqual({ singular: 'Post', plural: 'Posts' });
82
+ expect(posts.type).toBe('post');
83
+ expect(posts.fieldCount).toBe(3);
84
+ expect(posts.fields).toEqual(expect.arrayContaining([
85
+ { name: 'title', type: 'text', required: true },
86
+ { name: 'body', type: 'richText', required: false },
87
+ { name: 'author', type: 'relationship', required: false },
88
+ ]));
89
+ const pages = bySlug.get('pages');
90
+ expect(pages.type).toBe('page');
91
+ expect(pages.fieldCount).toBe(2);
92
+ expect(pages.fields.find((f) => f.name === 'title')?.required).toBe(true);
93
+ });
94
+ it('surfaces admin.hidden as `hidden: true` so agents can choose to skip them', async () => {
95
+ // Internal collections should still be discoverable — the agent
96
+ // might legitimately want to read from them — but the flag lets
97
+ // the agent down-rank them in its planning.
98
+ const handler = makeHandler();
99
+ const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
100
+ const payload = (await response.json());
101
+ const hiddenEntry = payload.data.find((c) => c.slug === 'hidden_internal');
102
+ expect(hiddenEntry?.hidden).toBe(true);
103
+ const postsEntry = payload.data.find((c) => c.slug === 'posts');
104
+ expect(postsEntry?.hidden).toBe(false);
105
+ });
106
+ it('returns 500 when no CMS config is registered (deploy misconfig)', async () => {
107
+ // Defensive: route must error loudly when getActuateConfig() is
108
+ // missing instead of returning `{ data: [] }`, which would lie to
109
+ // the MCP fallback chain about the server's actual state.
110
+ const handler = handleActuateAPI({ prismaClient: {} });
111
+ const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
112
+ expect(response.status).toBe(500);
113
+ });
114
+ it('does not leak deeper schema (validators, hooks, access functions)', async () => {
115
+ // The response is unauthenticated — field-level validators or
116
+ // access functions could include source pointers / closure state
117
+ // that mustn't ship in the public surface. Keep the contract
118
+ // tight: only `name`, `type`, `required` per field.
119
+ const handler = makeHandler();
120
+ const response = await handler(new Request('https://example.com/api/cms/collections', { method: 'GET' }));
121
+ const payload = (await response.json());
122
+ for (const collection of payload.data) {
123
+ for (const field of collection.fields) {
124
+ expect(Object.keys(field).sort()).toEqual(['name', 'required', 'type']);
125
+ }
126
+ }
127
+ });
128
+ });
129
+ //# sourceMappingURL=collections-discovery.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collections-discovery.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/collections-discovery.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAqnB5C,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AA2ND,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA8rNzD"}
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/api/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAqnB5C,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E;AA2ND,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAquNzD"}
@@ -776,6 +776,44 @@ export function registerCMSRoutes(router) {
776
776
  headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' },
777
777
  });
778
778
  });
779
+ // ---------------------------------------------------------------------------
780
+ // Collection discovery (Phase 7 PR F)
781
+ //
782
+ // Lightweight enumeration sourced directly from `getActuateConfig()` so
783
+ // agents and the MCP server's `list_collections` tool don't depend on
784
+ // OpenAPI spec generation succeeding. OpenAPI is still the source of
785
+ // truth for full per-field JSON Schemas; this endpoint trades depth
786
+ // for reliability and surfaces just enough metadata for the agent to
787
+ // plan a follow-up call (create_document, list_documents, etc.).
788
+ //
789
+ // Auth: unauthenticated, like /openapi.json — the response contains
790
+ // only schema-shape metadata that is already public via OpenAPI.
791
+ // ---------------------------------------------------------------------------
792
+ router.get('/collections', async () => {
793
+ const config = getActuateConfig();
794
+ if (!config)
795
+ return errorResponse('CMS not configured', 500);
796
+ const collections = Object.entries(config.collections ?? {}).map(([slug, def]) => {
797
+ const fields = (def?.fields ?? {});
798
+ // Reduce each field to `{ name, type, required }` — enough for the
799
+ // agent to plan a create/update without sending the full validator.
800
+ const fieldSummary = Object.entries(fields).map(([name, fieldDef]) => ({
801
+ name,
802
+ type: typeof fieldDef?.type === 'string' ? fieldDef.type : 'unknown',
803
+ required: fieldDef?.required === true,
804
+ }));
805
+ return {
806
+ slug,
807
+ labels: def?.labels ?? { singular: slug, plural: slug },
808
+ type: def?.type ?? 'page',
809
+ urlPrefix: def?.urlPrefix ?? slug,
810
+ hidden: def?.admin?.hidden === true,
811
+ fieldCount: fieldSummary.length,
812
+ fields: fieldSummary,
813
+ };
814
+ });
815
+ return json({ data: collections });
816
+ });
779
817
  router.get('/docs', async () => {
780
818
  const html = `<!DOCTYPE html>
781
819
  <html>