@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.
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +3 -4
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +11 -10
- package/dist/core/blueprints/compile.d.ts +1 -1
- package/dist/core/blueprints/compile.d.ts.map +1 -1
- package/dist/core/blueprints/compile.js +3 -3
- package/dist/core/blueprints/mutations.js +2 -2
- package/dist/core/forms/rate-limit.d.ts +1 -1
- package/dist/core/forms/rate-limit.d.ts.map +1 -1
- package/dist/core/forms/rate-limit.js +3 -3
- package/dist/core/forms/unique.d.ts +1 -1
- package/dist/core/forms/unique.d.ts.map +1 -1
- package/dist/core/forms/unique.js +4 -4
- package/dist/core/globals/compile.d.ts +1 -1
- package/dist/core/globals/compile.d.ts.map +1 -1
- package/dist/core/globals/compile.js +2 -2
- package/dist/core/globals/definition.d.ts +1 -1
- package/dist/core/globals/definition.d.ts.map +1 -1
- package/dist/core/globals/definition.js +3 -3
- package/dist/core/repos/globals.js +3 -3
- package/dist/core/sha256.d.ts +3 -0
- package/dist/core/sha256.d.ts.map +1 -0
- package/dist/core/sha256.js +9 -0
- package/dist/integration/index.js +1 -1
- package/dist/integration/install-hook.d.ts.map +1 -1
- package/dist/integration/install-hook.js +6 -4
- package/dist/integration/wrangler-config.d.ts +12 -0
- package/dist/integration/wrangler-config.d.ts.map +1 -0
- package/dist/integration/wrangler-config.js +97 -0
- package/dist/integration/wrangler-patch.d.ts +1 -0
- package/dist/integration/wrangler-patch.d.ts.map +1 -1
- package/dist/integration/wrangler-patch.js +2 -1
- package/dist/server/routes/form-submit.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -3
- package/src/admin/assets/logo-mark.svg +5 -0
- package/src/admin/client/active-locale.ts +17 -0
- package/src/admin/client/api.ts +21 -0
- package/src/admin/client/form-from-zod.ts +7 -0
- package/src/admin/client/live-preview-enabled.ts +5 -0
- package/src/admin/components/AdminShell.astro +45 -0
- package/src/admin/components/AuthSettings.vue +60 -0
- package/src/admin/components/BlockEditor.vue +53 -0
- package/src/admin/components/BlueprintEditor.vue +1783 -0
- package/src/admin/components/CollectionKindIcon.vue +26 -0
- package/src/admin/components/CollectionTree.vue +220 -0
- package/src/admin/components/EntryEditorWithPreview.vue +130 -0
- package/src/admin/components/EntryForm.vue +411 -0
- package/src/admin/components/EntryList.vue +121 -0
- package/src/admin/components/EntryStatusBadge.vue +24 -0
- package/src/admin/components/FormEditor.vue +233 -0
- package/src/admin/components/FormList.vue +54 -0
- package/src/admin/components/GlobalSetEditor.vue +272 -0
- package/src/admin/components/GlobalSetList.vue +55 -0
- package/src/admin/components/LivePreviewPanel.vue +171 -0
- package/src/admin/components/LoginForm.vue +53 -0
- package/src/admin/components/MediaLibrary.vue +106 -0
- package/src/admin/components/MediaPicker.vue +49 -0
- package/src/admin/components/RevisionDiff.vue +11 -0
- package/src/admin/components/RevisionList.vue +134 -0
- package/src/admin/components/SeoFields.vue +113 -0
- package/src/admin/components/SetEditor.vue +137 -0
- package/src/admin/components/SetList.vue +32 -0
- package/src/admin/components/SettingsForm.vue +189 -0
- package/src/admin/components/SideNav.vue +152 -0
- package/src/admin/components/SubmissionDetail.vue +45 -0
- package/src/admin/components/SubmissionList.vue +89 -0
- package/src/admin/components/ToastHost.vue +33 -0
- package/src/admin/components/TreeRow.vue +163 -0
- package/src/admin/components/UserEditor.vue +186 -0
- package/src/admin/components/UserList.vue +46 -0
- package/src/admin/components/blocks/BlockItem.vue +32 -0
- package/src/admin/components/blocks/BlockToolbar.vue +12 -0
- package/src/admin/components/blocks/edit/CodeEdit.vue +18 -0
- package/src/admin/components/blocks/edit/EmbedEdit.vue +14 -0
- package/src/admin/components/blocks/edit/HeadingEdit.vue +19 -0
- package/src/admin/components/blocks/edit/ImageEdit.vue +40 -0
- package/src/admin/components/blocks/edit/ListEdit.vue +36 -0
- package/src/admin/components/blocks/edit/ParagraphEdit.vue +14 -0
- package/src/admin/components/blocks/edit/QuoteEdit.vue +18 -0
- package/src/admin/components/fields/BlocksField.vue +123 -0
- package/src/admin/components/fields/BlocksSetsPicker.vue +59 -0
- package/src/admin/components/fields/BoolField.vue +10 -0
- package/src/admin/components/fields/DateField.vue +22 -0
- package/src/admin/components/fields/EntriesField.vue +153 -0
- package/src/admin/components/fields/EntryField.vue +138 -0
- package/src/admin/components/fields/EnumField.vue +81 -0
- package/src/admin/components/fields/FieldRenderer.vue +87 -0
- package/src/admin/components/fields/GridField.vue +173 -0
- package/src/admin/components/fields/LinkField.vue +219 -0
- package/src/admin/components/fields/MediaField.vue +69 -0
- package/src/admin/components/fields/NumberField.vue +12 -0
- package/src/admin/components/fields/ObjectField.vue +18 -0
- package/src/admin/components/fields/RefField.vue +170 -0
- package/src/admin/components/fields/RepeaterField.vue +27 -0
- package/src/admin/components/fields/ReplicatorField.vue +121 -0
- package/src/admin/components/fields/TextField.vue +11 -0
- package/src/admin/components/fields/TextareaField.vue +11 -0
- package/src/admin/components/fields/VulseAccordionGroupNodeView.vue +82 -0
- package/src/admin/components/fields/VulseAccordionNodeView.vue +128 -0
- package/src/admin/components/fields/VulseCalloutNodeView.vue +81 -0
- package/src/admin/components/fields/VulseIframeNodeView.vue +112 -0
- package/src/admin/components/fields/VulseSetNodeView.vue +68 -0
- package/src/admin/components/fields/VulseVideoNodeView.vue +104 -0
- package/src/admin/components/fields/blocks-editor-extensions.ts +26 -0
- package/src/admin/components/fields/emoji-extension.ts +54 -0
- package/src/admin/components/fields/link-extension.ts +48 -0
- package/src/admin/components/fields/set-node-utils.ts +115 -0
- package/src/admin/components/fields/url-utils.ts +85 -0
- package/src/admin/components/fields/vulse-accordion-extension.ts +64 -0
- package/src/admin/components/fields/vulse-accordion-group-extension.ts +49 -0
- package/src/admin/components/fields/vulse-callout-extension.ts +53 -0
- package/src/admin/components/fields/vulse-iframe-extension.ts +96 -0
- package/src/admin/components/fields/vulse-set-extension.ts +66 -0
- package/src/admin/components/fields/vulse-video-extension.ts +65 -0
- package/src/admin/composables/toast.ts +35 -0
- package/src/admin/composables/useEntrySearch.ts +112 -0
- package/src/admin/composables/useSets.ts +31 -0
- package/src/admin/pages/collections/[name]/[id]/revisions.astro +27 -0
- package/src/admin/pages/collections/[name]/[id].astro +90 -0
- package/src/admin/pages/collections/[name]/index.astro +31 -0
- package/src/admin/pages/collections/[name]/new.astro +38 -0
- package/src/admin/pages/forms/[handle]/submissions/[id].astro +9 -0
- package/src/admin/pages/forms/[handle]/submissions/index.astro +9 -0
- package/src/admin/pages/forms/[handle].astro +9 -0
- package/src/admin/pages/forms/index.astro +7 -0
- package/src/admin/pages/forms/new.astro +7 -0
- package/src/admin/pages/index.astro +36 -0
- package/src/admin/pages/login.astro +14 -0
- package/src/admin/pages/media.astro +8 -0
- package/src/admin/pages/schema/[handle].astro +10 -0
- package/src/admin/pages/schema/new.astro +9 -0
- package/src/admin/pages/settings/auth.astro +9 -0
- package/src/admin/pages/settings/globals/[handle].astro +9 -0
- package/src/admin/pages/settings/globals/index.astro +7 -0
- package/src/admin/pages/settings/globals/new.astro +7 -0
- package/src/admin/pages/settings/index.astro +8 -0
- package/src/admin/pages/settings/sets/[handle].astro +9 -0
- package/src/admin/pages/settings/sets/index.astro +7 -0
- package/src/admin/pages/settings/sets/new.astro +7 -0
- package/src/admin/pages/users/[id].astro +10 -0
- package/src/admin/pages/users/index.astro +8 -0
- package/src/admin/styles/admin.css +166 -0
- package/src/core/access.ts +9 -0
- package/src/core/blocks/schema.ts +66 -0
- package/src/core/blueprints/code-to-definition.ts +156 -0
- package/src/core/blueprints/compile.ts +176 -0
- package/src/core/blueprints/define.ts +12 -0
- package/src/core/blueprints/definition.ts +185 -0
- package/src/core/blueprints/load.ts +144 -0
- package/src/core/blueprints/mutations.ts +236 -0
- package/src/core/blueprints/preview-path.ts +33 -0
- package/src/core/blueprints/reflect-fields.ts +305 -0
- package/src/core/blueprints/registry.ts +14 -0
- package/src/core/blueprints/seed.ts +20 -0
- package/src/core/blueprints/select-helpers.ts +30 -0
- package/src/core/blueprints/seo.ts +180 -0
- package/src/core/blueprints/types.ts +59 -0
- package/src/core/blueprints/zod-helpers.ts +86 -0
- package/src/core/db.ts +11 -0
- package/src/core/errors.ts +34 -0
- package/src/core/forms/compile.ts +84 -0
- package/src/core/forms/definition.ts +102 -0
- package/src/core/forms/rate-limit.ts +52 -0
- package/src/core/forms/unique.ts +38 -0
- package/src/core/globals/compile.ts +35 -0
- package/src/core/globals/definition.ts +27 -0
- package/src/core/locales.ts +45 -0
- package/src/core/migrations.ts +48 -0
- package/src/core/parse-content.ts +85 -0
- package/src/core/plugins/definition.ts +150 -0
- package/src/core/preview-content.ts +21 -0
- package/src/core/repos/entries.ts +504 -0
- package/src/core/repos/forms.ts +270 -0
- package/src/core/repos/globals.ts +179 -0
- package/src/core/repos/media.ts +106 -0
- package/src/core/repos/preview-sessions.ts +108 -0
- package/src/core/repos/revisions.ts +60 -0
- package/src/core/repos/settings.ts +23 -0
- package/src/core/schema.ts +244 -0
- package/src/core/sets/compile.ts +12 -0
- package/src/core/sets/definition.ts +10 -0
- package/src/core/sets/service.ts +82 -0
- package/src/core/sets/validate-tree.ts +57 -0
- package/src/core/sha256.ts +10 -0
- package/src/core/slug.ts +30 -0
- package/src/scaffold/collection-write.ts +83 -0
- package/src/scaffold/collection.ts +277 -0
- package/src/server/assets/live-preview-bridge.content.ts +2 -0
- package/src/server/assets/live-preview-bridge.js +535 -0
- package/src/server/better-auth.ts +82 -0
- package/src/server/cf-images.ts +34 -0
- package/src/server/cron.ts +37 -0
- package/src/server/email.ts +17 -0
- package/src/server/endpoints/api-auth.ts +10 -0
- package/src/server/endpoints/api-vulse-blueprints.ts +23 -0
- package/src/server/endpoints/api-vulse-entries-locales.ts +12 -0
- package/src/server/endpoints/api-vulse-entries-move.ts +7 -0
- package/src/server/endpoints/api-vulse-entries-publish.ts +7 -0
- package/src/server/endpoints/api-vulse-entries-tree.ts +7 -0
- package/src/server/endpoints/api-vulse-entries.ts +23 -0
- package/src/server/endpoints/api-vulse-form-handle.ts +30 -0
- package/src/server/endpoints/api-vulse-form-public.ts +7 -0
- package/src/server/endpoints/api-vulse-form-submit.ts +7 -0
- package/src/server/endpoints/api-vulse-form-upload.ts +7 -0
- package/src/server/endpoints/api-vulse-forms.ts +12 -0
- package/src/server/endpoints/api-vulse-globals-handle.ts +20 -0
- package/src/server/endpoints/api-vulse-globals-public-handle.ts +7 -0
- package/src/server/endpoints/api-vulse-globals-public.ts +7 -0
- package/src/server/endpoints/api-vulse-globals-value.ts +8 -0
- package/src/server/endpoints/api-vulse-globals.ts +12 -0
- package/src/server/endpoints/api-vulse-media-file.ts +7 -0
- package/src/server/endpoints/api-vulse-media-id.ts +12 -0
- package/src/server/endpoints/api-vulse-media.ts +12 -0
- package/src/server/endpoints/api-vulse-preview-bridge.ts +11 -0
- package/src/server/endpoints/api-vulse-preview-sessions-id.ts +10 -0
- package/src/server/endpoints/api-vulse-preview-sessions.ts +7 -0
- package/src/server/endpoints/api-vulse-preview-start.ts +7 -0
- package/src/server/endpoints/api-vulse-preview-stop.ts +7 -0
- package/src/server/endpoints/api-vulse-revisions-restore.ts +7 -0
- package/src/server/endpoints/api-vulse-revisions.ts +7 -0
- package/src/server/endpoints/api-vulse-search.ts +7 -0
- package/src/server/endpoints/api-vulse-sets.ts +23 -0
- package/src/server/endpoints/api-vulse-settings.ts +12 -0
- package/src/server/endpoints/api-vulse-users-id.ts +12 -0
- package/src/server/endpoints/api-vulse-users-reset-password.ts +7 -0
- package/src/server/endpoints/api-vulse-users-role.ts +9 -0
- package/src/server/endpoints/api-vulse-users.ts +7 -0
- package/src/server/endpoints/with-runtime.ts +11 -0
- package/src/server/env.ts +23 -0
- package/src/server/envelope.ts +21 -0
- package/src/server/forms/email.ts +11 -0
- package/src/server/forms/process-submission.ts +95 -0
- package/src/server/forms/queue.ts +25 -0
- package/src/server/forms/templates.ts +24 -0
- package/src/server/forms/webhook.ts +19 -0
- package/src/server/handler.ts +66 -0
- package/src/server/image-probe.ts +35 -0
- package/src/server/loader.ts +54 -0
- package/src/server/plugins.ts +214 -0
- package/src/server/preview.ts +25 -0
- package/src/server/r2.ts +13 -0
- package/src/server/routes/blueprints.ts +62 -0
- package/src/server/routes/entries.ts +255 -0
- package/src/server/routes/form-submit.ts +168 -0
- package/src/server/routes/form-upload.ts +100 -0
- package/src/server/routes/forms.ts +88 -0
- package/src/server/routes/globals-public.ts +30 -0
- package/src/server/routes/globals.ts +93 -0
- package/src/server/routes/media.ts +145 -0
- package/src/server/routes/preview-sessions.ts +76 -0
- package/src/server/routes/preview.ts +36 -0
- package/src/server/routes/revisions.ts +29 -0
- package/src/server/routes/search.ts +31 -0
- package/src/server/routes/sets.ts +40 -0
- package/src/server/routes/settings.ts +24 -0
- package/src/server/routes/users.ts +127 -0
- package/src/server/runtime.ts +99 -0
- package/src/server/sdk/collections.ts +98 -0
- package/src/server/sdk/index.ts +25 -0
- package/src/server/sdk/media.ts +11 -0
- 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
|
+
}
|