@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,270 @@
|
|
|
1
|
+
import { eq, and, gt, lt, sql, desc } from 'drizzle-orm'
|
|
2
|
+
import { nanoid } from 'nanoid'
|
|
3
|
+
import type { VulseDb } from '../db.js'
|
|
4
|
+
import {
|
|
5
|
+
vulseForms,
|
|
6
|
+
vulseFormSubmissions,
|
|
7
|
+
vulseFormUniqueValues,
|
|
8
|
+
vulseFormUploadDrafts,
|
|
9
|
+
} from '../schema.js'
|
|
10
|
+
import { NotFoundError, ValidationError } from '../errors.js'
|
|
11
|
+
import {
|
|
12
|
+
type FormDefinition,
|
|
13
|
+
FormDefinitionSchema,
|
|
14
|
+
} from '../forms/definition.js'
|
|
15
|
+
|
|
16
|
+
export interface FormRow {
|
|
17
|
+
handle: string
|
|
18
|
+
label: string
|
|
19
|
+
definition: FormDefinition
|
|
20
|
+
enabled: boolean
|
|
21
|
+
createdAt: Date
|
|
22
|
+
updatedAt: Date
|
|
23
|
+
submissionCount?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseRow(row: typeof vulseForms.$inferSelect): FormRow {
|
|
27
|
+
return {
|
|
28
|
+
handle: row.handle,
|
|
29
|
+
label: row.label,
|
|
30
|
+
definition: FormDefinitionSchema.parse(row.definition),
|
|
31
|
+
enabled: row.enabled,
|
|
32
|
+
createdAt: row.createdAt,
|
|
33
|
+
updatedAt: row.updatedAt,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class FormsRepo {
|
|
38
|
+
constructor(private db: VulseDb) {}
|
|
39
|
+
|
|
40
|
+
async create(input: FormDefinition): Promise<FormRow> {
|
|
41
|
+
const parsed = FormDefinitionSchema.safeParse(input)
|
|
42
|
+
if (!parsed.success) throw new ValidationError('Invalid form', { issues: parsed.error.issues })
|
|
43
|
+
const def = parsed.data
|
|
44
|
+
const now = new Date()
|
|
45
|
+
await this.db.insert(vulseForms).values({
|
|
46
|
+
handle: def.handle,
|
|
47
|
+
label: def.label,
|
|
48
|
+
definition: def,
|
|
49
|
+
enabled: def.settings.enabled,
|
|
50
|
+
createdAt: now,
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
})
|
|
53
|
+
const row = await this.findByHandle(def.handle)
|
|
54
|
+
if (!row) throw new Error(`form not found after create: ${def.handle}`)
|
|
55
|
+
return row
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async findByHandle(handle: string): Promise<FormRow | null> {
|
|
59
|
+
const row = await this.db.select().from(vulseForms).where(eq(vulseForms.handle, handle)).get()
|
|
60
|
+
return row ? parseRow(row) : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async list(): Promise<FormRow[]> {
|
|
64
|
+
const rows = await this.db
|
|
65
|
+
.select({
|
|
66
|
+
handle: vulseForms.handle,
|
|
67
|
+
label: vulseForms.label,
|
|
68
|
+
definition: vulseForms.definition,
|
|
69
|
+
enabled: vulseForms.enabled,
|
|
70
|
+
createdAt: vulseForms.createdAt,
|
|
71
|
+
updatedAt: vulseForms.updatedAt,
|
|
72
|
+
submissionCount: sql<number>`(
|
|
73
|
+
SELECT COUNT(*) FROM vulse_form_submissions
|
|
74
|
+
WHERE form_handle = ${vulseForms.handle}
|
|
75
|
+
)`.mapWith(Number),
|
|
76
|
+
})
|
|
77
|
+
.from(vulseForms)
|
|
78
|
+
.orderBy(vulseForms.createdAt)
|
|
79
|
+
return rows.map((r) => ({
|
|
80
|
+
...parseRow({
|
|
81
|
+
...r,
|
|
82
|
+
// The list query intentionally omits schemaVersion; supply the default
|
|
83
|
+
// so parseRow's type-cast remains accurate without an over-broad `as`.
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
} as typeof vulseForms.$inferSelect),
|
|
86
|
+
submissionCount: r.submissionCount,
|
|
87
|
+
}))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async update(handle: string, input: FormDefinition): Promise<FormRow> {
|
|
91
|
+
if (input.handle !== handle) {
|
|
92
|
+
throw new Error(`form handle is immutable (got '${input.handle}', expected '${handle}')`)
|
|
93
|
+
}
|
|
94
|
+
const parsed = FormDefinitionSchema.safeParse(input)
|
|
95
|
+
if (!parsed.success) throw new ValidationError('Invalid form', { issues: parsed.error.issues })
|
|
96
|
+
const def = parsed.data
|
|
97
|
+
await this.db.update(vulseForms).set({
|
|
98
|
+
label: def.label,
|
|
99
|
+
definition: def,
|
|
100
|
+
enabled: def.settings.enabled,
|
|
101
|
+
updatedAt: new Date(),
|
|
102
|
+
}).where(eq(vulseForms.handle, handle))
|
|
103
|
+
const row = await this.findByHandle(handle)
|
|
104
|
+
if (!row) throw new NotFoundError('form not found')
|
|
105
|
+
return row
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async delete(handle: string): Promise<void> {
|
|
109
|
+
await this.db.delete(vulseForms).where(eq(vulseForms.handle, handle))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface SubmissionMeta {
|
|
114
|
+
ip?: string
|
|
115
|
+
userAgent?: string
|
|
116
|
+
referer?: string
|
|
117
|
+
locale?: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface FileRef {
|
|
121
|
+
field: string
|
|
122
|
+
mediaId: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface SubmissionRow {
|
|
126
|
+
id: string
|
|
127
|
+
formHandle: string
|
|
128
|
+
payload: Record<string, unknown>
|
|
129
|
+
fileRefs: FileRef[]
|
|
130
|
+
meta: SubmissionMeta
|
|
131
|
+
status: 'received' | 'processed' | 'failed'
|
|
132
|
+
error: string | null
|
|
133
|
+
createdAt: Date
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseSubmission(row: typeof vulseFormSubmissions.$inferSelect): SubmissionRow {
|
|
137
|
+
return {
|
|
138
|
+
id: row.id,
|
|
139
|
+
formHandle: row.formHandle,
|
|
140
|
+
payload: row.payload as Record<string, unknown>,
|
|
141
|
+
fileRefs: (row.fileRefs ?? []) as FileRef[],
|
|
142
|
+
meta: row.meta as SubmissionMeta,
|
|
143
|
+
status: row.status,
|
|
144
|
+
error: row.error ?? null,
|
|
145
|
+
createdAt: row.createdAt,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class SubmissionsRepo {
|
|
150
|
+
constructor(private db: VulseDb) {}
|
|
151
|
+
|
|
152
|
+
async create(input: {
|
|
153
|
+
formHandle: string
|
|
154
|
+
payload: Record<string, unknown>
|
|
155
|
+
fileRefs?: FileRef[]
|
|
156
|
+
meta: SubmissionMeta
|
|
157
|
+
}): Promise<SubmissionRow> {
|
|
158
|
+
const now = new Date()
|
|
159
|
+
const row = {
|
|
160
|
+
id: nanoid(),
|
|
161
|
+
formHandle: input.formHandle,
|
|
162
|
+
payload: input.payload,
|
|
163
|
+
fileRefs: input.fileRefs ?? [],
|
|
164
|
+
meta: input.meta,
|
|
165
|
+
status: 'received' as const,
|
|
166
|
+
error: null,
|
|
167
|
+
createdAt: now,
|
|
168
|
+
}
|
|
169
|
+
await this.db.insert(vulseFormSubmissions).values(row)
|
|
170
|
+
return parseSubmission(row as typeof vulseFormSubmissions.$inferSelect)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async findById(id: string): Promise<SubmissionRow | null> {
|
|
174
|
+
const row = await this.db.select().from(vulseFormSubmissions).where(eq(vulseFormSubmissions.id, id)).get()
|
|
175
|
+
return row ? parseSubmission(row) : null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async list(opts: { formHandle: string; limit?: number; offset?: number }): Promise<SubmissionRow[]> {
|
|
179
|
+
let query = this.db.select().from(vulseFormSubmissions)
|
|
180
|
+
.where(eq(vulseFormSubmissions.formHandle, opts.formHandle))
|
|
181
|
+
.orderBy(desc(vulseFormSubmissions.createdAt))
|
|
182
|
+
if (opts.limit !== undefined) query = query.limit(opts.limit) as typeof query
|
|
183
|
+
if (opts.offset !== undefined) query = query.offset(opts.offset) as typeof query
|
|
184
|
+
const rows = await query
|
|
185
|
+
return rows.map(parseSubmission)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async delete(id: string): Promise<void> {
|
|
189
|
+
await this.db.delete(vulseFormUniqueValues).where(eq(vulseFormUniqueValues.submissionId, id))
|
|
190
|
+
await this.db.delete(vulseFormSubmissions).where(eq(vulseFormSubmissions.id, id))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async deleteMany(ids: string[]): Promise<number> {
|
|
194
|
+
if (ids.length === 0) return 0
|
|
195
|
+
for (const id of ids) {
|
|
196
|
+
await this.db.delete(vulseFormUniqueValues).where(eq(vulseFormUniqueValues.submissionId, id))
|
|
197
|
+
}
|
|
198
|
+
let deleted = 0
|
|
199
|
+
for (const id of ids) {
|
|
200
|
+
await this.db.delete(vulseFormSubmissions).where(eq(vulseFormSubmissions.id, id))
|
|
201
|
+
deleted++
|
|
202
|
+
}
|
|
203
|
+
return deleted
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async updateStatus(id: string, status: 'processed' | 'failed', error?: string | null): Promise<void> {
|
|
207
|
+
await this.db.update(vulseFormSubmissions).set({
|
|
208
|
+
status,
|
|
209
|
+
error: error ?? null,
|
|
210
|
+
}).where(eq(vulseFormSubmissions.id, id))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface UploadDraftRow {
|
|
215
|
+
id: string
|
|
216
|
+
formHandle: string
|
|
217
|
+
fieldName: string
|
|
218
|
+
mediaId: string
|
|
219
|
+
expiresAt: Date
|
|
220
|
+
createdAt: Date
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export class FormUploadDraftsRepo {
|
|
224
|
+
constructor(private db: VulseDb) {}
|
|
225
|
+
|
|
226
|
+
async create(input: {
|
|
227
|
+
formHandle: string
|
|
228
|
+
fieldName: string
|
|
229
|
+
mediaId: string
|
|
230
|
+
expiresAt: Date
|
|
231
|
+
}): Promise<UploadDraftRow> {
|
|
232
|
+
const now = new Date()
|
|
233
|
+
const row = {
|
|
234
|
+
id: nanoid(),
|
|
235
|
+
formHandle: input.formHandle,
|
|
236
|
+
fieldName: input.fieldName,
|
|
237
|
+
mediaId: input.mediaId,
|
|
238
|
+
expiresAt: input.expiresAt,
|
|
239
|
+
createdAt: now,
|
|
240
|
+
}
|
|
241
|
+
await this.db.insert(vulseFormUploadDrafts).values(row)
|
|
242
|
+
return row
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async findValid(formHandle: string, fieldName: string, mediaId: string): Promise<UploadDraftRow | null> {
|
|
246
|
+
const row = await this.db.select().from(vulseFormUploadDrafts)
|
|
247
|
+
.where(and(
|
|
248
|
+
eq(vulseFormUploadDrafts.formHandle, formHandle),
|
|
249
|
+
eq(vulseFormUploadDrafts.fieldName, fieldName),
|
|
250
|
+
eq(vulseFormUploadDrafts.mediaId, mediaId),
|
|
251
|
+
gt(vulseFormUploadDrafts.expiresAt, new Date()),
|
|
252
|
+
))
|
|
253
|
+
.get()
|
|
254
|
+
return row ?? null
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async delete(id: string): Promise<void> {
|
|
258
|
+
await this.db.delete(vulseFormUploadDrafts).where(eq(vulseFormUploadDrafts.id, id))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async listExpired(before: Date): Promise<UploadDraftRow[]> {
|
|
262
|
+
const rows = await this.db.select().from(vulseFormUploadDrafts)
|
|
263
|
+
.where(lt(vulseFormUploadDrafts.expiresAt, before))
|
|
264
|
+
return rows
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async attachToSubmission(draftId: string): Promise<void> {
|
|
268
|
+
await this.delete(draftId)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm'
|
|
2
|
+
import type { VulseDb } from '../db.js'
|
|
3
|
+
import { vulseGlobalSets, vulseGlobalValues } from '../schema.js'
|
|
4
|
+
import { ConflictError, NotFoundError, ValidationError } from '../errors.js'
|
|
5
|
+
import { loadCompiledSets } from '../sets/service.js'
|
|
6
|
+
import {
|
|
7
|
+
type GlobalSetDefinition,
|
|
8
|
+
GlobalSetDefinitionSchema,
|
|
9
|
+
hashGlobalSetDefinition,
|
|
10
|
+
} from '../globals/definition.js'
|
|
11
|
+
import { compileGlobalSet } from '../globals/compile.js'
|
|
12
|
+
|
|
13
|
+
export interface GlobalSetRow {
|
|
14
|
+
handle: string
|
|
15
|
+
label: string
|
|
16
|
+
definition: GlobalSetDefinition
|
|
17
|
+
blueprintHash: string
|
|
18
|
+
createdAt: Date
|
|
19
|
+
updatedAt: Date
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GlobalValueRow {
|
|
23
|
+
handle: string
|
|
24
|
+
content: Record<string, unknown>
|
|
25
|
+
createdAt: Date
|
|
26
|
+
updatedAt: Date
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type PublicGlobals = Record<string, Record<string, unknown>>
|
|
30
|
+
|
|
31
|
+
function parseSetRow(row: typeof vulseGlobalSets.$inferSelect): GlobalSetRow {
|
|
32
|
+
return {
|
|
33
|
+
handle: row.handle,
|
|
34
|
+
label: row.label,
|
|
35
|
+
definition: GlobalSetDefinitionSchema.parse(row.definition),
|
|
36
|
+
blueprintHash: row.blueprintHash,
|
|
37
|
+
createdAt: row.createdAt,
|
|
38
|
+
updatedAt: row.updatedAt,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseValueRow(row: typeof vulseGlobalValues.$inferSelect): GlobalValueRow {
|
|
43
|
+
return {
|
|
44
|
+
handle: row.handle,
|
|
45
|
+
content: (row.content ?? {}) as Record<string, unknown>,
|
|
46
|
+
createdAt: row.createdAt,
|
|
47
|
+
updatedAt: row.updatedAt,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class GlobalsRepo {
|
|
52
|
+
constructor(private db: VulseDb) {}
|
|
53
|
+
|
|
54
|
+
async listSets(): Promise<GlobalSetRow[]> {
|
|
55
|
+
const rows = await this.db.select().from(vulseGlobalSets).orderBy(vulseGlobalSets.createdAt)
|
|
56
|
+
return rows.map(parseSetRow)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async findSetByHandle(handle: string): Promise<GlobalSetRow | null> {
|
|
60
|
+
const row = await this.db.select().from(vulseGlobalSets).where(eq(vulseGlobalSets.handle, handle)).get()
|
|
61
|
+
return row ? parseSetRow(row) : null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async createSet(input: GlobalSetDefinition): Promise<GlobalSetRow> {
|
|
65
|
+
const parsed = GlobalSetDefinitionSchema.safeParse(input)
|
|
66
|
+
if (!parsed.success) throw new ValidationError('Invalid global set', { issues: parsed.error.issues })
|
|
67
|
+
const def = parsed.data
|
|
68
|
+
const existing = await this.findSetByHandle(def.handle)
|
|
69
|
+
if (existing) throw new ConflictError(`global set already exists: ${def.handle}`)
|
|
70
|
+
|
|
71
|
+
const now = new Date()
|
|
72
|
+
const hash = await hashGlobalSetDefinition(def)
|
|
73
|
+
await this.db.insert(vulseGlobalSets).values({
|
|
74
|
+
handle: def.handle,
|
|
75
|
+
label: def.label,
|
|
76
|
+
definition: def,
|
|
77
|
+
blueprintHash: hash,
|
|
78
|
+
createdAt: now,
|
|
79
|
+
updatedAt: now,
|
|
80
|
+
})
|
|
81
|
+
await this.db.insert(vulseGlobalValues).values({
|
|
82
|
+
handle: def.handle,
|
|
83
|
+
content: {},
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const row = await this.findSetByHandle(def.handle)
|
|
89
|
+
if (!row) throw new Error(`global set not found after create: ${def.handle}`)
|
|
90
|
+
return row
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async updateSet(handle: string, input: GlobalSetDefinition): Promise<GlobalSetRow> {
|
|
94
|
+
if (input.handle !== handle) {
|
|
95
|
+
throw new ValidationError('Global set handles are immutable', { issues: [{ path: ['handle'], message: 'Handle cannot be changed' }] })
|
|
96
|
+
}
|
|
97
|
+
const parsed = GlobalSetDefinitionSchema.safeParse(input)
|
|
98
|
+
if (!parsed.success) throw new ValidationError('Invalid global set', { issues: parsed.error.issues })
|
|
99
|
+
const def = parsed.data
|
|
100
|
+
const existing = await this.findSetByHandle(handle)
|
|
101
|
+
if (!existing) throw new NotFoundError('global set not found')
|
|
102
|
+
|
|
103
|
+
await this.db.update(vulseGlobalSets).set({
|
|
104
|
+
label: def.label,
|
|
105
|
+
definition: def,
|
|
106
|
+
blueprintHash: await hashGlobalSetDefinition(def),
|
|
107
|
+
updatedAt: new Date(),
|
|
108
|
+
}).where(eq(vulseGlobalSets.handle, handle))
|
|
109
|
+
|
|
110
|
+
const row = await this.findSetByHandle(handle)
|
|
111
|
+
if (!row) throw new NotFoundError('global set not found')
|
|
112
|
+
return row
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async deleteSet(handle: string): Promise<void> {
|
|
116
|
+
const existing = await this.findSetByHandle(handle)
|
|
117
|
+
if (!existing) throw new NotFoundError('global set not found')
|
|
118
|
+
await this.db.delete(vulseGlobalSets).where(eq(vulseGlobalSets.handle, handle))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getValue(handle: string): Promise<GlobalValueRow | null> {
|
|
122
|
+
const set = await this.findSetByHandle(handle)
|
|
123
|
+
if (!set) return null
|
|
124
|
+
const row = await this.db.select().from(vulseGlobalValues).where(eq(vulseGlobalValues.handle, handle)).get()
|
|
125
|
+
return row ? parseValueRow(row) : null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async updateValue(handle: string, input: unknown): Promise<GlobalValueRow> {
|
|
129
|
+
const set = await this.findSetByHandle(handle)
|
|
130
|
+
if (!set) throw new NotFoundError('global set not found')
|
|
131
|
+
|
|
132
|
+
const sets = await loadCompiledSets(this.db)
|
|
133
|
+
const compiled = await compileGlobalSet(set.definition, sets)
|
|
134
|
+
const result = compiled.schema.safeParse(input)
|
|
135
|
+
if (!result.success) {
|
|
136
|
+
throw new ValidationError('Invalid global value', { issues: result.error.issues })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const now = new Date()
|
|
140
|
+
const content = result.data as Record<string, unknown>
|
|
141
|
+
await this.db.insert(vulseGlobalValues).values({
|
|
142
|
+
handle,
|
|
143
|
+
content,
|
|
144
|
+
createdAt: now,
|
|
145
|
+
updatedAt: now,
|
|
146
|
+
}).onConflictDoUpdate({
|
|
147
|
+
target: vulseGlobalValues.handle,
|
|
148
|
+
set: { content, updatedAt: now },
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const row = await this.getValue(handle)
|
|
152
|
+
if (!row) throw new Error(`global value not found after update: ${handle}`)
|
|
153
|
+
return row
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async publicValues(): Promise<PublicGlobals> {
|
|
157
|
+
const rows = await this.db
|
|
158
|
+
.select({
|
|
159
|
+
handle: vulseGlobalValues.handle,
|
|
160
|
+
content: vulseGlobalValues.content,
|
|
161
|
+
})
|
|
162
|
+
.from(vulseGlobalValues)
|
|
163
|
+
.innerJoin(vulseGlobalSets, eq(vulseGlobalValues.handle, vulseGlobalSets.handle))
|
|
164
|
+
.orderBy(vulseGlobalSets.createdAt)
|
|
165
|
+
|
|
166
|
+
const out: PublicGlobals = {}
|
|
167
|
+
for (const row of rows) {
|
|
168
|
+
out[row.handle] = (row.content ?? {}) as Record<string, unknown>
|
|
169
|
+
}
|
|
170
|
+
return out
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async publicValue(handle: string): Promise<Record<string, unknown> | null> {
|
|
174
|
+
const set = await this.findSetByHandle(handle)
|
|
175
|
+
if (!set) return null
|
|
176
|
+
const value = await this.getValue(handle)
|
|
177
|
+
return value?.content ?? {}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { and, desc, eq, isNotNull, isNull, lt } from 'drizzle-orm'
|
|
2
|
+
import { nanoid } from 'nanoid'
|
|
3
|
+
import type { VulseDb } from '../db.js'
|
|
4
|
+
import { media } from '../schema.js'
|
|
5
|
+
import { NotFoundError } from '../errors.js'
|
|
6
|
+
|
|
7
|
+
export interface MediaRow {
|
|
8
|
+
id: string
|
|
9
|
+
r2Key: string
|
|
10
|
+
mime: string
|
|
11
|
+
size: number
|
|
12
|
+
width: number | null
|
|
13
|
+
height: number | null
|
|
14
|
+
alt: string | null
|
|
15
|
+
blurhash: string | null
|
|
16
|
+
uploadedBy: string | null
|
|
17
|
+
createdAt: Date
|
|
18
|
+
deletedAt: Date | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function rowToMedia(row: typeof media.$inferSelect): MediaRow {
|
|
22
|
+
return {
|
|
23
|
+
id: row.id,
|
|
24
|
+
r2Key: row.r2Key,
|
|
25
|
+
mime: row.mime,
|
|
26
|
+
size: row.size,
|
|
27
|
+
width: row.width ?? null,
|
|
28
|
+
height: row.height ?? null,
|
|
29
|
+
alt: row.alt ?? null,
|
|
30
|
+
blurhash: row.blurhash ?? null,
|
|
31
|
+
uploadedBy: row.uploadedBy ?? null,
|
|
32
|
+
createdAt: row.createdAt,
|
|
33
|
+
deletedAt: row.deletedAt ?? null,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class MediaRepo {
|
|
38
|
+
constructor(private db: VulseDb) {}
|
|
39
|
+
|
|
40
|
+
async create(input: {
|
|
41
|
+
r2Key: string
|
|
42
|
+
mime: string
|
|
43
|
+
size: number
|
|
44
|
+
uploadedBy?: string | null
|
|
45
|
+
width?: number | null
|
|
46
|
+
height?: number | null
|
|
47
|
+
alt?: string | null
|
|
48
|
+
blurhash?: string | null
|
|
49
|
+
}): Promise<MediaRow> {
|
|
50
|
+
const now = new Date()
|
|
51
|
+
const row = {
|
|
52
|
+
id: nanoid(),
|
|
53
|
+
r2Key: input.r2Key,
|
|
54
|
+
mime: input.mime,
|
|
55
|
+
size: input.size,
|
|
56
|
+
width: input.width ?? null,
|
|
57
|
+
height: input.height ?? null,
|
|
58
|
+
alt: input.alt ?? null,
|
|
59
|
+
blurhash: input.blurhash ?? null,
|
|
60
|
+
uploadedBy: input.uploadedBy ?? null,
|
|
61
|
+
createdAt: now,
|
|
62
|
+
deletedAt: null,
|
|
63
|
+
}
|
|
64
|
+
await this.db.insert(media).values(row)
|
|
65
|
+
return rowToMedia(row as typeof media.$inferSelect)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async list(opts: { includeDeleted?: boolean; limit?: number; offset?: number } = {}): Promise<MediaRow[]> {
|
|
69
|
+
const conditions = opts.includeDeleted ? undefined : isNull(media.deletedAt)
|
|
70
|
+
let query = this.db.select().from(media)
|
|
71
|
+
if (conditions) query = query.where(conditions) as typeof query
|
|
72
|
+
query = query.orderBy(desc(media.createdAt)) as typeof query
|
|
73
|
+
if (opts.limit !== undefined) query = query.limit(opts.limit) as typeof query
|
|
74
|
+
if (opts.offset !== undefined) query = query.offset(opts.offset) as typeof query
|
|
75
|
+
const rows = await query
|
|
76
|
+
return rows.map(rowToMedia)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async findById(id: string): Promise<MediaRow | null> {
|
|
80
|
+
const [row] = await this.db.select().from(media).where(eq(media.id, id))
|
|
81
|
+
return row ? rowToMedia(row) : null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async softDelete(id: string): Promise<void> {
|
|
85
|
+
const existing = await this.findById(id)
|
|
86
|
+
if (!existing) throw new NotFoundError(`Media ${id} not found`)
|
|
87
|
+
await this.db.update(media).set({ deletedAt: new Date() }).where(eq(media.id, id))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async updateAlt(id: string, alt: string): Promise<void> {
|
|
91
|
+
const existing = await this.findById(id)
|
|
92
|
+
if (!existing) throw new NotFoundError(`Media ${id} not found`)
|
|
93
|
+
await this.db.update(media).set({ alt }).where(eq(media.id, id))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async listPurgeable(days: number): Promise<MediaRow[]> {
|
|
97
|
+
const cutoff = new Date(Date.now() - days * 86_400_000)
|
|
98
|
+
const rows = await this.db.select().from(media)
|
|
99
|
+
.where(and(isNotNull(media.deletedAt), lt(media.deletedAt, cutoff)))
|
|
100
|
+
return rows.map(rowToMedia)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async hardDelete(id: string): Promise<void> {
|
|
104
|
+
await this.db.delete(media).where(eq(media.id, id))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import { eq, lt } from 'drizzle-orm'
|
|
3
|
+
import type { VulseDb } from '../db.js'
|
|
4
|
+
import { vulsePreviewSessions } from '../schema.js'
|
|
5
|
+
import { DEFAULT_LOCALE } from './entries.js'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000
|
|
8
|
+
|
|
9
|
+
export interface PreviewSessionRow {
|
|
10
|
+
id: string
|
|
11
|
+
userId: string
|
|
12
|
+
entryId: string | null
|
|
13
|
+
collection: string
|
|
14
|
+
locale: string
|
|
15
|
+
slug: string
|
|
16
|
+
content: unknown
|
|
17
|
+
expiresAt: Date
|
|
18
|
+
createdAt: Date
|
|
19
|
+
updatedAt: Date
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mapRow(row: typeof vulsePreviewSessions.$inferSelect): PreviewSessionRow {
|
|
23
|
+
return {
|
|
24
|
+
id: row.id,
|
|
25
|
+
userId: row.userId,
|
|
26
|
+
entryId: row.entryId ?? null,
|
|
27
|
+
collection: row.collection,
|
|
28
|
+
locale: row.locale,
|
|
29
|
+
slug: row.slug,
|
|
30
|
+
content: row.content,
|
|
31
|
+
expiresAt: row.expiresAt,
|
|
32
|
+
createdAt: row.createdAt,
|
|
33
|
+
updatedAt: row.updatedAt,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class PreviewSessionsRepo {
|
|
38
|
+
constructor(private db: VulseDb) {}
|
|
39
|
+
|
|
40
|
+
async create(input: {
|
|
41
|
+
userId: string
|
|
42
|
+
collection: string
|
|
43
|
+
slug: string
|
|
44
|
+
content: unknown
|
|
45
|
+
locale?: string
|
|
46
|
+
entryId?: string | null
|
|
47
|
+
ttlMs?: number
|
|
48
|
+
}): Promise<PreviewSessionRow> {
|
|
49
|
+
const now = new Date()
|
|
50
|
+
const ttl = input.ttlMs ?? DEFAULT_TTL_MS
|
|
51
|
+
const id = nanoid(32)
|
|
52
|
+
const row = {
|
|
53
|
+
id,
|
|
54
|
+
userId: input.userId,
|
|
55
|
+
entryId: input.entryId ?? null,
|
|
56
|
+
collection: input.collection,
|
|
57
|
+
locale: input.locale ?? DEFAULT_LOCALE,
|
|
58
|
+
slug: input.slug,
|
|
59
|
+
content: input.content,
|
|
60
|
+
expiresAt: new Date(now.getTime() + ttl),
|
|
61
|
+
createdAt: now,
|
|
62
|
+
updatedAt: now,
|
|
63
|
+
}
|
|
64
|
+
await this.db.insert(vulsePreviewSessions).values(row)
|
|
65
|
+
return mapRow(row)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async findById(id: string): Promise<PreviewSessionRow | null> {
|
|
69
|
+
const row = await this.db.query.vulsePreviewSessions.findFirst({
|
|
70
|
+
where: eq(vulsePreviewSessions.id, id),
|
|
71
|
+
})
|
|
72
|
+
if (!row) return null
|
|
73
|
+
if (row.expiresAt.getTime() < Date.now()) return null
|
|
74
|
+
return mapRow(row)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async update(id: string, userId: string, patch: { slug?: string; content?: unknown; locale?: string }): Promise<PreviewSessionRow | null> {
|
|
78
|
+
const existing = await this.findById(id)
|
|
79
|
+
if (!existing || existing.userId !== userId) return null
|
|
80
|
+
const now = new Date()
|
|
81
|
+
const next = {
|
|
82
|
+
slug: patch.slug ?? existing.slug,
|
|
83
|
+
content: patch.content ?? existing.content,
|
|
84
|
+
locale: patch.locale ?? existing.locale,
|
|
85
|
+
expiresAt: new Date(now.getTime() + DEFAULT_TTL_MS),
|
|
86
|
+
updatedAt: now,
|
|
87
|
+
}
|
|
88
|
+
await this.db.update(vulsePreviewSessions).set(next).where(eq(vulsePreviewSessions.id, id))
|
|
89
|
+
return { ...existing, ...next }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async delete(id: string, userId: string): Promise<boolean> {
|
|
93
|
+
const existing = await this.findById(id)
|
|
94
|
+
if (!existing || existing.userId !== userId) return false
|
|
95
|
+
await this.db.delete(vulsePreviewSessions).where(eq(vulsePreviewSessions.id, id))
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async purgeExpired(now = new Date()): Promise<number> {
|
|
100
|
+
const expired = await this.db.select({ id: vulsePreviewSessions.id })
|
|
101
|
+
.from(vulsePreviewSessions)
|
|
102
|
+
.where(lt(vulsePreviewSessions.expiresAt, now))
|
|
103
|
+
for (const row of expired) {
|
|
104
|
+
await this.db.delete(vulsePreviewSessions).where(eq(vulsePreviewSessions.id, row.id))
|
|
105
|
+
}
|
|
106
|
+
return expired.length
|
|
107
|
+
}
|
|
108
|
+
}
|