@growth-labs/cms 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -5
- package/dist/cli/migrations.d.ts +3 -0
- package/dist/cli/migrations.d.ts.map +1 -0
- package/dist/cli/migrations.js +73 -0
- package/dist/cli/migrations.js.map +1 -0
- package/dist/engine/content-metadata.d.ts +13 -0
- package/dist/engine/content-metadata.d.ts.map +1 -0
- package/dist/engine/content-metadata.js +80 -0
- package/dist/engine/content-metadata.js.map +1 -0
- package/dist/engine/index.d.ts +3 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +5 -1
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/publication.d.ts +29 -1
- package/dist/engine/publication.d.ts.map +1 -1
- package/dist/engine/publication.js +197 -15
- package/dist/engine/publication.js.map +1 -1
- package/dist/engine/published-content.d.ts +40 -0
- package/dist/engine/published-content.d.ts.map +1 -0
- package/dist/engine/published-content.js +388 -0
- package/dist/engine/published-content.js.map +1 -0
- package/dist/engine/publisher.d.ts +16 -1
- package/dist/engine/publisher.d.ts.map +1 -1
- package/dist/engine/publisher.js +74 -11
- package/dist/engine/publisher.js.map +1 -1
- package/dist/engine/revisions.d.ts.map +1 -1
- package/dist/engine/revisions.js +2 -0
- package/dist/engine/revisions.js.map +1 -1
- package/dist/migration-vendor-files.d.ts +17 -0
- package/dist/migration-vendor-files.d.ts.map +1 -0
- package/dist/migration-vendor-files.js +67 -0
- package/dist/migration-vendor-files.js.map +1 -0
- package/dist/migration-vendor.d.ts +26 -0
- package/dist/migration-vendor.d.ts.map +1 -0
- package/dist/migration-vendor.js +321 -0
- package/dist/migration-vendor.js.map +1 -0
- package/dist/schema/migrations.d.ts.map +1 -1
- package/dist/schema/migrations.js +24 -0
- package/dist/schema/migrations.js.map +1 -1
- package/dist/schema/types.d.ts +6 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js.map +1 -1
- package/migrations/0020_content_metadata_read_model.sql +19 -0
- package/migrations/0021_published_revision_slug_index.sql +5 -0
- package/package.json +8 -1
- package/src/cli/migrations.ts +85 -0
- package/src/engine/content-metadata.ts +122 -0
- package/src/engine/index.ts +26 -0
- package/src/engine/publication.ts +329 -18
- package/src/engine/published-content.ts +581 -0
- package/src/engine/publisher.ts +106 -10
- package/src/engine/revisions.ts +4 -0
- package/src/migration-vendor-files.ts +100 -0
- package/src/migration-vendor.ts +450 -0
- package/src/schema/migrations.ts +24 -0
- package/src/schema/types.ts +6 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export type ContentMetadataPrimitive = string | number | boolean | null
|
|
2
|
+
export type ContentMetadataValue =
|
|
3
|
+
| ContentMetadataPrimitive
|
|
4
|
+
| ContentMetadataValue[]
|
|
5
|
+
| { [key: string]: ContentMetadataValue }
|
|
6
|
+
export type ContentMetadata = Record<string, ContentMetadataValue>
|
|
7
|
+
|
|
8
|
+
export const MAX_CONTENT_METADATA_BYTES = 32 * 1024
|
|
9
|
+
const MAX_CONTENT_METADATA_DEPTH = 8
|
|
10
|
+
const MAX_CONTENT_METADATA_NODES = 1_024
|
|
11
|
+
const UNSAFE_METADATA_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
12
|
+
|
|
13
|
+
export class ContentMetadataError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
readonly code: 'content_metadata_invalid' | 'content_metadata_limit_exceeded',
|
|
16
|
+
message: string,
|
|
17
|
+
) {
|
|
18
|
+
super(message)
|
|
19
|
+
this.name = 'ContentMetadataError'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function byteLength(value: string): number {
|
|
24
|
+
return new TextEncoder().encode(value).byteLength
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assertMetadataValue(value: unknown, depth: number, nodes: { count: number }): void {
|
|
28
|
+
nodes.count += 1
|
|
29
|
+
if (nodes.count > MAX_CONTENT_METADATA_NODES) {
|
|
30
|
+
throw new ContentMetadataError(
|
|
31
|
+
'content_metadata_limit_exceeded',
|
|
32
|
+
`content metadata exceeds ${MAX_CONTENT_METADATA_NODES} values`,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
if (depth > MAX_CONTENT_METADATA_DEPTH) {
|
|
36
|
+
throw new ContentMetadataError(
|
|
37
|
+
'content_metadata_limit_exceeded',
|
|
38
|
+
`content metadata exceeds depth ${MAX_CONTENT_METADATA_DEPTH}`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return
|
|
42
|
+
if (typeof value === 'number') {
|
|
43
|
+
if (!Number.isFinite(value)) {
|
|
44
|
+
throw new ContentMetadataError(
|
|
45
|
+
'content_metadata_invalid',
|
|
46
|
+
'content metadata numbers must be finite',
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(value)) {
|
|
52
|
+
for (const item of value) assertMetadataValue(item, depth + 1, nodes)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
if (typeof value !== 'object') {
|
|
56
|
+
throw new ContentMetadataError(
|
|
57
|
+
'content_metadata_invalid',
|
|
58
|
+
'content metadata must contain only JSON values',
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
const prototype = Object.getPrototypeOf(value)
|
|
62
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
63
|
+
throw new ContentMetadataError(
|
|
64
|
+
'content_metadata_invalid',
|
|
65
|
+
'content metadata objects must use a plain object prototype',
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
for (const [key, child] of Object.entries(value)) {
|
|
69
|
+
if (!key || UNSAFE_METADATA_KEYS.has(key)) {
|
|
70
|
+
throw new ContentMetadataError(
|
|
71
|
+
'content_metadata_invalid',
|
|
72
|
+
'content metadata contains an unsafe key',
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
assertMetadataValue(child, depth + 1, nodes)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function serializeContentMetadata(value: ContentMetadata | undefined): string {
|
|
80
|
+
const metadata = value ?? {}
|
|
81
|
+
if (typeof metadata !== 'object' || metadata === null || Array.isArray(metadata)) {
|
|
82
|
+
throw new ContentMetadataError(
|
|
83
|
+
'content_metadata_invalid',
|
|
84
|
+
'content metadata must be a JSON object',
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
assertMetadataValue(metadata, 0, { count: 0 })
|
|
88
|
+
const encoded = JSON.stringify(metadata)
|
|
89
|
+
if (byteLength(encoded) > MAX_CONTENT_METADATA_BYTES) {
|
|
90
|
+
throw new ContentMetadataError(
|
|
91
|
+
'content_metadata_limit_exceeded',
|
|
92
|
+
`content metadata exceeds ${MAX_CONTENT_METADATA_BYTES} bytes`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return encoded
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function parseContentMetadata(raw: unknown): ContentMetadata {
|
|
99
|
+
if (typeof raw !== 'string') {
|
|
100
|
+
throw new ContentMetadataError(
|
|
101
|
+
'content_metadata_invalid',
|
|
102
|
+
'stored content metadata must be JSON text',
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
if (byteLength(raw) > MAX_CONTENT_METADATA_BYTES) {
|
|
106
|
+
throw new ContentMetadataError(
|
|
107
|
+
'content_metadata_limit_exceeded',
|
|
108
|
+
`stored content metadata exceeds ${MAX_CONTENT_METADATA_BYTES} bytes`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
let parsed: unknown
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(raw)
|
|
114
|
+
} catch {
|
|
115
|
+
throw new ContentMetadataError(
|
|
116
|
+
'content_metadata_invalid',
|
|
117
|
+
'stored content metadata is not valid JSON',
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
serializeContentMetadata(parsed as ContentMetadata)
|
|
121
|
+
return parsed as ContentMetadata
|
|
122
|
+
}
|
package/src/engine/index.ts
CHANGED
|
@@ -39,6 +39,16 @@ export {
|
|
|
39
39
|
type VerifyApiKeyResult,
|
|
40
40
|
verifyCmsApiKey,
|
|
41
41
|
} from './api-keys.js'
|
|
42
|
+
// Bounded site-specific metadata retained in immutable content revisions.
|
|
43
|
+
export {
|
|
44
|
+
type ContentMetadata,
|
|
45
|
+
ContentMetadataError,
|
|
46
|
+
type ContentMetadataPrimitive,
|
|
47
|
+
type ContentMetadataValue,
|
|
48
|
+
MAX_CONTENT_METADATA_BYTES,
|
|
49
|
+
parseContentMetadata,
|
|
50
|
+
serializeContentMetadata,
|
|
51
|
+
} from './content-metadata.js'
|
|
42
52
|
// content_contributors engine — list/replace co-authors (P3 Task 3).
|
|
43
53
|
export {
|
|
44
54
|
type ContributorInput,
|
|
@@ -121,6 +131,7 @@ export {
|
|
|
121
131
|
MAX_PUBLICATION_SURFACE_HOOK_TIMEOUT_MS,
|
|
122
132
|
type PublicationAttempt,
|
|
123
133
|
type PublicationAttemptState,
|
|
134
|
+
PublicationInputError,
|
|
124
135
|
PublicationNotFoundError,
|
|
125
136
|
type PublicationOperation,
|
|
126
137
|
PublicationPointerMoveError,
|
|
@@ -150,6 +161,21 @@ export {
|
|
|
150
161
|
type PublishGuardOutcome,
|
|
151
162
|
publishGuardResponseOrNull,
|
|
152
163
|
} from './publish-guard.js'
|
|
164
|
+
// Bounded, pointer-selected public content read model.
|
|
165
|
+
export {
|
|
166
|
+
getPublishedContentById,
|
|
167
|
+
getPublishedContentBySlug,
|
|
168
|
+
type ListPublishedContentOptions,
|
|
169
|
+
listPublishedContent,
|
|
170
|
+
MAX_PUBLISHED_CONTENT_PAGE_BYTES,
|
|
171
|
+
MAX_PUBLISHED_CONTENT_PAGE_SIZE,
|
|
172
|
+
MAX_PUBLISHED_CONTENT_PAYLOAD_BYTES,
|
|
173
|
+
type PublishedContent,
|
|
174
|
+
type PublishedContentCursor,
|
|
175
|
+
type PublishedContentPage,
|
|
176
|
+
PublishedContentReadError,
|
|
177
|
+
type PublishedContentReadErrorCode,
|
|
178
|
+
} from './published-content.js'
|
|
153
179
|
// The publishing engine — content data-access core.
|
|
154
180
|
export {
|
|
155
181
|
type ArticleInput,
|