create-vexcms 0.0.20 → 0.1.0-alpha.2

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 (153) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +93 -83
  3. package/dist/index.js +246 -81
  4. package/dist/index.mjs +30976 -0
  5. package/package.json +35 -15
  6. package/templates/base-nextjs/README.md +53 -205
  7. package/templates/base-nextjs/_env.example +7 -9
  8. package/templates/base-nextjs/_gitignore +41 -1
  9. package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
  10. package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
  11. package/templates/base-nextjs/convex/_generated/server.js +8 -0
  12. package/templates/base-nextjs/convex/auth/api.ts +49 -3
  13. package/templates/base-nextjs/convex/auth/db.ts +42 -300
  14. package/templates/base-nextjs/convex/auth/index.ts +11 -7
  15. package/templates/base-nextjs/convex/auth/options.ts +5 -29
  16. package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
  17. package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
  18. package/templates/base-nextjs/convex/http.ts +18 -3
  19. package/templates/base-nextjs/convex/schema.ts +104 -2
  20. package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
  21. package/templates/base-nextjs/convex/vex/globals.ts +19 -82
  22. package/templates/base-nextjs/convex/vex/media.ts +19 -246
  23. package/templates/base-nextjs/convex/vex.schema.ts +138 -0
  24. package/templates/base-nextjs/convex/vex.ts +19 -0
  25. package/templates/base-nextjs/convex.json +6 -0
  26. package/templates/base-nextjs/eslint.config.mjs +1 -21
  27. package/templates/base-nextjs/next.config.ts +10 -0
  28. package/templates/base-nextjs/package.json +47 -48
  29. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
  30. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
  31. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
  32. package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
  33. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
  34. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
  35. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
  36. package/templates/base-nextjs/src/app/globals.css +240 -104
  37. package/templates/base-nextjs/src/app/layout.tsx +23 -13
  38. package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
  39. package/templates/base-nextjs/src/auth/access.ts +46 -0
  40. package/templates/base-nextjs/src/auth/client.tsx +17 -5
  41. package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
  42. package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
  43. package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
  44. package/templates/base-nextjs/src/components/component-example.tsx +56 -464
  45. package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
  46. package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
  47. package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
  48. package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
  49. package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
  50. package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
  51. package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
  52. package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
  53. package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
  54. package/templates/base-nextjs/src/db/constants/index.ts +22 -35
  55. package/templates/base-nextjs/src/lib/utils.ts +0 -12
  56. package/templates/base-nextjs/src/proxy.ts +97 -12
  57. package/templates/base-nextjs/src/vex.config.ts +35 -0
  58. package/templates/base-nextjs/src/vex.types.ts +293 -0
  59. package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
  60. package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
  61. package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
  62. package/templates/base-nextjs/tsconfig.json +3 -0
  63. package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
  64. package/templates/marketing-site/convex/_generated/api.js +23 -0
  65. package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
  66. package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
  67. package/templates/marketing-site/convex/_generated/server.js +101 -0
  68. package/templates/marketing-site/convex/footers.ts +11 -7
  69. package/templates/marketing-site/convex/headers.ts +15 -5
  70. package/templates/marketing-site/convex/pages.ts +20 -35
  71. package/templates/marketing-site/convex/schema.ts +119 -0
  72. package/templates/marketing-site/convex/seed.ts +582 -35
  73. package/templates/marketing-site/convex/siteSettings.ts +19 -5
  74. package/templates/marketing-site/convex/theme.ts +55 -61
  75. package/templates/marketing-site/convex/vex.schema.ts +355 -0
  76. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
  77. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
  78. package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
  79. package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
  80. package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
  81. package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
  82. package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
  83. package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
  84. package/templates/marketing-site/src/lib/metadata.ts +51 -72
  85. package/templates/marketing-site/src/vex.config.ts +35 -0
  86. package/templates/marketing-site/src/vex.types.ts +634 -0
  87. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  88. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  89. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  90. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  91. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  92. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  93. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  94. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  95. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  96. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  97. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  98. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  99. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
  100. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  101. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  102. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  103. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  104. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  105. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  106. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  107. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  108. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  109. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  110. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  111. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  112. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  113. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  114. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  115. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  116. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  117. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  118. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  119. package/templates/base-nextjs/convex/vex/collections.ts +0 -333
  120. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  121. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  122. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  123. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  124. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  125. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  126. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  127. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  128. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  129. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  130. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  131. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  132. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  133. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  134. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  135. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  136. package/templates/base-nextjs/src/db/types.ts +0 -6
  137. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  138. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  139. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  140. package/templates/base-nextjs/vex.config.ts +0 -25
  141. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  142. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  143. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  144. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  145. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  146. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  147. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  148. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  149. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  150. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  151. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  152. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  153. package/templates/marketing-site/vex.config.ts +0 -32
