@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.
Files changed (56) hide show
  1. package/README.md +101 -5
  2. package/dist/cli/migrations.d.ts +3 -0
  3. package/dist/cli/migrations.d.ts.map +1 -0
  4. package/dist/cli/migrations.js +73 -0
  5. package/dist/cli/migrations.js.map +1 -0
  6. package/dist/engine/content-metadata.d.ts +13 -0
  7. package/dist/engine/content-metadata.d.ts.map +1 -0
  8. package/dist/engine/content-metadata.js +80 -0
  9. package/dist/engine/content-metadata.js.map +1 -0
  10. package/dist/engine/index.d.ts +3 -1
  11. package/dist/engine/index.d.ts.map +1 -1
  12. package/dist/engine/index.js +5 -1
  13. package/dist/engine/index.js.map +1 -1
  14. package/dist/engine/publication.d.ts +29 -1
  15. package/dist/engine/publication.d.ts.map +1 -1
  16. package/dist/engine/publication.js +197 -15
  17. package/dist/engine/publication.js.map +1 -1
  18. package/dist/engine/published-content.d.ts +40 -0
  19. package/dist/engine/published-content.d.ts.map +1 -0
  20. package/dist/engine/published-content.js +388 -0
  21. package/dist/engine/published-content.js.map +1 -0
  22. package/dist/engine/publisher.d.ts +16 -1
  23. package/dist/engine/publisher.d.ts.map +1 -1
  24. package/dist/engine/publisher.js +74 -11
  25. package/dist/engine/publisher.js.map +1 -1
  26. package/dist/engine/revisions.d.ts.map +1 -1
  27. package/dist/engine/revisions.js +2 -0
  28. package/dist/engine/revisions.js.map +1 -1
  29. package/dist/migration-vendor-files.d.ts +17 -0
  30. package/dist/migration-vendor-files.d.ts.map +1 -0
  31. package/dist/migration-vendor-files.js +67 -0
  32. package/dist/migration-vendor-files.js.map +1 -0
  33. package/dist/migration-vendor.d.ts +26 -0
  34. package/dist/migration-vendor.d.ts.map +1 -0
  35. package/dist/migration-vendor.js +321 -0
  36. package/dist/migration-vendor.js.map +1 -0
  37. package/dist/schema/migrations.d.ts.map +1 -1
  38. package/dist/schema/migrations.js +24 -0
  39. package/dist/schema/migrations.js.map +1 -1
  40. package/dist/schema/types.d.ts +6 -0
  41. package/dist/schema/types.d.ts.map +1 -1
  42. package/dist/schema/types.js.map +1 -1
  43. package/migrations/0020_content_metadata_read_model.sql +19 -0
  44. package/migrations/0021_published_revision_slug_index.sql +5 -0
  45. package/package.json +8 -1
  46. package/src/cli/migrations.ts +85 -0
  47. package/src/engine/content-metadata.ts +122 -0
  48. package/src/engine/index.ts +26 -0
  49. package/src/engine/publication.ts +329 -18
  50. package/src/engine/published-content.ts +581 -0
  51. package/src/engine/publisher.ts +106 -10
  52. package/src/engine/revisions.ts +4 -0
  53. package/src/migration-vendor-files.ts +100 -0
  54. package/src/migration-vendor.ts +450 -0
  55. package/src/schema/migrations.ts +24 -0
  56. 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
+ }
@@ -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,