@ekrist1/vulse 0.1.6-alpha.3 → 0.1.7-alpha.4

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 (264) hide show
  1. package/dist/cli/migrate.d.ts.map +1 -1
  2. package/dist/cli/migrate.js +3 -4
  3. package/dist/cli/setup.d.ts.map +1 -1
  4. package/dist/cli/setup.js +11 -10
  5. package/dist/core/blueprints/compile.d.ts +1 -1
  6. package/dist/core/blueprints/compile.d.ts.map +1 -1
  7. package/dist/core/blueprints/compile.js +3 -3
  8. package/dist/core/blueprints/mutations.js +2 -2
  9. package/dist/core/forms/rate-limit.d.ts +1 -1
  10. package/dist/core/forms/rate-limit.d.ts.map +1 -1
  11. package/dist/core/forms/rate-limit.js +3 -3
  12. package/dist/core/forms/unique.d.ts +1 -1
  13. package/dist/core/forms/unique.d.ts.map +1 -1
  14. package/dist/core/forms/unique.js +4 -4
  15. package/dist/core/globals/compile.d.ts +1 -1
  16. package/dist/core/globals/compile.d.ts.map +1 -1
  17. package/dist/core/globals/compile.js +2 -2
  18. package/dist/core/globals/definition.d.ts +1 -1
  19. package/dist/core/globals/definition.d.ts.map +1 -1
  20. package/dist/core/globals/definition.js +3 -3
  21. package/dist/core/repos/globals.js +3 -3
  22. package/dist/core/sha256.d.ts +3 -0
  23. package/dist/core/sha256.d.ts.map +1 -0
  24. package/dist/core/sha256.js +9 -0
  25. package/dist/integration/index.js +1 -1
  26. package/dist/integration/install-hook.d.ts.map +1 -1
  27. package/dist/integration/install-hook.js +6 -4
  28. package/dist/integration/wrangler-config.d.ts +12 -0
  29. package/dist/integration/wrangler-config.d.ts.map +1 -0
  30. package/dist/integration/wrangler-config.js +97 -0
  31. package/dist/integration/wrangler-patch.d.ts +1 -0
  32. package/dist/integration/wrangler-patch.d.ts.map +1 -1
  33. package/dist/integration/wrangler-patch.js +2 -1
  34. package/dist/server/routes/form-submit.js +1 -1
  35. package/dist/version.d.ts +1 -1
  36. package/dist/version.js +1 -1
  37. package/package.json +11 -3
  38. package/src/admin/assets/logo-mark.svg +5 -0
  39. package/src/admin/client/active-locale.ts +17 -0
  40. package/src/admin/client/api.ts +21 -0
  41. package/src/admin/client/form-from-zod.ts +7 -0
  42. package/src/admin/client/live-preview-enabled.ts +5 -0
  43. package/src/admin/components/AdminShell.astro +45 -0
  44. package/src/admin/components/AuthSettings.vue +60 -0
  45. package/src/admin/components/BlockEditor.vue +53 -0
  46. package/src/admin/components/BlueprintEditor.vue +1783 -0
  47. package/src/admin/components/CollectionKindIcon.vue +26 -0
  48. package/src/admin/components/CollectionTree.vue +220 -0
  49. package/src/admin/components/EntryEditorWithPreview.vue +130 -0
  50. package/src/admin/components/EntryForm.vue +411 -0
  51. package/src/admin/components/EntryList.vue +121 -0
  52. package/src/admin/components/EntryStatusBadge.vue +24 -0
  53. package/src/admin/components/FormEditor.vue +233 -0
  54. package/src/admin/components/FormList.vue +54 -0
  55. package/src/admin/components/GlobalSetEditor.vue +272 -0
  56. package/src/admin/components/GlobalSetList.vue +55 -0
  57. package/src/admin/components/LivePreviewPanel.vue +171 -0
  58. package/src/admin/components/LoginForm.vue +53 -0
  59. package/src/admin/components/MediaLibrary.vue +106 -0
  60. package/src/admin/components/MediaPicker.vue +49 -0
  61. package/src/admin/components/RevisionDiff.vue +11 -0
  62. package/src/admin/components/RevisionList.vue +134 -0
  63. package/src/admin/components/SeoFields.vue +113 -0
  64. package/src/admin/components/SetEditor.vue +137 -0
  65. package/src/admin/components/SetList.vue +32 -0
  66. package/src/admin/components/SettingsForm.vue +189 -0
  67. package/src/admin/components/SideNav.vue +152 -0
  68. package/src/admin/components/SubmissionDetail.vue +45 -0
  69. package/src/admin/components/SubmissionList.vue +89 -0
  70. package/src/admin/components/ToastHost.vue +33 -0
  71. package/src/admin/components/TreeRow.vue +163 -0
  72. package/src/admin/components/UserEditor.vue +186 -0
  73. package/src/admin/components/UserList.vue +46 -0
  74. package/src/admin/components/blocks/BlockItem.vue +32 -0
  75. package/src/admin/components/blocks/BlockToolbar.vue +12 -0
  76. package/src/admin/components/blocks/edit/CodeEdit.vue +18 -0
  77. package/src/admin/components/blocks/edit/EmbedEdit.vue +14 -0
  78. package/src/admin/components/blocks/edit/HeadingEdit.vue +19 -0
  79. package/src/admin/components/blocks/edit/ImageEdit.vue +40 -0
  80. package/src/admin/components/blocks/edit/ListEdit.vue +36 -0
  81. package/src/admin/components/blocks/edit/ParagraphEdit.vue +14 -0
  82. package/src/admin/components/blocks/edit/QuoteEdit.vue +18 -0
  83. package/src/admin/components/fields/BlocksField.vue +123 -0
  84. package/src/admin/components/fields/BlocksSetsPicker.vue +59 -0
  85. package/src/admin/components/fields/BoolField.vue +10 -0
  86. package/src/admin/components/fields/DateField.vue +22 -0
  87. package/src/admin/components/fields/EntriesField.vue +153 -0
  88. package/src/admin/components/fields/EntryField.vue +138 -0
  89. package/src/admin/components/fields/EnumField.vue +81 -0
  90. package/src/admin/components/fields/FieldRenderer.vue +87 -0
  91. package/src/admin/components/fields/GridField.vue +173 -0
  92. package/src/admin/components/fields/LinkField.vue +219 -0
  93. package/src/admin/components/fields/MediaField.vue +69 -0
  94. package/src/admin/components/fields/NumberField.vue +12 -0
  95. package/src/admin/components/fields/ObjectField.vue +18 -0
  96. package/src/admin/components/fields/RefField.vue +170 -0
  97. package/src/admin/components/fields/RepeaterField.vue +27 -0
  98. package/src/admin/components/fields/ReplicatorField.vue +121 -0
  99. package/src/admin/components/fields/TextField.vue +11 -0
  100. package/src/admin/components/fields/TextareaField.vue +11 -0
  101. package/src/admin/components/fields/VulseAccordionGroupNodeView.vue +82 -0
  102. package/src/admin/components/fields/VulseAccordionNodeView.vue +128 -0
  103. package/src/admin/components/fields/VulseCalloutNodeView.vue +81 -0
  104. package/src/admin/components/fields/VulseIframeNodeView.vue +112 -0
  105. package/src/admin/components/fields/VulseSetNodeView.vue +68 -0
  106. package/src/admin/components/fields/VulseVideoNodeView.vue +104 -0
  107. package/src/admin/components/fields/blocks-editor-extensions.ts +26 -0
  108. package/src/admin/components/fields/emoji-extension.ts +54 -0
  109. package/src/admin/components/fields/link-extension.ts +48 -0
  110. package/src/admin/components/fields/set-node-utils.ts +115 -0
  111. package/src/admin/components/fields/url-utils.ts +85 -0
  112. package/src/admin/components/fields/vulse-accordion-extension.ts +64 -0
  113. package/src/admin/components/fields/vulse-accordion-group-extension.ts +49 -0
  114. package/src/admin/components/fields/vulse-callout-extension.ts +53 -0
  115. package/src/admin/components/fields/vulse-iframe-extension.ts +96 -0
  116. package/src/admin/components/fields/vulse-set-extension.ts +66 -0
  117. package/src/admin/components/fields/vulse-video-extension.ts +65 -0
  118. package/src/admin/composables/toast.ts +35 -0
  119. package/src/admin/composables/useEntrySearch.ts +112 -0
  120. package/src/admin/composables/useSets.ts +31 -0
  121. package/src/admin/pages/collections/[name]/[id]/revisions.astro +27 -0
  122. package/src/admin/pages/collections/[name]/[id].astro +90 -0
  123. package/src/admin/pages/collections/[name]/index.astro +31 -0
  124. package/src/admin/pages/collections/[name]/new.astro +38 -0
  125. package/src/admin/pages/forms/[handle]/submissions/[id].astro +9 -0
  126. package/src/admin/pages/forms/[handle]/submissions/index.astro +9 -0
  127. package/src/admin/pages/forms/[handle].astro +9 -0
  128. package/src/admin/pages/forms/index.astro +7 -0
  129. package/src/admin/pages/forms/new.astro +7 -0
  130. package/src/admin/pages/index.astro +36 -0
  131. package/src/admin/pages/login.astro +14 -0
  132. package/src/admin/pages/media.astro +8 -0
  133. package/src/admin/pages/schema/[handle].astro +10 -0
  134. package/src/admin/pages/schema/new.astro +9 -0
  135. package/src/admin/pages/settings/auth.astro +9 -0
  136. package/src/admin/pages/settings/globals/[handle].astro +9 -0
  137. package/src/admin/pages/settings/globals/index.astro +7 -0
  138. package/src/admin/pages/settings/globals/new.astro +7 -0
  139. package/src/admin/pages/settings/index.astro +8 -0
  140. package/src/admin/pages/settings/sets/[handle].astro +9 -0
  141. package/src/admin/pages/settings/sets/index.astro +7 -0
  142. package/src/admin/pages/settings/sets/new.astro +7 -0
  143. package/src/admin/pages/users/[id].astro +10 -0
  144. package/src/admin/pages/users/index.astro +8 -0
  145. package/src/admin/styles/admin.css +166 -0
  146. package/src/core/access.ts +9 -0
  147. package/src/core/blocks/schema.ts +66 -0
  148. package/src/core/blueprints/code-to-definition.ts +156 -0
  149. package/src/core/blueprints/compile.ts +176 -0
  150. package/src/core/blueprints/define.ts +12 -0
  151. package/src/core/blueprints/definition.ts +185 -0
  152. package/src/core/blueprints/load.ts +144 -0
  153. package/src/core/blueprints/mutations.ts +236 -0
  154. package/src/core/blueprints/preview-path.ts +33 -0
  155. package/src/core/blueprints/reflect-fields.ts +305 -0
  156. package/src/core/blueprints/registry.ts +14 -0
  157. package/src/core/blueprints/seed.ts +20 -0
  158. package/src/core/blueprints/select-helpers.ts +30 -0
  159. package/src/core/blueprints/seo.ts +180 -0
  160. package/src/core/blueprints/types.ts +59 -0
  161. package/src/core/blueprints/zod-helpers.ts +86 -0
  162. package/src/core/db.ts +11 -0
  163. package/src/core/errors.ts +34 -0
  164. package/src/core/forms/compile.ts +84 -0
  165. package/src/core/forms/definition.ts +102 -0
  166. package/src/core/forms/rate-limit.ts +52 -0
  167. package/src/core/forms/unique.ts +38 -0
  168. package/src/core/globals/compile.ts +35 -0
  169. package/src/core/globals/definition.ts +27 -0
  170. package/src/core/locales.ts +45 -0
  171. package/src/core/migrations.ts +48 -0
  172. package/src/core/parse-content.ts +85 -0
  173. package/src/core/plugins/definition.ts +150 -0
  174. package/src/core/preview-content.ts +21 -0
  175. package/src/core/repos/entries.ts +504 -0
  176. package/src/core/repos/forms.ts +270 -0
  177. package/src/core/repos/globals.ts +179 -0
  178. package/src/core/repos/media.ts +106 -0
  179. package/src/core/repos/preview-sessions.ts +108 -0
  180. package/src/core/repos/revisions.ts +60 -0
  181. package/src/core/repos/settings.ts +23 -0
  182. package/src/core/schema.ts +244 -0
  183. package/src/core/sets/compile.ts +12 -0
  184. package/src/core/sets/definition.ts +10 -0
  185. package/src/core/sets/service.ts +82 -0
  186. package/src/core/sets/validate-tree.ts +57 -0
  187. package/src/core/sha256.ts +10 -0
  188. package/src/core/slug.ts +30 -0
  189. package/src/scaffold/collection-write.ts +83 -0
  190. package/src/scaffold/collection.ts +277 -0
  191. package/src/server/assets/live-preview-bridge.content.ts +2 -0
  192. package/src/server/assets/live-preview-bridge.js +535 -0
  193. package/src/server/better-auth.ts +82 -0
  194. package/src/server/cf-images.ts +34 -0
  195. package/src/server/cron.ts +37 -0
  196. package/src/server/email.ts +17 -0
  197. package/src/server/endpoints/api-auth.ts +10 -0
  198. package/src/server/endpoints/api-vulse-blueprints.ts +23 -0
  199. package/src/server/endpoints/api-vulse-entries-locales.ts +12 -0
  200. package/src/server/endpoints/api-vulse-entries-move.ts +7 -0
  201. package/src/server/endpoints/api-vulse-entries-publish.ts +7 -0
  202. package/src/server/endpoints/api-vulse-entries-tree.ts +7 -0
  203. package/src/server/endpoints/api-vulse-entries.ts +23 -0
  204. package/src/server/endpoints/api-vulse-form-handle.ts +30 -0
  205. package/src/server/endpoints/api-vulse-form-public.ts +7 -0
  206. package/src/server/endpoints/api-vulse-form-submit.ts +7 -0
  207. package/src/server/endpoints/api-vulse-form-upload.ts +7 -0
  208. package/src/server/endpoints/api-vulse-forms.ts +12 -0
  209. package/src/server/endpoints/api-vulse-globals-handle.ts +20 -0
  210. package/src/server/endpoints/api-vulse-globals-public-handle.ts +7 -0
  211. package/src/server/endpoints/api-vulse-globals-public.ts +7 -0
  212. package/src/server/endpoints/api-vulse-globals-value.ts +8 -0
  213. package/src/server/endpoints/api-vulse-globals.ts +12 -0
  214. package/src/server/endpoints/api-vulse-media-file.ts +7 -0
  215. package/src/server/endpoints/api-vulse-media-id.ts +12 -0
  216. package/src/server/endpoints/api-vulse-media.ts +12 -0
  217. package/src/server/endpoints/api-vulse-preview-bridge.ts +11 -0
  218. package/src/server/endpoints/api-vulse-preview-sessions-id.ts +10 -0
  219. package/src/server/endpoints/api-vulse-preview-sessions.ts +7 -0
  220. package/src/server/endpoints/api-vulse-preview-start.ts +7 -0
  221. package/src/server/endpoints/api-vulse-preview-stop.ts +7 -0
  222. package/src/server/endpoints/api-vulse-revisions-restore.ts +7 -0
  223. package/src/server/endpoints/api-vulse-revisions.ts +7 -0
  224. package/src/server/endpoints/api-vulse-search.ts +7 -0
  225. package/src/server/endpoints/api-vulse-sets.ts +23 -0
  226. package/src/server/endpoints/api-vulse-settings.ts +12 -0
  227. package/src/server/endpoints/api-vulse-users-id.ts +12 -0
  228. package/src/server/endpoints/api-vulse-users-reset-password.ts +7 -0
  229. package/src/server/endpoints/api-vulse-users-role.ts +9 -0
  230. package/src/server/endpoints/api-vulse-users.ts +7 -0
  231. package/src/server/endpoints/with-runtime.ts +11 -0
  232. package/src/server/env.ts +23 -0
  233. package/src/server/envelope.ts +21 -0
  234. package/src/server/forms/email.ts +11 -0
  235. package/src/server/forms/process-submission.ts +95 -0
  236. package/src/server/forms/queue.ts +25 -0
  237. package/src/server/forms/templates.ts +24 -0
  238. package/src/server/forms/webhook.ts +19 -0
  239. package/src/server/handler.ts +66 -0
  240. package/src/server/image-probe.ts +35 -0
  241. package/src/server/loader.ts +54 -0
  242. package/src/server/plugins.ts +214 -0
  243. package/src/server/preview.ts +25 -0
  244. package/src/server/r2.ts +13 -0
  245. package/src/server/routes/blueprints.ts +62 -0
  246. package/src/server/routes/entries.ts +255 -0
  247. package/src/server/routes/form-submit.ts +168 -0
  248. package/src/server/routes/form-upload.ts +100 -0
  249. package/src/server/routes/forms.ts +88 -0
  250. package/src/server/routes/globals-public.ts +30 -0
  251. package/src/server/routes/globals.ts +93 -0
  252. package/src/server/routes/media.ts +145 -0
  253. package/src/server/routes/preview-sessions.ts +76 -0
  254. package/src/server/routes/preview.ts +36 -0
  255. package/src/server/routes/revisions.ts +29 -0
  256. package/src/server/routes/search.ts +31 -0
  257. package/src/server/routes/sets.ts +40 -0
  258. package/src/server/routes/settings.ts +24 -0
  259. package/src/server/routes/users.ts +127 -0
  260. package/src/server/runtime.ts +99 -0
  261. package/src/server/sdk/collections.ts +98 -0
  262. package/src/server/sdk/index.ts +25 -0
  263. package/src/server/sdk/media.ts +11 -0
  264. package/src/server/sdk/search.ts +90 -0