@@ -1,648 +0,0 @@
1
- import type { DataModel } from "@convex/_generated/dataModel"
2
- import type { GenericQueryCtx, TableNamesInDataModel } from "convex/server"
3
-
4
- import { mutation, query } from "@convex/_generated/server"
5
- import {
6
- hasPermission,
7
- extractUserFields,
8
- DEFAULT_MAX_VERSIONS_PER_DOC,
9
- } from "@vexcms/core"
10
- import { ConvexError, v } from "convex/values"
11
-
12
- import { TABLE_SLUG_USERS } from "~/db/constants"
13
-
14
- import config from "../../vex.config"
15
- import { access } from "../../src/vexcms/access"
16
- import * as Versions from "./model/versions"
17
-
18
- function requireVersionedCollection(slug: string) {
19
- const collections = [...config.collections, ...(config.media?.collections ?? []), ...(config.globals ?? [])]
20
- const match = collections.find((c: any) => c.slug === slug)
21
- if (!match) {
22
- throw new ConvexError(`Collection not found: ${slug}`)
23
- }
24
- if (!match.versions?.drafts) {
25
- throw new ConvexError(`Collection "${slug}" does not have versioning enabled`)
26
- }
27
- return match
28
- }
29
-
30
- async function getUser(ctx: GenericQueryCtx<DataModel>) {
31
- const identity = await ctx.auth.getUserIdentity()
32
- if (!identity?.email) return null
33
- const user = await ctx.db
34
- .query(TABLE_SLUG_USERS)
35
- .withIndex("by_email", (q) => q.eq("email", identity.email!))
36
- .first()
37
- if (!user) return null
38
- return {
39
- user: user as Record<string, unknown>,
40
- roles: (user.role as string[]) ?? [],
41
- email: identity.email,
42
- }
43
- }
44
-
45
- async function requireUser(ctx: GenericQueryCtx<DataModel>) {
46
- const result = await getUser(ctx)
47
- if (!result) throw new ConvexError("Not authenticated")
48
- return result
49
- }
50
-
51
- function checkPermission(props: Parameters<typeof hasPermission>[0]) {
52
- const result = hasPermission(props)
53
- const denied =
54
- result === false ||
55
- (typeof result === "object" && !Object.values(result).some(Boolean))
56
- if (denied) {
57
- throw new ConvexError(`Access denied: "${props.action}" on "${props.resource}"`)
58
- }
59
- return result
60
- }
61
-
62
- /**
63
- * Creates a new document in a versioned collection.
64
- * The document starts with vex_status: "draft" and an initial version is created.
65
- */
66
- export const createDraftDocument = mutation({
67
- args: {
68
- collectionSlug: v.string(),
69
- fields: v.any(),
70
- },
71
- handler: async (ctx, { collectionSlug, fields }) => {
72
- const collection = requireVersionedCollection(collectionSlug)
73
- const { user, roles, email } = await requireUser(ctx)
74
-
75
- checkPermission({
76
- access,
77
- user,
78
- userRoles: roles,
79
- resource: collectionSlug,
80
- action: "create",
81
- data: fields as Record<string, unknown>,
82
- })
83
-
84
- const validatedFields = fields as Record<string, unknown>
85
-
86
- const tableName = (collection.tableName ?? collectionSlug) as TableNamesInDataModel<DataModel>
87
- const newDocId = await (ctx.db as any).insert(tableName, {
88
- ...validatedFields,
89
- vex_status: "draft",
90
- vex_version: 1,
91
- vex_publishedAt: undefined,
92
- })
93
-
94
- await Versions.createVersion<DataModel>({
95
- ctx,
96
- collection: collectionSlug,
97
- documentId: newDocId as string,
98
- snapshot: validatedFields,
99
- status: "draft",
100
- createdBy: email,
101
- })
102
-
103
- return { documentId: newDocId as string, version: 1 }
104
- },
105
- })
106
-
107
- /**
108
- * Saves a draft version without publishing.
109
- * Creates a new version record in vex_versions.
110
- * Does NOT update the main document's content fields.
111
- */
112
- export const saveDraft = mutation({
113
- args: {
114
- collectionSlug: v.string(),
115
- documentId: v.string(),
116
- fields: v.any(),
117
- restoredFrom: v.optional(v.number()),
118
- },
119
- handler: async (ctx, { collectionSlug, documentId, fields, restoredFrom }) => {
120
- const collection = requireVersionedCollection(collectionSlug)
121
- const { user, roles, email } = await requireUser(ctx)
122
-
123
- checkPermission({
124
- access,
125
- user,
126
- userRoles: roles,
127
- resource: collectionSlug,
128
- action: "update",
129
- data: fields as Record<string, unknown>,
130
- })
131
-
132
- const doc = await ctx.db.get(documentId as any)
133
- if (!doc) throw new ConvexError("Document not found")
134
-
135
- // If no versions exist yet, snapshot the current main doc as v1 published
136
- // (preserves the original state before any edits)
137
- const latestVersion = await Versions.getLatestVersion<DataModel>({
138
- ctx,
139
- collection: collectionSlug,
140
- documentId,
141
- })
142
- if (!latestVersion) {
143
- const originalSnapshot = extractUserFields({
144
- document: doc as Record<string, unknown>,
145
- })
146
- await Versions.createVersion<DataModel>({
147
- ctx,
148
- collection: collectionSlug,
149
- documentId,
150
- snapshot: originalSnapshot,
151
- status: "published",
152
- createdBy: null,
153
- })
154
- }
155
-
156
- // The frontend sends ALL field values as a complete snapshot
157
- const snapshot = fields as Record<string, unknown>
158
-
159
- const { version } = await Versions.createVersion<DataModel>({
160
- ctx,
161
- collection: collectionSlug,
162
- documentId,
163
- snapshot,
164
- status: "draft",
165
- createdBy: email,
166
- restoredFrom,
167
- })
168
-
169
- await Versions.cleanupOldVersions<DataModel>({
170
- ctx,
171
- collection: collectionSlug,
172
- documentId,
173
- maxPerDoc: collection.versions?.maxPerDoc ?? DEFAULT_MAX_VERSIONS_PER_DOC,
174
- })
175
-
176
- return { version }
177
- },
178
- })
179
-
180
- /**
181
- * Publishes the latest draft version.
182
- * Copies the latest version's snapshot to the main document's fields,
183
- * sets vex_status to "published", and creates a published version record.
184
- */
185
- export const publish = mutation({
186
- args: {
187
- collectionSlug: v.string(),
188
- documentId: v.string(),
189
- fields: v.optional(v.any()),
190
- },
191
- handler: async (ctx, { collectionSlug, documentId, fields }) => {
192
- const collection = requireVersionedCollection(collectionSlug)
193
- const { user, roles, email } = await requireUser(ctx)
194
-
195
- checkPermission({
196
- access,
197
- user,
198
- userRoles: roles,
199
- resource: collectionSlug,
200
- action: "update",
201
- })
202
-
203
- const doc = await ctx.db.get(documentId as any)
204
- if (!doc) throw new ConvexError("Document not found")
205
-
206
- // Check if this document has any version history yet
207
- const latestVersion = await Versions.getLatestVersion<DataModel>({
208
- ctx,
209
- collection: collectionSlug,
210
- documentId,
211
- })
212
-
213
- // If no versions exist, snapshot the current main doc as v1 published
214
- // (preserves the original state before any edits)
215
- if (!latestVersion) {
216
- const originalSnapshot = extractUserFields({
217
- document: doc as Record<string, unknown>,
218
- })
219
- await Versions.createVersion<DataModel>({
220
- ctx,
221
- collection: collectionSlug,
222
- documentId,
223
- snapshot: originalSnapshot,
224
- status: "published",
225
- createdBy: null,
226
- })
227
- }
228
-
229
- let snapshot: Record<string, unknown>
230
-
231
- if (fields) {
232
- snapshot = fields as Record<string, unknown>
233
- } else if (latestVersion) {
234
- snapshot = latestVersion.snapshot as Record<string, unknown>
235
- } else {
236
- // No fields provided and no prior versions — republish current doc as-is
237
- snapshot = extractUserFields({
238
- document: doc as Record<string, unknown>,
239
- })
240
- }
241
-
242
- // If the latest version is already published with the same snapshot,
243
- // don't create a duplicate — just ensure the main doc is up to date
244
- if (
245
- latestVersion &&
246
- (latestVersion as any).status === "published" &&
247
- JSON.stringify((latestVersion as any).snapshot) === JSON.stringify(snapshot)
248
- ) {
249
- const existingVersion = (latestVersion as any).version as number
250
- // Ensure main doc reflects published state
251
- if ((doc as any).vex_status !== "published") {
252
- await (ctx.db as any).patch(documentId as any, {
253
- vex_status: "published",
254
- vex_publishedAt: Date.now(),
255
- vex_version: existingVersion,
256
- })
257
- }
258
- return { version: existingVersion }
259
- }
260
-
261
- // If the latest version is a draft with the same snapshot, promote it
262
- // to published instead of creating a new version entry
263
- if (
264
- latestVersion &&
265
- (latestVersion as any).status === "draft" &&
266
- JSON.stringify((latestVersion as any).snapshot) === JSON.stringify(snapshot)
267
- ) {
268
- const existingVersion = (latestVersion as any).version as number
269
- await (ctx.db as any).patch((latestVersion as any)._id, {
270
- status: "published",
271
- createdAt: Date.now(),
272
- })
273
- await (ctx.db as any).patch(documentId as any, {
274
- ...snapshot,
275
- vex_status: "published",
276
- vex_publishedAt: Date.now(),
277
- vex_version: existingVersion,
278
- })
279
- return { version: existingVersion }
280
- }
281
-
282
- // Create a new published version record
283
- const { version } = await Versions.createVersion<DataModel>({
284
- ctx,
285
- collection: collectionSlug,
286
- documentId,
287
- snapshot,
288
- status: "published",
289
- createdBy: email,
290
- })
291
-
292
- // Update the main document
293
- await (ctx.db as any).patch(documentId as any, {
294
- ...snapshot,
295
- vex_status: "published",
296
- vex_publishedAt: Date.now(),
297
- vex_version: version,
298
- })
299
-
300
- await Versions.cleanupOldVersions<DataModel>({
301
- ctx,
302
- collection: collectionSlug,
303
- documentId,
304
- maxPerDoc: collection.versions?.maxPerDoc ?? DEFAULT_MAX_VERSIONS_PER_DOC,
305
- })
306
-
307
- return { version }
308
- },
309
- })
310
-
311
- /**
312
- * Unpublishes a document by setting vex_status back to "draft".
313
- */
314
- export const unpublish = mutation({
315
- args: {
316
- collectionSlug: v.string(),
317
- documentId: v.string(),
318
- },
319
- handler: async (ctx, { collectionSlug, documentId }) => {
320
- requireVersionedCollection(collectionSlug)
321
- const { user, roles, email } = await requireUser(ctx)
322
-
323
- checkPermission({
324
- access,
325
- user,
326
- userRoles: roles,
327
- resource: collectionSlug,
328
- action: "update",
329
- })
330
-
331
- const doc = await ctx.db.get(documentId as any)
332
- if (!doc) throw new ConvexError("Document not found")
333
-
334
- if ((doc as any).vex_status !== "published") {
335
- throw new ConvexError("Document is not published")
336
- }
337
-
338
- // Patch the main document status back to draft
339
- await (ctx.db as any).patch(documentId as any, {
340
- vex_status: "draft",
341
- })
342
-
343
- // Patch the latest published version back to draft instead of creating a new one
344
- const latestVersion = await Versions.getLatestVersion<DataModel>({
345
- ctx,
346
- collection: collectionSlug,
347
- documentId,
348
- })
349
-
350
- if (latestVersion && (latestVersion as any).status === "published") {
351
- await (ctx.db as any).patch((latestVersion as any)._id, {
352
- status: "draft",
353
- })
354
- }
355
-
356
- return { version: (latestVersion as any)?.version ?? 0 }
357
- },
358
- })
359
-
360
- /**
361
- * Autosave — coalesces into a single autosave version record.
362
- */
363
- export const autosave = mutation({
364
- args: {
365
- collectionSlug: v.string(),
366
- documentId: v.string(),
367
- fields: v.any(),
368
- },
369
- handler: async (ctx, { collectionSlug, documentId, fields }) => {
370
- requireVersionedCollection(collectionSlug)
371
- const { user, roles, email } = await requireUser(ctx)
372
-
373
- checkPermission({
374
- access,
375
- user,
376
- userRoles: roles,
377
- resource: collectionSlug,
378
- action: "update",
379
- })
380
-
381
- const doc = await ctx.db.get(documentId as any)
382
- if (!doc) throw new ConvexError("Document not found")
383
-
384
- const validatedFields = fields as Record<string, unknown>
385
-
386
- // Get latest version to merge snapshot
387
- const latestVersion = await Versions.getLatestVersion<DataModel>({
388
- ctx,
389
- collection: collectionSlug,
390
- documentId,
391
- })
392
-
393
- const mergedSnapshot = {
394
- ...((latestVersion?.snapshot as Record<string, unknown>) ?? {}),
395
- ...validatedFields,
396
- }
397
-
398
- const { version } = await Versions.coalesceAutosave<DataModel>({
399
- ctx,
400
- collection: collectionSlug,
401
- documentId,
402
- snapshot: mergedSnapshot,
403
- createdBy: email,
404
- })
405
-
406
- return { version }
407
- },
408
- })
409
-
410
- /**
411
- * Gets a version's snapshot for restoring into the edit form.
412
- * Does NOT create a new version — the user must explicitly save
413
- * after reviewing the restored values in the form.
414
- */
415
- export const getVersionSnapshot = query({
416
- args: {
417
- collectionSlug: v.string(),
418
- documentId: v.string(),
419
- version: v.number(),
420
- },
421
- handler: async (ctx, { collectionSlug, documentId, version: versionNum }) => {
422
- requireVersionedCollection(collectionSlug)
423
-
424
- const targetVersion = await Versions.getVersion<DataModel>({
425
- ctx,
426
- collection: collectionSlug,
427
- documentId,
428
- version: versionNum,
429
- })
430
- if (!targetVersion) throw new ConvexError("Version not found")
431
-
432
- return {
433
- version: targetVersion.version as number,
434
- snapshot: targetVersion.snapshot as Record<string, unknown>,
435
- }
436
- },
437
- })
438
-
439
- /**
440
- * Permanently deletes a specific version record.
441
- */
442
- export const deleteVersion = mutation({
443
- args: {
444
- collectionSlug: v.string(),
445
- documentId: v.string(),
446
- version: v.number(),
447
- },
448
- handler: async (ctx, { collectionSlug, documentId, version: versionNum }) => {
449
- requireVersionedCollection(collectionSlug)
450
- const { user, roles } = await requireUser(ctx)
451
-
452
- checkPermission({
453
- access,
454
- user,
455
- userRoles: roles,
456
- resource: collectionSlug,
457
- action: "update",
458
- })
459
-
460
- const targetVersion = await Versions.getVersion<DataModel>({
461
- ctx,
462
- collection: collectionSlug,
463
- documentId,
464
- version: versionNum,
465
- })
466
- if (!targetVersion) throw new ConvexError("Version not found")
467
-
468
- await (ctx.db as any).delete(targetVersion._id)
469
-
470
- return { deleted: versionNum }
471
- },
472
- })
473
-
474
- /**
475
- * Lists version history for a document.
476
- */
477
- export const listVersions = query({
478
- args: {
479
- collectionSlug: v.string(),
480
- documentId: v.string(),
481
- limit: v.optional(v.number()),
482
- },
483
- handler: async (ctx, { collectionSlug, documentId, limit }) => {
484
- requireVersionedCollection(collectionSlug)
485
-
486
- return await Versions.listVersions<DataModel>({
487
- ctx,
488
- collection: collectionSlug,
489
- documentId,
490
- limit,
491
- })
492
- },
493
- })
494
-
495
- /**
496
- * Gets the document content for the admin edit view.
497
- * Returns the latest version's snapshot if versions exist,
498
- * otherwise falls back to the main document's fields.
499
- */
500
- export const getDocumentForEdit = query({
501
- args: {
502
- collectionSlug: v.string(),
503
- documentId: v.string(),
504
- },
505
- handler: async (ctx, { collectionSlug, documentId }) => {
506
- const mainDoc = await ctx.db.get(documentId as any)
507
- if (!mainDoc) return null
508
-
509
- // Check if this collection/global actually has versioning enabled in config.
510
- // If not (e.g. config not yet redeployed), just return the raw document.
511
- const collections = [...config.collections, ...(config.media?.collections ?? []), ...(config.globals ?? [])]
512
- const match = collections.find((c: any) => c.slug === collectionSlug)
513
- if (!match?.versions?.drafts) {
514
- const doc = mainDoc as Record<string, unknown>
515
- if (!doc.vex_status) doc.vex_status = "published"
516
- return doc
517
- }
518
-
519
- const latestVersion = await Versions.getLatestVersion<DataModel>({
520
- ctx,
521
- collection: collectionSlug,
522
- documentId,
523
- })
524
-
525
- if (latestVersion) {
526
- return {
527
- _id: mainDoc._id,
528
- ...(latestVersion.snapshot as Record<string, unknown>),
529
- vex_status: (latestVersion as any).status ?? (mainDoc as any).vex_status ?? "draft",
530
- vex_version: latestVersion.version,
531
- vex_publishedAt: (mainDoc as any).vex_publishedAt,
532
- _creationTime: mainDoc._creationTime,
533
- }
534
- }
535
-
536
- // Fallback: return main doc as-is (e.g., versioning just enabled on existing collection)
537
- // Pre-existing documents without vex_status are treated as published
538
- const doc = mainDoc as Record<string, unknown>
539
- if (!doc.vex_status) {
540
- doc.vex_status = "published"
541
- }
542
- return doc
543
- },
544
- })
545
-
546
- /**
547
- * Creates an initial version entry for a document that has none.
548
- * Called by the admin edit view when it detects a versioned document
549
- * without any version records (e.g., versioning was just enabled on
550
- * an existing collection).
551
- *
552
- * Preserves the document's current status — if vex_status is "published",
553
- * the version is created as published. Otherwise defaults to "draft".
554
- *
555
- * No-ops if the document already has version entries.
556
- */
557
- export const initializeVersion = mutation({
558
- args: {
559
- collectionSlug: v.string(),
560
- documentId: v.string(),
561
- },
562
- handler: async (ctx, { collectionSlug, documentId }) => {
563
- const mainDoc = await ctx.db.get(documentId as any)
564
- if (!mainDoc) return null
565
-
566
- // Check if versions already exist
567
- const existing = await Versions.getLatestVersion<DataModel>({
568
- ctx,
569
- collection: collectionSlug,
570
- documentId,
571
- })
572
- if (existing) return existing
573
-
574
- // Build snapshot from the main document's current fields
575
- const { _id, _creationTime, ...snapshot } = mainDoc as Record<string, unknown>
576
-
577
- // Determine status from the document's current state
578
- const status = (snapshot.vex_status as string) === "published" ? "published" : "draft"
579
-
580
- // Ensure the main doc has vex_status set
581
- if (!snapshot.vex_status) {
582
- await ctx.db.patch(documentId as any, { vex_status: "published" } as any)
583
- snapshot.vex_status = "published"
584
- }
585
-
586
- // Create the initial version
587
- const versionId = await Versions.createVersion<DataModel>({
588
- ctx,
589
- collection: collectionSlug,
590
- documentId,
591
- snapshot,
592
- status: status as "draft" | "published",
593
- createdBy: null,
594
- })
595
-
596
- return versionId
597
- },
598
- })
599
-
600
- /**
601
- * Backfills vex_status on existing documents in a versioned collection
602
- * that don't have the field yet (e.g. versioning was just enabled on
603
- * a collection with existing data, or VEX was added to an existing project).
604
- *
605
- * Only sets vex_status to "published" — does NOT create version records.
606
- * Version records are created lazily on first saveDraft or publish, which
607
- * avoids doubling the collection data in one go.
608
- *
609
- * Called by the VEX CLI in a paginated loop (cursor-based) so it can
610
- * handle production tables with thousands of documents without hitting
611
- * Convex mutation limits.
612
- *
613
- * Safe to run repeatedly — skips documents that already have vex_status.
614
- */
615
- export const backfillVersionStatus = mutation({
616
- args: {
617
- collectionSlug: v.string(),
618
- cursor: v.optional(v.string()),
619
- batchSize: v.optional(v.number()),
620
- },
621
- handler: async (ctx, { collectionSlug, cursor, batchSize = 100 }) => {
622
- const collection = requireVersionedCollection(collectionSlug)
623
-
624
- // No auth check — this is a system migration called by the VEX CLI,
625
- // not a user-facing action. The ConvexHttpClient has no auth session.
626
-
627
- const tableName = (collection.tableName ?? collectionSlug) as TableNamesInDataModel<DataModel>
628
- const results = await (ctx.db as any)
629
- .query(tableName)
630
- .paginate({ cursor: cursor ?? null, numItems: batchSize })
631
-
632
- let patched = 0
633
- for (const doc of results.page) {
634
- if (!doc.vex_status) {
635
- await (ctx.db as any).patch(doc._id, {
636
- vex_status: "published",
637
- })
638
- patched++
639
- }
640
- }
641
-
642
- return {
643
- patched,
644
- isDone: results.isDone,
645
- cursor: results.continueCursor,
646
- }
647
- },
648
- })
@@ -1,33 +0,0 @@
1
- "use client"
2
-
3
- import { AdminLayout } from "@vexcms/admin-next"
4
- import type { PermissionUser } from "@vexcms/admin-next"
5
- import type { ClientVexConfig } from "@vexcms/core"
6
- import { access } from "~/vexcms/access"
7
-
8
- /**
9
- * Client wrapper that provides the access config directly (not through RSC serialization).
10
- * The access config contains callback functions that can't be serialized through RSC.
11
- */
12
- export function AdminLayoutWrapper({
13
- config,
14
- user,
15
- permissionUser,
16
- children,
17
- }: {
18
- config: ClientVexConfig
19
- user?: { name: string; email: string; avatar?: string }
20
- permissionUser?: PermissionUser
21
- children: React.ReactNode
22
- }) {
23
- return (
24
- <AdminLayout
25
- config={config}
26
- user={user}
27
- permissionUser={permissionUser}
28
- access={access}
29
- >
30
- {children}
31
- </AdminLayout>
32
- )
33
- }
@@ -1,38 +0,0 @@
1
- "use client"
2
-
3
- import { AdminPage } from "@vexcms/admin-next"
4
- import type { ClientVexConfig } from "@vexcms/core"
5
- import { extractLivePreviewConfigs, sanitizeConfigForClient } from "@vexcms/core"
6
- import { RichTextFieldWithMedia } from "./RichTextFieldWithMedia"
7
- import originalConfig from "~/../vex.config"
8
-
9
- const livePreviewConfigs = extractLivePreviewConfigs(originalConfig)
10
- // Sanitize on the client so component references in admin.components survive
11
- // (they can't cross the RSC serialization boundary)
12
- const clientConfig = sanitizeConfigForClient(originalConfig)
13
-
14
- export function AdminPageWrapper({
15
- config: _serverConfig,
16
- path,
17
- }: {
18
- config: ClientVexConfig
19
- path?: string[]
20
- }) {
21
- return (
22
- <AdminPage
23
- config={clientConfig}
24
- path={path}
25
- livePreviewConfigs={livePreviewConfigs}
26
- renderRichTextField={(props: any) => (
27
- <RichTextFieldWithMedia
28
- field={props.field}
29
- fieldDef={props.fieldDef}
30
- name={props.name}
31
- generateUploadUrl={props.generateUploadUrl}
32
- createMediaDocument={props.createMediaDocument}
33
- onUploadNew={props.onUploadNew}
34
- />
35
- )}
36
- />
37
- )
38
- }