@brew-cms/api 0.1.0 → 0.2.2
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/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/idempotency.d.ts +13 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +26 -0
- package/dist/idempotency.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +34 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +436 -0
- package/dist/router.js.map +1 -0
- package/dist/validators.d.ts +155 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +57 -0
- package/dist/validators.js.map +1 -0
- package/package.json +9 -5
- package/src/__tests__/api.test.ts +0 -416
- package/src/errors.ts +0 -42
- package/src/idempotency.ts +0 -41
- package/src/router.ts +0 -538
- package/src/validators.ts +0 -63
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const CreateDocumentRequestSchema: z.ZodObject<{
|
|
3
|
+
type: z.ZodEnum<["post", "page"]>;
|
|
4
|
+
slug: z.ZodString;
|
|
5
|
+
title: z.ZodString;
|
|
6
|
+
excerpt: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
7
|
+
sourceMarkdown: z.ZodString;
|
|
8
|
+
frontmatter: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
9
|
+
seo: z.ZodNullable<z.ZodOptional<z.ZodObject<{
|
|
10
|
+
metaTitle: z.ZodOptional<z.ZodString>;
|
|
11
|
+
metaDescription: z.ZodOptional<z.ZodString>;
|
|
12
|
+
ogImage: z.ZodOptional<z.ZodString>;
|
|
13
|
+
noIndex: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
metaTitle?: string | undefined;
|
|
16
|
+
metaDescription?: string | undefined;
|
|
17
|
+
ogImage?: string | undefined;
|
|
18
|
+
noIndex?: boolean | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
metaTitle?: string | undefined;
|
|
21
|
+
metaDescription?: string | undefined;
|
|
22
|
+
ogImage?: string | undefined;
|
|
23
|
+
noIndex?: boolean | undefined;
|
|
24
|
+
}>>>;
|
|
25
|
+
}, "strip", z.ZodTypeAny, {
|
|
26
|
+
type: "post" | "page";
|
|
27
|
+
slug: string;
|
|
28
|
+
title: string;
|
|
29
|
+
sourceMarkdown: string;
|
|
30
|
+
excerpt?: string | null | undefined;
|
|
31
|
+
frontmatter?: Record<string, unknown> | undefined;
|
|
32
|
+
seo?: {
|
|
33
|
+
metaTitle?: string | undefined;
|
|
34
|
+
metaDescription?: string | undefined;
|
|
35
|
+
ogImage?: string | undefined;
|
|
36
|
+
noIndex?: boolean | undefined;
|
|
37
|
+
} | null | undefined;
|
|
38
|
+
}, {
|
|
39
|
+
type: "post" | "page";
|
|
40
|
+
slug: string;
|
|
41
|
+
title: string;
|
|
42
|
+
sourceMarkdown: string;
|
|
43
|
+
excerpt?: string | null | undefined;
|
|
44
|
+
frontmatter?: Record<string, unknown> | undefined;
|
|
45
|
+
seo?: {
|
|
46
|
+
metaTitle?: string | undefined;
|
|
47
|
+
metaDescription?: string | undefined;
|
|
48
|
+
ogImage?: string | undefined;
|
|
49
|
+
noIndex?: boolean | undefined;
|
|
50
|
+
} | null | undefined;
|
|
51
|
+
}>;
|
|
52
|
+
export declare const UpdateDocumentRequestSchema: z.ZodObject<{
|
|
53
|
+
title: z.ZodOptional<z.ZodString>;
|
|
54
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
55
|
+
excerpt: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
56
|
+
sourceMarkdown: z.ZodOptional<z.ZodString>;
|
|
57
|
+
frontmatter: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
58
|
+
seo: z.ZodNullable<z.ZodOptional<z.ZodObject<{
|
|
59
|
+
metaTitle: z.ZodOptional<z.ZodString>;
|
|
60
|
+
metaDescription: z.ZodOptional<z.ZodString>;
|
|
61
|
+
ogImage: z.ZodOptional<z.ZodString>;
|
|
62
|
+
noIndex: z.ZodOptional<z.ZodBoolean>;
|
|
63
|
+
}, "strip", z.ZodTypeAny, {
|
|
64
|
+
metaTitle?: string | undefined;
|
|
65
|
+
metaDescription?: string | undefined;
|
|
66
|
+
ogImage?: string | undefined;
|
|
67
|
+
noIndex?: boolean | undefined;
|
|
68
|
+
}, {
|
|
69
|
+
metaTitle?: string | undefined;
|
|
70
|
+
metaDescription?: string | undefined;
|
|
71
|
+
ogImage?: string | undefined;
|
|
72
|
+
noIndex?: boolean | undefined;
|
|
73
|
+
}>>>;
|
|
74
|
+
changeSummary: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
75
|
+
}, "strip", z.ZodTypeAny, {
|
|
76
|
+
slug?: string | undefined;
|
|
77
|
+
title?: string | undefined;
|
|
78
|
+
excerpt?: string | null | undefined;
|
|
79
|
+
sourceMarkdown?: string | undefined;
|
|
80
|
+
frontmatter?: Record<string, unknown> | undefined;
|
|
81
|
+
seo?: {
|
|
82
|
+
metaTitle?: string | undefined;
|
|
83
|
+
metaDescription?: string | undefined;
|
|
84
|
+
ogImage?: string | undefined;
|
|
85
|
+
noIndex?: boolean | undefined;
|
|
86
|
+
} | null | undefined;
|
|
87
|
+
changeSummary?: string | null | undefined;
|
|
88
|
+
}, {
|
|
89
|
+
slug?: string | undefined;
|
|
90
|
+
title?: string | undefined;
|
|
91
|
+
excerpt?: string | null | undefined;
|
|
92
|
+
sourceMarkdown?: string | undefined;
|
|
93
|
+
frontmatter?: Record<string, unknown> | undefined;
|
|
94
|
+
seo?: {
|
|
95
|
+
metaTitle?: string | undefined;
|
|
96
|
+
metaDescription?: string | undefined;
|
|
97
|
+
ogImage?: string | undefined;
|
|
98
|
+
noIndex?: boolean | undefined;
|
|
99
|
+
} | null | undefined;
|
|
100
|
+
changeSummary?: string | null | undefined;
|
|
101
|
+
}>;
|
|
102
|
+
export declare const PublishDocumentRequestSchema: z.ZodObject<{
|
|
103
|
+
revisionId: z.ZodOptional<z.ZodString>;
|
|
104
|
+
}, "strip", z.ZodTypeAny, {
|
|
105
|
+
revisionId?: string | undefined;
|
|
106
|
+
}, {
|
|
107
|
+
revisionId?: string | undefined;
|
|
108
|
+
}>;
|
|
109
|
+
export declare const ScheduleDocumentRequestSchema: z.ZodObject<{
|
|
110
|
+
scheduledAt: z.ZodString;
|
|
111
|
+
revisionId: z.ZodOptional<z.ZodString>;
|
|
112
|
+
}, "strip", z.ZodTypeAny, {
|
|
113
|
+
scheduledAt: string;
|
|
114
|
+
revisionId?: string | undefined;
|
|
115
|
+
}, {
|
|
116
|
+
scheduledAt: string;
|
|
117
|
+
revisionId?: string | undefined;
|
|
118
|
+
}>;
|
|
119
|
+
export declare const CreateTopicRequestSchema: z.ZodObject<{
|
|
120
|
+
slug: z.ZodString;
|
|
121
|
+
name: z.ZodString;
|
|
122
|
+
description: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
123
|
+
}, "strip", z.ZodTypeAny, {
|
|
124
|
+
slug: string;
|
|
125
|
+
name: string;
|
|
126
|
+
description?: string | null | undefined;
|
|
127
|
+
}, {
|
|
128
|
+
slug: string;
|
|
129
|
+
name: string;
|
|
130
|
+
description?: string | null | undefined;
|
|
131
|
+
}>;
|
|
132
|
+
export declare const CreateTagRequestSchema: z.ZodObject<{
|
|
133
|
+
slug: z.ZodString;
|
|
134
|
+
name: z.ZodString;
|
|
135
|
+
}, "strip", z.ZodTypeAny, {
|
|
136
|
+
slug: string;
|
|
137
|
+
name: string;
|
|
138
|
+
}, {
|
|
139
|
+
slug: string;
|
|
140
|
+
name: string;
|
|
141
|
+
}>;
|
|
142
|
+
export declare const CreateSeriesRequestSchema: z.ZodObject<{
|
|
143
|
+
slug: z.ZodString;
|
|
144
|
+
name: z.ZodString;
|
|
145
|
+
description: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
|
146
|
+
}, "strip", z.ZodTypeAny, {
|
|
147
|
+
slug: string;
|
|
148
|
+
name: string;
|
|
149
|
+
description?: string | null | undefined;
|
|
150
|
+
}, {
|
|
151
|
+
slug: string;
|
|
152
|
+
name: string;
|
|
153
|
+
description?: string | null | undefined;
|
|
154
|
+
}>;
|
|
155
|
+
//# sourceMappingURL=validators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBtC,CAAC;AAEH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBtC,CAAC;AAEH,eAAO,MAAM,4BAA4B;;;;;;EAEvC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;EAGxC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;EAInC,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;EAGjC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;EAIpC,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const CreateDocumentRequestSchema = z.object({
|
|
3
|
+
type: z.enum(['post', 'page']),
|
|
4
|
+
slug: z.string().min(1),
|
|
5
|
+
title: z.string().min(1),
|
|
6
|
+
excerpt: z.string().optional().nullable(),
|
|
7
|
+
sourceMarkdown: z.string(),
|
|
8
|
+
frontmatter: z.record(z.string(), z.unknown()).optional(),
|
|
9
|
+
seo: z
|
|
10
|
+
.object({
|
|
11
|
+
metaTitle: z.string().optional(),
|
|
12
|
+
metaDescription: z.string().optional(),
|
|
13
|
+
ogImage: z.string().optional(),
|
|
14
|
+
noIndex: z.boolean().optional(),
|
|
15
|
+
})
|
|
16
|
+
.optional()
|
|
17
|
+
.nullable(),
|
|
18
|
+
});
|
|
19
|
+
export const UpdateDocumentRequestSchema = z.object({
|
|
20
|
+
title: z.string().optional(),
|
|
21
|
+
slug: z.string().optional(),
|
|
22
|
+
excerpt: z.string().optional().nullable(),
|
|
23
|
+
sourceMarkdown: z.string().optional(),
|
|
24
|
+
frontmatter: z.record(z.string(), z.unknown()).optional(),
|
|
25
|
+
seo: z
|
|
26
|
+
.object({
|
|
27
|
+
metaTitle: z.string().optional(),
|
|
28
|
+
metaDescription: z.string().optional(),
|
|
29
|
+
ogImage: z.string().optional(),
|
|
30
|
+
noIndex: z.boolean().optional(),
|
|
31
|
+
})
|
|
32
|
+
.optional()
|
|
33
|
+
.nullable(),
|
|
34
|
+
changeSummary: z.string().optional().nullable(),
|
|
35
|
+
});
|
|
36
|
+
export const PublishDocumentRequestSchema = z.object({
|
|
37
|
+
revisionId: z.string().optional(),
|
|
38
|
+
});
|
|
39
|
+
export const ScheduleDocumentRequestSchema = z.object({
|
|
40
|
+
scheduledAt: z.string().datetime(),
|
|
41
|
+
revisionId: z.string().optional(),
|
|
42
|
+
});
|
|
43
|
+
export const CreateTopicRequestSchema = z.object({
|
|
44
|
+
slug: z.string().min(1),
|
|
45
|
+
name: z.string().min(1),
|
|
46
|
+
description: z.string().optional().nullable(),
|
|
47
|
+
});
|
|
48
|
+
export const CreateTagRequestSchema = z.object({
|
|
49
|
+
slug: z.string().min(1),
|
|
50
|
+
name: z.string().min(1),
|
|
51
|
+
});
|
|
52
|
+
export const CreateSeriesRequestSchema = z.object({
|
|
53
|
+
slug: z.string().min(1),
|
|
54
|
+
name: z.string().min(1),
|
|
55
|
+
description: z.string().optional().nullable(),
|
|
56
|
+
});
|
|
57
|
+
//# sourceMappingURL=validators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.js","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brew-cms/api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "REST API router, OpenAPI spec, idempotency, and request validation for BrewCMS",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -12,15 +12,19 @@
|
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
15
19
|
"dependencies": {
|
|
16
|
-
"@brew-cms/content": "0.
|
|
17
|
-
"@brew-cms/core": "0.1
|
|
18
|
-
"@brew-cms/intelligence": "0.1
|
|
20
|
+
"@brew-cms/content": "0.2.2",
|
|
21
|
+
"@brew-cms/core": "0.2.1",
|
|
22
|
+
"@brew-cms/intelligence": "0.2.1",
|
|
19
23
|
"@brew-cms/policy": "0.1.0",
|
|
20
24
|
"zod": "^3.24.1"
|
|
21
25
|
},
|
|
22
26
|
"devDependencies": {
|
|
23
|
-
"@brew-cms/db": "0.1
|
|
27
|
+
"@brew-cms/db": "0.2.1"
|
|
24
28
|
},
|
|
25
29
|
"scripts": {
|
|
26
30
|
"build": "tsc -b",
|
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { handleApiRequest, type ApiContext } from '../router.js';
|
|
3
|
-
import { InMemoryIdempotencyStore } from '../idempotency.js';
|
|
4
|
-
import {
|
|
5
|
-
DocumentService,
|
|
6
|
-
WorkflowService,
|
|
7
|
-
AgentService,
|
|
8
|
-
AuditService,
|
|
9
|
-
TaxonomyService,
|
|
10
|
-
type Actor,
|
|
11
|
-
type Clock,
|
|
12
|
-
type IdGenerator,
|
|
13
|
-
} from '@brew-cms/core';
|
|
14
|
-
import { PolicyEngine } from '@brew-cms/policy';
|
|
15
|
-
import {
|
|
16
|
-
createDatabaseConnection,
|
|
17
|
-
seedInitialData,
|
|
18
|
-
SQLiteDocumentRepository,
|
|
19
|
-
SQLiteRevisionRepository,
|
|
20
|
-
SQLiteTaxonomyRepository,
|
|
21
|
-
SQLiteAuditEventRepository,
|
|
22
|
-
SQLiteAgentRepository,
|
|
23
|
-
SQLiteMediaAssetRepository,
|
|
24
|
-
} from '@brew-cms/db';
|
|
25
|
-
import {
|
|
26
|
-
DefaultSourceRegistry,
|
|
27
|
-
DeterministicMockEmbeddingProvider,
|
|
28
|
-
MemorySemanticIndexAdapter,
|
|
29
|
-
MockContentSourceAdapter,
|
|
30
|
-
IndexingService,
|
|
31
|
-
HybridRetrievalService,
|
|
32
|
-
CanonicalSourceResolver,
|
|
33
|
-
} from '@brew-cms/intelligence';
|
|
34
|
-
|
|
35
|
-
describe('REST API /api/v1 and Router', () => {
|
|
36
|
-
let ctx: ApiContext;
|
|
37
|
-
const adminActor: Actor = {
|
|
38
|
-
id: 'usr_admin_default',
|
|
39
|
-
type: 'human',
|
|
40
|
-
role: 'Admin',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
const db = createDatabaseConnection({ filePath: ':memory:' });
|
|
45
|
-
seedInitialData(db);
|
|
46
|
-
|
|
47
|
-
const docRepo = new SQLiteDocumentRepository(db);
|
|
48
|
-
const revRepo = new SQLiteRevisionRepository(db);
|
|
49
|
-
const taxRepo = new SQLiteTaxonomyRepository(db);
|
|
50
|
-
const auditRepo = new SQLiteAuditEventRepository(db);
|
|
51
|
-
const agentRepo = new SQLiteAgentRepository(db);
|
|
52
|
-
|
|
53
|
-
const policy = new PolicyEngine();
|
|
54
|
-
const clock: Clock = { now: () => new Date('2026-09-12T12:00:00Z') };
|
|
55
|
-
let idCounter = 1;
|
|
56
|
-
const idGen: IdGenerator = { generate: (p = 'id') => `${p}_${idCounter++}` };
|
|
57
|
-
|
|
58
|
-
const documentService = new DocumentService(docRepo, revRepo, auditRepo, policy, clock, idGen);
|
|
59
|
-
const workflowService = new WorkflowService(docRepo, revRepo, auditRepo, policy, clock, idGen);
|
|
60
|
-
const agentService = new AgentService(agentRepo, auditRepo, policy, clock, idGen);
|
|
61
|
-
const auditService = new AuditService(auditRepo);
|
|
62
|
-
const taxonomyService = new TaxonomyService(taxRepo);
|
|
63
|
-
|
|
64
|
-
const mediaRepo = new SQLiteMediaAssetRepository(db);
|
|
65
|
-
|
|
66
|
-
ctx = {
|
|
67
|
-
documentService,
|
|
68
|
-
workflowService,
|
|
69
|
-
agentService,
|
|
70
|
-
auditService,
|
|
71
|
-
taxonomyService,
|
|
72
|
-
revisionRepo: revRepo,
|
|
73
|
-
mediaRepo,
|
|
74
|
-
agentRepo,
|
|
75
|
-
idempotencyStore: new InMemoryIdempotencyStore(),
|
|
76
|
-
};
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('serves health endpoints', async () => {
|
|
80
|
-
const live = await handleApiRequest(
|
|
81
|
-
{ method: 'GET', path: '/api/health/live', actor: adminActor },
|
|
82
|
-
ctx
|
|
83
|
-
);
|
|
84
|
-
expect(live.status).toBe(200);
|
|
85
|
-
expect((live.body as any).status).toBe('live');
|
|
86
|
-
|
|
87
|
-
const ready = await handleApiRequest(
|
|
88
|
-
{ method: 'GET', path: '/api/health/ready', actor: adminActor },
|
|
89
|
-
ctx
|
|
90
|
-
);
|
|
91
|
-
expect(ready.status).toBe(200);
|
|
92
|
-
expect((ready.body as any).status).toBe('ready');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('creates and publishes a document via REST endpoints', async () => {
|
|
96
|
-
// 1. Create document
|
|
97
|
-
const createRes = await handleApiRequest(
|
|
98
|
-
{
|
|
99
|
-
method: 'POST',
|
|
100
|
-
path: '/api/v1/documents',
|
|
101
|
-
actor: adminActor,
|
|
102
|
-
body: {
|
|
103
|
-
type: 'post',
|
|
104
|
-
slug: 'rest-intro',
|
|
105
|
-
title: 'REST Architecture in BrewCMS',
|
|
106
|
-
sourceMarkdown: '# REST Architecture\n\nContent operating system REST APIs.',
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
ctx
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
expect(createRes.status).toBe(201);
|
|
113
|
-
const doc = (createRes.body as any).document;
|
|
114
|
-
expect(doc.id).toBeDefined();
|
|
115
|
-
expect(doc.status).toBe('DRAFT');
|
|
116
|
-
|
|
117
|
-
// 2. Publish document
|
|
118
|
-
const publishRes = await handleApiRequest(
|
|
119
|
-
{
|
|
120
|
-
method: 'POST',
|
|
121
|
-
path: `/api/v1/documents/${doc.id}/publish`,
|
|
122
|
-
actor: adminActor,
|
|
123
|
-
body: {},
|
|
124
|
-
},
|
|
125
|
-
ctx
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
expect(publishRes.status).toBe(200);
|
|
129
|
-
expect((publishRes.body as any).document.status).toBe('PUBLISHED');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('enforces idempotency using Idempotency-Key header', async () => {
|
|
133
|
-
const key = 'idem_unique_key_123';
|
|
134
|
-
|
|
135
|
-
// First mutation
|
|
136
|
-
const req1 = await handleApiRequest(
|
|
137
|
-
{
|
|
138
|
-
method: 'POST',
|
|
139
|
-
path: '/api/v1/documents',
|
|
140
|
-
headers: { 'idempotency-key': key },
|
|
141
|
-
actor: adminActor,
|
|
142
|
-
body: {
|
|
143
|
-
type: 'post',
|
|
144
|
-
slug: 'idempotent-doc',
|
|
145
|
-
title: 'Idempotency Post',
|
|
146
|
-
sourceMarkdown: '# Test',
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
ctx
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
expect(req1.status).toBe(201);
|
|
153
|
-
const initialId = (req1.body as any).document.id;
|
|
154
|
-
|
|
155
|
-
// Second identical request with same key
|
|
156
|
-
const req2 = await handleApiRequest(
|
|
157
|
-
{
|
|
158
|
-
method: 'POST',
|
|
159
|
-
path: '/api/v1/documents',
|
|
160
|
-
headers: { 'idempotency-key': key },
|
|
161
|
-
actor: adminActor,
|
|
162
|
-
body: {
|
|
163
|
-
type: 'post',
|
|
164
|
-
slug: 'idempotent-doc',
|
|
165
|
-
title: 'Idempotency Post',
|
|
166
|
-
sourceMarkdown: '# Test',
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
ctx
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
expect(req2.status).toBe(201);
|
|
173
|
-
expect(req2.headers?.['X-Cache-Lookup']).toBe('HIT');
|
|
174
|
-
expect((req2.body as any).document.id).toBe(initialId);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('returns standard machine-readable error envelope on validation failure', async () => {
|
|
178
|
-
const res = await handleApiRequest(
|
|
179
|
-
{
|
|
180
|
-
method: 'POST',
|
|
181
|
-
path: '/api/v1/documents',
|
|
182
|
-
actor: adminActor,
|
|
183
|
-
headers: { 'x-request-id': 'req_xyz' },
|
|
184
|
-
body: {
|
|
185
|
-
type: 'invalid_type',
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
ctx
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
expect(res.status).toBeGreaterThanOrEqual(400);
|
|
192
|
-
const body = res.body as any;
|
|
193
|
-
expect(body.error).toBeDefined();
|
|
194
|
-
expect(body.error.code).toBeDefined();
|
|
195
|
-
expect(body.error.requestId).toBe('req_xyz');
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('handles full editorial lifecycle: submit -> approve -> publish -> unpublish -> restore', async () => {
|
|
199
|
-
// 1. Create draft
|
|
200
|
-
const createRes = await handleApiRequest(
|
|
201
|
-
{
|
|
202
|
-
method: 'POST',
|
|
203
|
-
path: '/api/v1/documents',
|
|
204
|
-
actor: adminActor,
|
|
205
|
-
body: {
|
|
206
|
-
type: 'post',
|
|
207
|
-
slug: 'lifecycle-test',
|
|
208
|
-
title: 'Lifecycle Post',
|
|
209
|
-
sourceMarkdown: '# Lifecycle Test\n\nInitial version.',
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
ctx
|
|
213
|
-
);
|
|
214
|
-
expect(createRes.status).toBe(201);
|
|
215
|
-
const docId = (createRes.body as any).document.id;
|
|
216
|
-
const initialRevId = (createRes.body as any).revision.id;
|
|
217
|
-
|
|
218
|
-
// 2. Submit for review
|
|
219
|
-
const submitRes = await handleApiRequest(
|
|
220
|
-
{
|
|
221
|
-
method: 'POST',
|
|
222
|
-
path: `/api/v1/documents/${docId}/submit`,
|
|
223
|
-
actor: adminActor,
|
|
224
|
-
body: {},
|
|
225
|
-
},
|
|
226
|
-
ctx
|
|
227
|
-
);
|
|
228
|
-
expect(submitRes.status).toBe(200);
|
|
229
|
-
expect((submitRes.body as any).status).toBe('IN_REVIEW');
|
|
230
|
-
|
|
231
|
-
// 3. Approve
|
|
232
|
-
const approveRes = await handleApiRequest(
|
|
233
|
-
{
|
|
234
|
-
method: 'POST',
|
|
235
|
-
path: `/api/v1/documents/${docId}/approve`,
|
|
236
|
-
actor: adminActor,
|
|
237
|
-
body: {},
|
|
238
|
-
},
|
|
239
|
-
ctx
|
|
240
|
-
);
|
|
241
|
-
expect(approveRes.status).toBe(200);
|
|
242
|
-
expect((approveRes.body as any).status).toBe('APPROVED');
|
|
243
|
-
|
|
244
|
-
// 4. Publish
|
|
245
|
-
const publishRes = await handleApiRequest(
|
|
246
|
-
{
|
|
247
|
-
method: 'POST',
|
|
248
|
-
path: `/api/v1/documents/${docId}/publish`,
|
|
249
|
-
actor: adminActor,
|
|
250
|
-
body: {},
|
|
251
|
-
},
|
|
252
|
-
ctx
|
|
253
|
-
);
|
|
254
|
-
expect(publishRes.status).toBe(200);
|
|
255
|
-
expect((publishRes.body as any).document.status).toBe('PUBLISHED');
|
|
256
|
-
|
|
257
|
-
// 5. Unpublish
|
|
258
|
-
const unpublishRes = await handleApiRequest(
|
|
259
|
-
{
|
|
260
|
-
method: 'POST',
|
|
261
|
-
path: `/api/v1/documents/${docId}/unpublish`,
|
|
262
|
-
actor: adminActor,
|
|
263
|
-
body: {},
|
|
264
|
-
},
|
|
265
|
-
ctx
|
|
266
|
-
);
|
|
267
|
-
expect(unpublishRes.status).toBe(200);
|
|
268
|
-
expect((unpublishRes.body as any).status).toBe('DRAFT');
|
|
269
|
-
|
|
270
|
-
// 6. Restore initial revision
|
|
271
|
-
const restoreRes = await handleApiRequest(
|
|
272
|
-
{
|
|
273
|
-
method: 'POST',
|
|
274
|
-
path: `/api/v1/documents/${docId}/restore`,
|
|
275
|
-
actor: adminActor,
|
|
276
|
-
body: { revisionId: initialRevId },
|
|
277
|
-
},
|
|
278
|
-
ctx
|
|
279
|
-
);
|
|
280
|
-
expect(restoreRes.status).toBe(200);
|
|
281
|
-
expect((restoreRes.body as any).restoredRevision).toBeDefined();
|
|
282
|
-
expect((restoreRes.body as any).restoredRevision.sourceMarkdown).toBe('# Lifecycle Test\n\nInitial version.');
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('manages media and agent endpoints', async () => {
|
|
286
|
-
// Media list and create
|
|
287
|
-
const mediaCreate = await handleApiRequest(
|
|
288
|
-
{
|
|
289
|
-
method: 'POST',
|
|
290
|
-
path: '/api/v1/media',
|
|
291
|
-
actor: adminActor,
|
|
292
|
-
body: {
|
|
293
|
-
filename: 'banner.png',
|
|
294
|
-
mimeType: 'image/png',
|
|
295
|
-
sizeBytes: 1024,
|
|
296
|
-
altText: 'Banner Image',
|
|
297
|
-
},
|
|
298
|
-
},
|
|
299
|
-
ctx
|
|
300
|
-
);
|
|
301
|
-
expect(mediaCreate.status).toBe(201);
|
|
302
|
-
expect((mediaCreate.body as any).id).toBeDefined();
|
|
303
|
-
|
|
304
|
-
const mediaList = await handleApiRequest(
|
|
305
|
-
{
|
|
306
|
-
method: 'GET',
|
|
307
|
-
path: '/api/v1/media',
|
|
308
|
-
actor: adminActor,
|
|
309
|
-
},
|
|
310
|
-
ctx
|
|
311
|
-
);
|
|
312
|
-
expect(mediaList.status).toBe(200);
|
|
313
|
-
expect((mediaList.body as any).items.length).toBeGreaterThan(0);
|
|
314
|
-
|
|
315
|
-
// Agent list
|
|
316
|
-
const agentList = await handleApiRequest(
|
|
317
|
-
{
|
|
318
|
-
method: 'GET',
|
|
319
|
-
path: '/api/v1/agents',
|
|
320
|
-
actor: adminActor,
|
|
321
|
-
},
|
|
322
|
-
ctx
|
|
323
|
-
);
|
|
324
|
-
expect(agentList.status).toBe(200);
|
|
325
|
-
expect(Array.isArray((agentList.body as any).items)).toBe(true);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('exposes intelligence status, indexing, and hybrid retrieval endpoints', async () => {
|
|
329
|
-
// Set up intelligence services
|
|
330
|
-
const registry = new DefaultSourceRegistry();
|
|
331
|
-
const mockSource = new MockContentSourceAdapter('brew-sqlite', [
|
|
332
|
-
{
|
|
333
|
-
sourceId: 'brew-sqlite',
|
|
334
|
-
contentId: 'doc-ai-1',
|
|
335
|
-
revisionId: 'rev-1',
|
|
336
|
-
title: 'Distributed Enterprise Intelligence',
|
|
337
|
-
canonicalUrl: '/posts/distributed-enterprise-intelligence',
|
|
338
|
-
summary: 'How AI agents work with governed content workflows.',
|
|
339
|
-
body: '# Distributed Enterprise Intelligence\n\nHow AI agents work with governed content workflows and retrieval.',
|
|
340
|
-
publishedAt: '2026-09-12T00:00:00Z',
|
|
341
|
-
},
|
|
342
|
-
]);
|
|
343
|
-
registry.register(mockSource);
|
|
344
|
-
|
|
345
|
-
const embeddingProvider = new DeterministicMockEmbeddingProvider();
|
|
346
|
-
const semanticIndex = new MemorySemanticIndexAdapter();
|
|
347
|
-
const indexingService = new IndexingService(registry, embeddingProvider, semanticIndex);
|
|
348
|
-
const retrievalService = new HybridRetrievalService(embeddingProvider, semanticIndex);
|
|
349
|
-
const canonicalResolver = new CanonicalSourceResolver(registry);
|
|
350
|
-
|
|
351
|
-
const intelligenceCtx: ApiContext = {
|
|
352
|
-
...ctx,
|
|
353
|
-
sourceRegistry: registry,
|
|
354
|
-
indexingService,
|
|
355
|
-
retrievalService,
|
|
356
|
-
canonicalResolver,
|
|
357
|
-
};
|
|
358
|
-
|
|
359
|
-
// 1. Status endpoint
|
|
360
|
-
const statusRes = await handleApiRequest(
|
|
361
|
-
{ method: 'GET', path: '/api/v1/intelligence/status', actor: adminActor },
|
|
362
|
-
intelligenceCtx
|
|
363
|
-
);
|
|
364
|
-
expect(statusRes.status).toBe(200);
|
|
365
|
-
expect((statusRes.body as any).status).toBe('operational');
|
|
366
|
-
expect((statusRes.body as any).sources.length).toBe(1);
|
|
367
|
-
expect((statusRes.body as any).sources[0].id).toBe('brew-sqlite');
|
|
368
|
-
|
|
369
|
-
// 2. Trigger Indexing
|
|
370
|
-
const indexRes = await handleApiRequest(
|
|
371
|
-
{
|
|
372
|
-
method: 'POST',
|
|
373
|
-
path: '/api/v1/intelligence/index',
|
|
374
|
-
actor: adminActor,
|
|
375
|
-
body: { sourceId: 'brew-sqlite' },
|
|
376
|
-
},
|
|
377
|
-
intelligenceCtx
|
|
378
|
-
);
|
|
379
|
-
expect(indexRes.status).toBe(200);
|
|
380
|
-
expect((indexRes.body as any).summary.indexed).toBe(1);
|
|
381
|
-
|
|
382
|
-
// 3. Search endpoint (references only)
|
|
383
|
-
const searchRes = await handleApiRequest(
|
|
384
|
-
{
|
|
385
|
-
method: 'GET',
|
|
386
|
-
path: '/api/v1/intelligence/search',
|
|
387
|
-
actor: adminActor,
|
|
388
|
-
query: { q: 'enterprise intelligence' },
|
|
389
|
-
},
|
|
390
|
-
intelligenceCtx
|
|
391
|
-
);
|
|
392
|
-
expect(searchRes.status).toBe(200);
|
|
393
|
-
expect((searchRes.body as any).total).toBeGreaterThan(0);
|
|
394
|
-
const item = (searchRes.body as any).items[0];
|
|
395
|
-
expect(item.contentId).toBe('doc-ai-1');
|
|
396
|
-
expect(item.sourceId).toBe('brew-sqlite');
|
|
397
|
-
// Content body must NOT be in search reference result
|
|
398
|
-
expect(item.markdown).toBeUndefined();
|
|
399
|
-
|
|
400
|
-
// 4. Search with resolve=true (canonical resolution)
|
|
401
|
-
const resolvedSearchRes = await handleApiRequest(
|
|
402
|
-
{
|
|
403
|
-
method: 'GET',
|
|
404
|
-
path: '/api/v1/intelligence/search',
|
|
405
|
-
actor: adminActor,
|
|
406
|
-
query: { q: 'enterprise intelligence', resolve: 'true' },
|
|
407
|
-
},
|
|
408
|
-
intelligenceCtx
|
|
409
|
-
);
|
|
410
|
-
expect(resolvedSearchRes.status).toBe(200);
|
|
411
|
-
const resolvedItem = (resolvedSearchRes.body as any).items[0];
|
|
412
|
-
expect(resolvedItem.reference.contentId).toBe('doc-ai-1');
|
|
413
|
-
expect(resolvedItem.canonical.title).toBe('Distributed Enterprise Intelligence');
|
|
414
|
-
expect(resolvedItem.canonical.body).toContain('governed content workflows');
|
|
415
|
-
});
|
|
416
|
-
});
|