@@ -0,0 +1,185 @@
1
+ import { z } from 'astro/zod'
2
+ import { SeoFieldMappingSchema } from './seo.js'
3
+
4
+ export const SelectOptionSchema = z.union([
5
+ z.string().min(1),
6
+ z.object({ key: z.string().min(1), label: z.string().min(1) }),
7
+ ])
8
+
9
+ export const LinkValueSchema = z.discriminatedUnion('type', [
10
+ z.object({ type: z.literal('url'), url: z.string().min(1) }),
11
+ z.object({
12
+ type: z.literal('entry'),
13
+ entryId: z.string().min(1),
14
+ collection: z.string().min(1),
15
+ }),
16
+ z.object({ type: z.literal('first-child') }),
17
+ ])
18
+
19
+ const textFieldUiSchema = z.object({ kind: z.literal('text') })
20
+ const textareaFieldUiSchema = z.object({ kind: z.literal('textarea') })
21
+ const blocksFieldUiSchema = z.object({
22
+ kind: z.literal('blocks'),
23
+ sets: z.array(z.string().regex(/^[a-z][a-z0-9_-]*$/)).optional(),
24
+ })
25
+ const dateFieldUiSchema = z.object({ kind: z.literal('date') })
26
+ const booleanFieldUiSchema = z.object({ kind: z.literal('boolean') })
27
+ const selectFieldUiSchema = z.object({
28
+ kind: z.literal('select'),
29
+ options: z.array(SelectOptionSchema).min(1),
30
+ multiple: z.boolean().optional(),
31
+ placeholder: z.string().optional(),
32
+ clearable: z.boolean().optional(),
33
+ })
34
+ const relationshipFieldUiSchema = z.object({
35
+ kind: z.literal('relationship'),
36
+ to: z.string().min(1),
37
+ })
38
+ const entryFieldUiSchema = z.object({
39
+ kind: z.literal('entry'),
40
+ collections: z.array(z.string().min(1)).min(1),
41
+ })
42
+ const entriesFieldUiSchema = z.object({
43
+ kind: z.literal('entries'),
44
+ collections: z.array(z.string().min(1)).min(1),
45
+ max: z.number().int().positive().optional(),
46
+ })
47
+ const linkFieldUiSchema = z.object({
48
+ kind: z.literal('link'),
49
+ collections: z.array(z.string().min(1)).optional(),
50
+ })
51
+ const assetFieldUiSchema = z.object({ kind: z.literal('asset') })
52
+
53
+ const nonReplicatorFieldUiSchemas = [
54
+ textFieldUiSchema,
55
+ textareaFieldUiSchema,
56
+ blocksFieldUiSchema,
57
+ dateFieldUiSchema,
58
+ booleanFieldUiSchema,
59
+ selectFieldUiSchema,
60
+ relationshipFieldUiSchema,
61
+ entryFieldUiSchema,
62
+ entriesFieldUiSchema,
63
+ linkFieldUiSchema,
64
+ assetFieldUiSchema,
65
+ ] as const
66
+
67
+ export const NonReplicatorFieldUiSchema = z.discriminatedUnion('kind', nonReplicatorFieldUiSchemas)
68
+
69
+ export const FieldValidationSchema = z
70
+ .object({
71
+ min: z.number().int().nonnegative().optional(),
72
+ max: z.number().int().positive().optional(),
73
+ })
74
+ .optional()
75
+
76
+ export const NestedFieldDefinitionSchema = z.object({
77
+ name: z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/),
78
+ label: z.string().optional(),
79
+ ui: NonReplicatorFieldUiSchema,
80
+ optional: z.boolean(),
81
+ default: z.unknown().optional(),
82
+ validation: FieldValidationSchema,
83
+ })
84
+
85
+ export const ReplicatorSetSchema = z.object({
86
+ name: z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/),
87
+ label: z.string().optional(),
88
+ fields: z.array(NestedFieldDefinitionSchema).min(1),
89
+ })
90
+
91
+ const replicatorFieldUiSchema = z.object({
92
+ kind: z.literal('replicator'),
93
+ sets: z.array(ReplicatorSetSchema).min(1),
94
+ })
95
+
96
+ const gridFieldUiSchema = z.object({
97
+ kind: z.literal('grid'),
98
+ fields: z.array(NestedFieldDefinitionSchema).min(1),
99
+ minRows: z.number().int().nonnegative().optional(),
100
+ maxRows: z.number().int().positive().optional(),
101
+ mode: z.enum(['table', 'stacked']).optional(),
102
+ addLabel: z.string().optional(),
103
+ })
104
+
105
+ const fieldUiSchemas = [
106
+ ...nonReplicatorFieldUiSchemas,
107
+ replicatorFieldUiSchema,
108
+ gridFieldUiSchema,
109
+ ] as const
110
+
111
+ export const FieldUiSchema = z.discriminatedUnion('kind', fieldUiSchemas)
112
+
113
+ export const FieldDefinitionSchema = z.object({
114
+ name: z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/),
115
+ label: z.string().optional(),
116
+ ui: FieldUiSchema,
117
+ optional: z.boolean(),
118
+ default: z.unknown().optional(),
119
+ validation: FieldValidationSchema,
120
+ })
121
+
122
+ export const PreviewDefinitionSchema = z.object({
123
+ path: z
124
+ .string()
125
+ .min(1)
126
+ .refine((p) => p.startsWith('/'), 'path must start with /')
127
+ .refine((p) => p.includes('{slug}'), 'path must include {slug}'),
128
+ rootSelector: z.string().min(1).optional(),
129
+ live: z.boolean().optional(),
130
+ })
131
+
132
+ const BlueprintDefinitionObjectSchema = z.object({
133
+ handle: z.string().regex(/^[a-z][a-z0-9_-]*$/),
134
+ label: z.string().min(1),
135
+ singleton: z.boolean(),
136
+ tree: z.boolean().optional(),
137
+ maxDepth: z.number().int().positive().optional(),
138
+ drafts: z.boolean().optional(),
139
+ seo: z.boolean().optional(),
140
+ seoMapping: SeoFieldMappingSchema.optional(),
141
+ preview: PreviewDefinitionSchema.optional(),
142
+ fields: z.array(FieldDefinitionSchema).min(1),
143
+ })
144
+
145
+ function checkBlueprintConstraints(
146
+ d: { singleton: boolean; tree?: boolean | undefined; maxDepth?: number | undefined },
147
+ ctx: z.RefinementCtx,
148
+ ) {
149
+ if (d.singleton && d.tree) {
150
+ ctx.addIssue({
151
+ code: 'custom',
152
+ message: 'A blueprint cannot be both singleton and tree-structured.',
153
+ path: ['tree'],
154
+ })
155
+ }
156
+ if (d.maxDepth !== undefined && !d.tree) {
157
+ ctx.addIssue({
158
+ code: 'custom',
159
+ message: 'maxDepth requires tree: true.',
160
+ path: ['maxDepth'],
161
+ })
162
+ }
163
+ }
164
+
165
+ export const BlueprintDefinitionSchema =
166
+ BlueprintDefinitionObjectSchema.superRefine(checkBlueprintConstraints)
167
+
168
+ export type SelectOption = z.infer<typeof SelectOptionSchema>
169
+ export type LinkValue = z.infer<typeof LinkValueSchema>
170
+ export type NonReplicatorFieldUi = z.infer<typeof NonReplicatorFieldUiSchema>
171
+ export type NestedFieldDefinition = z.infer<typeof NestedFieldDefinitionSchema>
172
+ export type ReplicatorSetDefinition = z.infer<typeof ReplicatorSetSchema>
173
+ export type FieldUi = z.infer<typeof FieldUiSchema>
174
+ export type FieldDefinition = z.infer<typeof FieldDefinitionSchema>
175
+ export type PreviewDefinition = z.infer<typeof PreviewDefinitionSchema>
176
+ export type BlueprintDefinition = z.infer<typeof BlueprintDefinitionSchema>
177
+
178
+ export const FieldDefinitionWithRenameSchema = FieldDefinitionSchema.extend({
179
+ previousName: z.string().optional(),
180
+ })
181
+ export const BlueprintDefinitionWithRenamesSchema = BlueprintDefinitionObjectSchema.extend({
182
+ fields: z.array(FieldDefinitionWithRenameSchema).min(1),
183
+ }).superRefine(checkBlueprintConstraints)
184
+ export type FieldDefinitionWithRename = z.infer<typeof FieldDefinitionWithRenameSchema>
185
+ export type BlueprintDefinitionWithRenames = z.infer<typeof BlueprintDefinitionWithRenamesSchema>
@@ -0,0 +1,144 @@
1
+ import type { z } from 'astro/zod'
2
+ import { BlueprintRegistry } from './registry.js'
3
+ import type { Blueprint } from './types.js'
4
+ import type { VulseDb } from '../db.js'
5
+ import { createDb } from '../db.js'
6
+ import { compileBlueprintSchema } from './compile.js'
7
+ import { listBlueprintDefinitions } from './mutations.js'
8
+ import { seedCodeBlueprints } from './seed.js'
9
+ import { loadCompiledSets } from '../sets/service.js'
10
+ import { toPreviewConfig } from './preview-path.js'
11
+ import { applySeoToSchema, type SeoFieldMapping } from './seo.js'
12
+
13
+ let registryCache: BlueprintRegistry | null = null
14
+ let seededBlueprints: Blueprint[] | null = null
15
+
16
+ async function loadBlueprintModules(): Promise<Blueprint[]> {
17
+ if (seededBlueprints) return seededBlueprints
18
+ try {
19
+ const mod = await import('virtual:vulse-blueprints')
20
+ return mod.default as Blueprint[]
21
+ } catch {
22
+ return []
23
+ }
24
+ }
25
+
26
+ function finalizeBlueprint(bp: Blueprint): Blueprint {
27
+ const seo = bp.seo === true
28
+ if (!seo) return bp
29
+ return {
30
+ ...bp,
31
+ seo: true,
32
+ schema: applySeoToSchema(bp.schema as z.ZodObject<z.ZodRawShape>),
33
+ }
34
+ }
35
+
36
+ function normalizeSeoMapping(
37
+ mapping: Partial<Record<keyof SeoFieldMapping, string | undefined>> | undefined,
38
+ ): SeoFieldMapping | undefined {
39
+ if (!mapping) return undefined
40
+ const out: SeoFieldMapping = {}
41
+ if (mapping.metaTitle !== undefined) out.metaTitle = mapping.metaTitle
42
+ if (mapping.metaDescription !== undefined) out.metaDescription = mapping.metaDescription
43
+ if (mapping.ogImage !== undefined) out.ogImage = mapping.ogImage
44
+ return Object.keys(out).length ? out : undefined
45
+ }
46
+
47
+ function mergeAdmin(compiled: Blueprint, code?: Blueprint): Blueprint['admin'] {
48
+ const admin = code?.admin ?? compiled.admin
49
+ const seoMapping = normalizeSeoMapping(code?.admin?.seoMapping ?? compiled.definition?.seoMapping)
50
+ if (!seoMapping) return admin
51
+ return { ...admin, seoMapping }
52
+ }
53
+
54
+ function mergeBlueprint(compiled: Blueprint, code?: Blueprint): Blueprint {
55
+ const rawPreview = code?.preview ?? compiled.preview
56
+ const preview = rawPreview ? toPreviewConfig(rawPreview) : undefined
57
+ const seo = code?.seo ?? compiled.seo
58
+ return finalizeBlueprint({
59
+ ...compiled,
60
+ admin: mergeAdmin(compiled, code),
61
+ ...(code?.access ? { access: code.access } : {}),
62
+ ...(preview ? { preview } : {}),
63
+ ...(seo ? { seo: true } : {}),
64
+ })
65
+ }
66
+
67
+ function inferAdmin(def: Blueprint['definition']): Blueprint['admin'] {
68
+ const titleField = def?.fields.find((f) => f.ui.kind === 'text')?.name ?? def?.fields[0]?.name ?? 'id'
69
+ return { titleField, listColumns: [titleField] }
70
+ }
71
+
72
+ export async function registryFromDb(db: VulseDb): Promise<BlueprintRegistry> {
73
+ const codeBlueprints = await loadBlueprintModules()
74
+ await seedCodeBlueprints(db, codeBlueprints)
75
+ const sets = await loadCompiledSets(db)
76
+ const codeByName = new Map(codeBlueprints.map((bp) => [bp.name, bp]))
77
+ const reg = new BlueprintRegistry()
78
+
79
+ const definitions = await listBlueprintDefinitions(db)
80
+ for (const def of definitions) {
81
+ const schema = compileBlueprintSchema(def, { sets })
82
+ const code = codeByName.get(def.handle)
83
+ const bp: Blueprint = mergeBlueprint({
84
+ name: def.handle,
85
+ label: def.label,
86
+ schema,
87
+ admin: code?.admin ?? inferAdmin(def),
88
+ singleton: def.singleton,
89
+ fields: def.fields,
90
+ definition: def,
91
+ ...(def.tree !== undefined ? { tree: def.tree } : {}),
92
+ ...(def.maxDepth !== undefined ? { maxDepth: def.maxDepth } : {}),
93
+ ...(def.drafts !== undefined ? { drafts: def.drafts } : {}),
94
+ ...(def.seo !== undefined ? { seo: def.seo } : {}),
95
+ ...(def.preview ? { preview: toPreviewConfig(def.preview) } : {}),
96
+ }, code)
97
+ reg.register(bp)
98
+ }
99
+
100
+ for (const code of codeBlueprints) {
101
+ if (!reg.has(code.name)) reg.register(finalizeBlueprint(code))
102
+ }
103
+
104
+ return reg
105
+ }
106
+
107
+ /** Load registry from D1 when available, otherwise code-only blueprints. */
108
+ export async function registryForRequest(db?: VulseDb): Promise<BlueprintRegistry> {
109
+ if (db) {
110
+ return registryFromDb(db)
111
+ }
112
+ if (registryCache) return registryCache
113
+ try {
114
+ const { getRuntimeEnv } = await import('../../server/env.js')
115
+ const env = getRuntimeEnv()
116
+ const conn = createDb(env.DB)
117
+ registryCache = await registryFromDb(conn)
118
+ return registryCache
119
+ } catch {
120
+ return registryFromUserCollections()
121
+ }
122
+ }
123
+
124
+ export async function registryFromUserCollections(db?: VulseDb): Promise<BlueprintRegistry> {
125
+ if (db) return registryFromDb(db)
126
+ if (registryCache) return registryCache
127
+ const reg = new BlueprintRegistry()
128
+ for (const bp of await loadBlueprintModules()) {
129
+ reg.register(finalizeBlueprint(bp))
130
+ }
131
+ registryCache = reg
132
+ return reg
133
+ }
134
+
135
+ /** For tests: bypass blueprint loading with explicit blueprints. */
136
+ export function _seedRegistry(blueprints: Blueprint[]): void {
137
+ seededBlueprints = blueprints
138
+ registryCache = null
139
+ }
140
+
141
+ export function _resetRegistry(): void {
142
+ registryCache = null
143
+ seededBlueprints = null
144
+ }
@@ -0,0 +1,236 @@
1
+ import { asc, eq, sql } from 'drizzle-orm'
2
+ import type { VulseDb } from '../db.js'
3
+ import { vulseCollections } from '../schema.js'
4
+ import { NotFoundError, ValidationError } from '../errors.js'
5
+ import { hashDefinition } from './compile.js'
6
+ import {
7
+ type BlueprintDefinition,
8
+ BlueprintDefinitionSchema,
9
+ type BlueprintDefinitionWithRenames,
10
+ BlueprintDefinitionWithRenamesSchema,
11
+ type FieldDefinitionWithRename,
12
+ type FieldUi,
13
+ type NestedFieldDefinition,
14
+ } from './definition.js'
15
+
16
+ export async function createBlueprint(
17
+ db: VulseDb,
18
+ input: BlueprintDefinition,
19
+ ): Promise<BlueprintDefinition> {
20
+ const def = await validateNew(db, input)
21
+ const now = Date.now()
22
+ await db.insert(vulseCollections).values({
23
+ handle: def.handle,
24
+ label: def.label,
25
+ definition: def,
26
+ blueprintHash: await hashDefinition(def),
27
+ singleton: def.singleton,
28
+ tree: def.tree === true,
29
+ drafts: def.drafts === true,
30
+ createdAt: new Date(now),
31
+ updatedAt: new Date(now),
32
+ })
33
+ return def
34
+ }
35
+
36
+ export async function updateBlueprint(
37
+ db: VulseDb,
38
+ handle: string,
39
+ input: BlueprintDefinitionWithRenames,
40
+ ): Promise<BlueprintDefinition> {
41
+ const existing = await loadDefinition(db, handle)
42
+ if (!existing) throw new NotFoundError(`blueprint not found: ${handle}`)
43
+
44
+ const incoming = { ...input, handle }
45
+ const parsed = parseOrThrow(BlueprintDefinitionWithRenamesSchema, incoming)
46
+
47
+ const oldNames = new Set(existing.fields.map((f) => f.name))
48
+ for (const f of parsed.fields) {
49
+ if (f.previousName !== undefined && !oldNames.has(f.previousName)) {
50
+ throw new ValidationError(`previousName '${f.previousName}' was not in the prior definition`, {
51
+ issues: [{ path: ['fields', parsed.fields.indexOf(f), 'previousName'] }],
52
+ })
53
+ }
54
+ }
55
+
56
+ await ensureValidCrossField(db, parsed, handle)
57
+
58
+ const renames = computeRenames(parsed.fields)
59
+ const canonical = stripRenames(parsed)
60
+
61
+ for (const [oldName, newName] of renames) {
62
+ // Per-locale content lives in `vulse_entry_locales`. Rename keys in both
63
+ // the live `content` and the in-flight `draft_content` when present.
64
+ await db.run(sql`
65
+ UPDATE vulse_entry_locales
66
+ SET content = json_set(
67
+ json_remove(content, '$.' || ${oldName}),
68
+ '$.' || ${newName},
69
+ json_extract(content, '$.' || ${oldName})
70
+ )
71
+ WHERE collection = ${handle}
72
+ AND json_extract(content, '$.' || ${oldName}) IS NOT NULL
73
+ `)
74
+ await db.run(sql`
75
+ UPDATE vulse_entry_locales
76
+ SET draft_content = json_set(
77
+ json_remove(draft_content, '$.' || ${oldName}),
78
+ '$.' || ${newName},
79
+ json_extract(draft_content, '$.' || ${oldName})
80
+ )
81
+ WHERE collection = ${handle}
82
+ AND draft_content IS NOT NULL
83
+ AND json_extract(draft_content, '$.' || ${oldName}) IS NOT NULL
84
+ `)
85
+ }
86
+ await db.update(vulseCollections).set({
87
+ label: canonical.label,
88
+ definition: canonical,
89
+ blueprintHash: await hashDefinition(canonical),
90
+ singleton: canonical.singleton,
91
+ tree: canonical.tree === true,
92
+ drafts: canonical.drafts === true,
93
+ updatedAt: new Date(),
94
+ }).where(eq(vulseCollections.handle, handle))
95
+
96
+ return canonical
97
+ }
98
+
99
+ export async function deleteBlueprint(db: VulseDb, handle: string): Promise<void> {
100
+ const existing = await db.select({ handle: vulseCollections.handle })
101
+ .from(vulseCollections)
102
+ .where(eq(vulseCollections.handle, handle))
103
+ .get()
104
+ if (!existing) throw new NotFoundError(`blueprint not found: ${handle}`)
105
+ await db.delete(vulseCollections).where(eq(vulseCollections.handle, handle))
106
+ }
107
+
108
+ export async function listBlueprintDefinitions(db: VulseDb): Promise<BlueprintDefinition[]> {
109
+ const rows = await db.select({ definition: vulseCollections.definition })
110
+ .from(vulseCollections)
111
+ .orderBy(asc(vulseCollections.createdAt))
112
+ return rows.map((r) => BlueprintDefinitionSchema.parse(r.definition))
113
+ }
114
+
115
+ export async function getBlueprintDefinition(db: VulseDb, handle: string): Promise<BlueprintDefinition | null> {
116
+ const row = await db.select({ definition: vulseCollections.definition })
117
+ .from(vulseCollections)
118
+ .where(eq(vulseCollections.handle, handle))
119
+ .get()
120
+ if (!row) return null
121
+ return BlueprintDefinitionSchema.parse(row.definition)
122
+ }
123
+
124
+ async function validateNew(db: VulseDb, input: BlueprintDefinition): Promise<BlueprintDefinition> {
125
+ const def = parseOrThrow(BlueprintDefinitionSchema, input)
126
+ const dup = await db.select({ handle: vulseCollections.handle })
127
+ .from(vulseCollections)
128
+ .where(eq(vulseCollections.handle, def.handle))
129
+ .get()
130
+ if (dup) {
131
+ throw new ValidationError(`handle '${def.handle}' already exists`, { issues: [{ path: ['handle'] }] })
132
+ }
133
+ await ensureValidCrossField(db, def, null)
134
+ return def
135
+ }
136
+
137
+ async function ensureValidCrossField(
138
+ db: VulseDb,
139
+ def: BlueprintDefinition | BlueprintDefinitionWithRenames,
140
+ selfHandle: string | null,
141
+ ): Promise<void> {
142
+ await ensureValidFieldList(db, def.fields, ['fields'], selfHandle ?? def.handle)
143
+ }
144
+
145
+ async function ensureValidFieldList(
146
+ db: VulseDb,
147
+ fields: Array<{ name: string; ui: FieldUi } | NestedFieldDefinition>,
148
+ path: Array<string | number>,
149
+ currentHandle: string,
150
+ ): Promise<void> {
151
+ const seen = new Set<string>()
152
+ for (let i = 0; i < fields.length; i++) {
153
+ const f = fields[i]!
154
+ if (seen.has(f.name)) {
155
+ throw new ValidationError(`duplicate field name '${f.name}'`, { issues: [{ path: [...path, i, 'name'] }] })
156
+ }
157
+ seen.add(f.name)
158
+ }
159
+
160
+ for (let i = 0; i < fields.length; i++) {
161
+ const f = fields[i]!
162
+ if (f.ui.kind === 'relationship' && 'to' in f.ui) {
163
+ if (f.ui.to === currentHandle) continue
164
+ const target = await db.select({ handle: vulseCollections.handle })
165
+ .from(vulseCollections)
166
+ .where(eq(vulseCollections.handle, f.ui.to))
167
+ .get()
168
+ if (!target) {
169
+ throw new ValidationError(`relationship target '${f.ui.to}' does not exist`, {
170
+ issues: [{ path: [...path, i, 'ui', 'to'] }],
171
+ })
172
+ }
173
+ }
174
+
175
+ if (f.ui.kind === 'replicator' && 'sets' in f.ui) {
176
+ const seenSets = new Set<string>()
177
+ for (let j = 0; j < f.ui.sets.length; j++) {
178
+ const set = f.ui.sets[j]!
179
+ if (seenSets.has(set.name)) {
180
+ throw new ValidationError(`duplicate set name '${set.name}'`, {
181
+ issues: [{ path: [...path, i, 'ui', 'sets', j, 'name'] }],
182
+ })
183
+ }
184
+ seenSets.add(set.name)
185
+ await ensureValidFieldList(db, set.fields, [...path, i, 'ui', 'sets', j, 'fields'], currentHandle)
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ function computeRenames(fields: FieldDefinitionWithRename[]): Array<[string, string]> {
192
+ const out: Array<[string, string]> = []
193
+ for (const f of fields) {
194
+ if (f.previousName !== undefined && f.previousName !== f.name) {
195
+ out.push([f.previousName, f.name])
196
+ }
197
+ }
198
+ return out
199
+ }
200
+
201
+ function stripRenames(def: BlueprintDefinitionWithRenames): BlueprintDefinition {
202
+ return {
203
+ handle: def.handle,
204
+ label: def.label,
205
+ singleton: def.singleton,
206
+ ...(def.tree !== undefined ? { tree: def.tree } : {}),
207
+ ...(def.maxDepth !== undefined ? { maxDepth: def.maxDepth } : {}),
208
+ ...(def.drafts !== undefined ? { drafts: def.drafts } : {}),
209
+ ...(def.seo !== undefined ? { seo: def.seo } : {}),
210
+ ...(def.seoMapping !== undefined ? { seoMapping: def.seoMapping } : {}),
211
+ ...(def.preview !== undefined ? { preview: def.preview } : {}),
212
+ fields: def.fields.map(({ previousName: _previousName, ...rest }) => rest),
213
+ }
214
+ }
215
+
216
+ async function loadDefinition(db: VulseDb, handle: string): Promise<BlueprintDefinition | null> {
217
+ const row = await db.select({ definition: vulseCollections.definition })
218
+ .from(vulseCollections)
219
+ .where(eq(vulseCollections.handle, handle))
220
+ .get()
221
+ if (!row) return null
222
+ return BlueprintDefinitionSchema.parse(row.definition)
223
+ }
224
+
225
+ function parseOrThrow<T>(
226
+ schema: {
227
+ safeParse: (x: unknown) => { success: true; data: T } | { success: false; error: { issues: unknown[] } }
228
+ },
229
+ value: unknown,
230
+ ): T {
231
+ const result = schema.safeParse(value)
232
+ if (!result.success) {
233
+ throw new ValidationError('Validation failed', { issues: result.error.issues })
234
+ }
235
+ return result.data
236
+ }
@@ -0,0 +1,33 @@
1
+ import type { PreviewDefinition } from './definition.js'
2
+ import type { PreviewConfig } from './types.js'
3
+
4
+ /** Normalize preview config for Blueprint types (`exactOptionalPropertyTypes`). */
5
+ export function toPreviewConfig(preview: PreviewDefinition | PreviewConfig): PreviewConfig {
6
+ return {
7
+ path: preview.path,
8
+ ...(preview.rootSelector !== undefined ? { rootSelector: preview.rootSelector } : {}),
9
+ ...(preview.live !== undefined ? { live: preview.live } : {}),
10
+ }
11
+ }
12
+
13
+ /** Default public URL template for a collection's entry pages. */
14
+ export function defaultPreviewPath(collectionHandle: string): string {
15
+ if (!collectionHandle || collectionHandle === 'page') return '/{slug}'
16
+ return `/${collectionHandle}/{slug}`
17
+ }
18
+
19
+ export function resolvePreviewPath(bp: { name: string; preview?: PreviewConfig | null }): string {
20
+ return bp.preview?.path ?? defaultPreviewPath(bp.name)
21
+ }
22
+
23
+ export function resolvePreviewConfig(bp: {
24
+ name: string
25
+ preview?: PreviewConfig | null
26
+ }): PreviewConfig {
27
+ const path = resolvePreviewPath(bp)
28
+ return {
29
+ path,
30
+ ...(bp.preview?.rootSelector ? { rootSelector: bp.preview.rootSelector } : {}),
31
+ ...(bp.preview?.live === false ? { live: false } : {}),
32
+ }
33
+ }