create-vexcms 0.0.3

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 (116) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +2409 -0
  3. package/package.json +37 -0
  4. package/templates/base-nextjs/.prettierignore +26 -0
  5. package/templates/base-nextjs/.prettierrc +7 -0
  6. package/templates/base-nextjs/README.md +256 -0
  7. package/templates/base-nextjs/_gitignore +2 -0
  8. package/templates/base-nextjs/components.json +24 -0
  9. package/templates/base-nextjs/convex/_generated/api.d.ts +125 -0
  10. package/templates/base-nextjs/convex/_generated/api.js +23 -0
  11. package/templates/base-nextjs/convex/_generated/dataModel.d.ts +60 -0
  12. package/templates/base-nextjs/convex/_generated/server.d.ts +143 -0
  13. package/templates/base-nextjs/convex/_generated/server.js +93 -0
  14. package/templates/base-nextjs/convex/auth/adapter/index.ts +230 -0
  15. package/templates/base-nextjs/convex/auth/adapter/utils.ts +547 -0
  16. package/templates/base-nextjs/convex/auth/api.ts +8 -0
  17. package/templates/base-nextjs/convex/auth/config.ts +10 -0
  18. package/templates/base-nextjs/convex/auth/db.ts +303 -0
  19. package/templates/base-nextjs/convex/auth/index.ts +14 -0
  20. package/templates/base-nextjs/convex/auth/options.ts +63 -0
  21. package/templates/base-nextjs/convex/auth/plugins/index.ts +20 -0
  22. package/templates/base-nextjs/convex/auth/sessions.ts +60 -0
  23. package/templates/base-nextjs/convex/auth.config.ts +7 -0
  24. package/templates/base-nextjs/convex/convex.config.ts +5 -0
  25. package/templates/base-nextjs/convex/http.ts +28 -0
  26. package/templates/base-nextjs/convex/schema.ts +3 -0
  27. package/templates/base-nextjs/convex/vex/auth.ts +38 -0
  28. package/templates/base-nextjs/convex/vex/collections.ts +321 -0
  29. package/templates/base-nextjs/convex/vex/firstUser.ts +134 -0
  30. package/templates/base-nextjs/convex/vex/helpers.ts +33 -0
  31. package/templates/base-nextjs/convex/vex/impersonation.ts +51 -0
  32. package/templates/base-nextjs/convex/vex/media.ts +246 -0
  33. package/templates/base-nextjs/convex/vex/migrate.ts +74 -0
  34. package/templates/base-nextjs/convex/vex/model/collections.ts +196 -0
  35. package/templates/base-nextjs/convex/vex/model/media.ts +87 -0
  36. package/templates/base-nextjs/convex/vex/model/versions.ts +254 -0
  37. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +53 -0
  38. package/templates/base-nextjs/convex/vex/versions.ts +588 -0
  39. package/templates/base-nextjs/eslint.config.mjs +164 -0
  40. package/templates/base-nextjs/next.config.ts +10 -0
  41. package/templates/base-nextjs/package.json +67 -0
  42. package/templates/base-nextjs/postcss.config.mjs +7 -0
  43. package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
  44. package/templates/base-nextjs/public/file.svg +1 -0
  45. package/templates/base-nextjs/public/globe.svg +1 -0
  46. package/templates/base-nextjs/public/next.svg +1 -0
  47. package/templates/base-nextjs/public/vercel.svg +1 -0
  48. package/templates/base-nextjs/public/window.svg +1 -0
  49. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/page.tsx +9 -0
  50. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +23 -0
  51. package/templates/base-nextjs/src/app/(frontend)/@auth/default.tsx +3 -0
  52. package/templates/base-nextjs/src/app/(frontend)/@auth/page.tsx +3 -0
  53. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/page.tsx +9 -0
  54. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +11 -0
  55. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +14 -0
  56. package/templates/base-nextjs/src/app/(frontend)/page.tsx +116 -0
  57. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +33 -0
  58. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +38 -0
  59. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +101 -0
  60. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +15 -0
  61. package/templates/base-nextjs/src/app/admin/layout.tsx +33 -0
  62. package/templates/base-nextjs/src/app/api/auth/[...all]/route.ts +6 -0
  63. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  64. package/templates/base-nextjs/src/app/globals.css +130 -0
  65. package/templates/base-nextjs/src/app/layout.tsx +30 -0
  66. package/templates/base-nextjs/src/auth/client.tsx +41 -0
  67. package/templates/base-nextjs/src/auth/permissions.ts +102 -0
  68. package/templates/base-nextjs/src/auth/server.ts +16 -0
  69. package/templates/base-nextjs/src/auth/serverUtils.ts +64 -0
  70. package/templates/base-nextjs/src/auth/types.ts +3 -0
  71. package/templates/base-nextjs/src/components/component-example.tsx +474 -0
  72. package/templates/base-nextjs/src/components/example.tsx +52 -0
  73. package/templates/base-nextjs/src/components/providers/client.tsx +9 -0
  74. package/templates/base-nextjs/src/components/providers/convex.tsx +32 -0
  75. package/templates/base-nextjs/src/components/providers/server.tsx +15 -0
  76. package/templates/base-nextjs/src/components/providers/theme.tsx +11 -0
  77. package/templates/base-nextjs/src/components/ui/alert-dialog.tsx +162 -0
  78. package/templates/base-nextjs/src/components/ui/badge.tsx +52 -0
  79. package/templates/base-nextjs/src/components/ui/button.tsx +60 -0
  80. package/templates/base-nextjs/src/components/ui/card.tsx +92 -0
  81. package/templates/base-nextjs/src/components/ui/combobox.tsx +271 -0
  82. package/templates/base-nextjs/src/components/ui/dialog.tsx +135 -0
  83. package/templates/base-nextjs/src/components/ui/dropdown-menu.tsx +246 -0
  84. package/templates/base-nextjs/src/components/ui/field.tsx +224 -0
  85. package/templates/base-nextjs/src/components/ui/input-group.tsx +146 -0
  86. package/templates/base-nextjs/src/components/ui/input.tsx +20 -0
  87. package/templates/base-nextjs/src/components/ui/label.tsx +20 -0
  88. package/templates/base-nextjs/src/components/ui/select.tsx +189 -0
  89. package/templates/base-nextjs/src/components/ui/separator.tsx +21 -0
  90. package/templates/base-nextjs/src/components/ui/textarea.tsx +18 -0
  91. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +67 -0
  92. package/templates/base-nextjs/src/db/constants/auth.ts +8 -0
  93. package/templates/base-nextjs/src/db/constants/index.ts +44 -0
  94. package/templates/base-nextjs/src/db/types.ts +6 -0
  95. package/templates/base-nextjs/src/env.mjs +48 -0
  96. package/templates/base-nextjs/src/lib/utils.ts +6 -0
  97. package/templates/base-nextjs/src/proxy.ts +23 -0
  98. package/templates/base-nextjs/src/vexcms/access.ts +28 -0
  99. package/templates/base-nextjs/src/vexcms/auth.ts +4 -0
  100. package/templates/base-nextjs/src/vexcms/collections/index.ts +1 -0
  101. package/templates/base-nextjs/src/vexcms/collections/media.ts +15 -0
  102. package/templates/base-nextjs/src/vexcms/collections/users.ts +46 -0
  103. package/templates/base-nextjs/tsconfig.json +60 -0
  104. package/templates/base-nextjs/vex.config.ts +23 -0
  105. package/templates/marketing-site/convex/pages.ts +46 -0
  106. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +58 -0
  107. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +69 -0
  108. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +71 -0
  109. package/templates/marketing-site/src/db/constants/index.ts +52 -0
  110. package/templates/marketing-site/src/vexcms/collections/footers.ts +28 -0
  111. package/templates/marketing-site/src/vexcms/collections/headers.ts +35 -0
  112. package/templates/marketing-site/src/vexcms/collections/index.ts +7 -0
  113. package/templates/marketing-site/src/vexcms/collections/pages.ts +40 -0
  114. package/templates/marketing-site/src/vexcms/collections/site_settings.ts +31 -0
  115. package/templates/marketing-site/src/vexcms/collections/themes.ts +37 -0
  116. package/templates/marketing-site/vex.config.ts +32 -0
@@ -0,0 +1,254 @@
1
+ import type {
2
+ GenericDataModel,
3
+ GenericMutationCtx,
4
+ GenericQueryCtx,
5
+ } from "convex/server"
6
+
7
+ /**
8
+ * Status values for version records.
9
+ * - "draft": a saved draft
10
+ * - "published": this snapshot was published to the main document
11
+ * - "autosave": an autosave version (coalesced in-place)
12
+ */
13
+ export type VersionStatus = "draft" | "published" | "autosave"
14
+
15
+ /**
16
+ * Gets the next version number for a document.
17
+ */
18
+ export async function getNextVersionNumber<DataModel extends GenericDataModel>(props: {
19
+ ctx: GenericQueryCtx<DataModel>
20
+ collection: string
21
+ documentId: string
22
+ }): Promise<number> {
23
+ // Query all versions and find the highest version number.
24
+ // We can't just take the latest by createdAt because previewSnapshot
25
+ // entries have version: 0 and may have a later createdAt.
26
+ const allVersions = await (props.ctx.db as any)
27
+ .query("vex_versions")
28
+ .withIndex("by_document", (q: any) =>
29
+ q.eq("collection", props.collection).eq("documentId", props.documentId),
30
+ )
31
+ .collect()
32
+
33
+ let maxVersion = 0
34
+ for (const v of allVersions) {
35
+ const ver = v.version as number
36
+ if (ver > maxVersion) maxVersion = ver
37
+ }
38
+
39
+ return maxVersion + 1
40
+ }
41
+
42
+ /**
43
+ * Creates a new version record in the vex_versions table.
44
+ */
45
+ export async function createVersion<DataModel extends GenericDataModel>(props: {
46
+ ctx: GenericMutationCtx<DataModel>
47
+ collection: string
48
+ documentId: string
49
+ snapshot: Record<string, unknown>
50
+ status: VersionStatus
51
+ createdBy: string | null
52
+ restoredFrom?: number
53
+ }): Promise<{ versionId: string; version: number }> {
54
+ const version = await getNextVersionNumber({
55
+ ctx: props.ctx,
56
+ collection: props.collection,
57
+ documentId: props.documentId,
58
+ })
59
+
60
+ const versionId = await (props.ctx.db as any).insert("vex_versions", {
61
+ collection: props.collection,
62
+ documentId: props.documentId,
63
+ version,
64
+ status: props.status,
65
+ snapshot: props.snapshot,
66
+ createdAt: Date.now(),
67
+ createdBy: props.createdBy ?? undefined,
68
+ isAutosave: props.status === "autosave",
69
+ restoredFrom: props.restoredFrom ?? undefined,
70
+ })
71
+
72
+ return { versionId: versionId as string, version }
73
+ }
74
+
75
+ /**
76
+ * Gets the latest version for a document (any status).
77
+ * Used by the admin edit view to load the most recent content.
78
+ */
79
+ export async function getLatestVersion<DataModel extends GenericDataModel>(props: {
80
+ ctx: GenericQueryCtx<DataModel>
81
+ collection: string
82
+ documentId: string
83
+ }): Promise<Record<string, unknown> | null> {
84
+ // Get all versions and find the one with the highest version number,
85
+ // excluding previewSnapshot and autosave entries.
86
+ const allVersions = await (props.ctx.db as any)
87
+ .query("vex_versions")
88
+ .withIndex("by_document", (q: any) =>
89
+ q.eq("collection", props.collection).eq("documentId", props.documentId),
90
+ )
91
+ .collect()
92
+
93
+ let latest: Record<string, unknown> | null = null
94
+ let maxVersion = -1
95
+ for (const v of allVersions) {
96
+ if (v.status === "previewSnapshot" || v.status === "autosave") continue
97
+ const ver = v.version as number
98
+ if (ver > maxVersion) {
99
+ maxVersion = ver
100
+ latest = v as Record<string, unknown>
101
+ }
102
+ }
103
+
104
+ return latest
105
+ }
106
+
107
+ /**
108
+ * Gets the latest version with status "published" for a document.
109
+ */
110
+ export async function getLatestPublishedVersion<DataModel extends GenericDataModel>(props: {
111
+ ctx: GenericQueryCtx<DataModel>
112
+ collection: string
113
+ documentId: string
114
+ }): Promise<Record<string, unknown> | null> {
115
+ const latest = await (props.ctx.db as any)
116
+ .query("vex_versions")
117
+ .withIndex("by_document_status", (q: any) =>
118
+ q
119
+ .eq("collection", props.collection)
120
+ .eq("documentId", props.documentId)
121
+ .eq("status", "published"),
122
+ )
123
+ .order("desc")
124
+ .first()
125
+
126
+ return latest as Record<string, unknown> | null
127
+ }
128
+
129
+ /**
130
+ * Lists all versions for a document, ordered newest first.
131
+ * Excludes autosave versions from the list.
132
+ */
133
+ export async function listVersions<DataModel extends GenericDataModel>(props: {
134
+ ctx: GenericQueryCtx<DataModel>
135
+ collection: string
136
+ documentId: string
137
+ limit?: number
138
+ }): Promise<Record<string, unknown>[]> {
139
+ const versions = await (props.ctx.db as any)
140
+ .query("vex_versions")
141
+ .withIndex("by_document_latest", (q: any) =>
142
+ q.eq("collection", props.collection).eq("documentId", props.documentId),
143
+ )
144
+ .order("desc")
145
+ .take(props.limit ?? 50)
146
+
147
+ return (versions as Record<string, unknown>[])
148
+ .filter((v) => !v.isAutosave && v.status !== "previewSnapshot")
149
+ .map((v) => ({
150
+ _id: v._id,
151
+ version: v.version,
152
+ status: v.status,
153
+ createdAt: v.createdAt,
154
+ createdBy: v.createdBy,
155
+ isAutosave: v.isAutosave,
156
+ restoredFrom: v.restoredFrom ?? null,
157
+ }))
158
+ }
159
+
160
+ /**
161
+ * Gets a specific version by version number.
162
+ * Includes the full snapshot for restore preview.
163
+ */
164
+ export async function getVersion<DataModel extends GenericDataModel>(props: {
165
+ ctx: GenericQueryCtx<DataModel>
166
+ collection: string
167
+ documentId: string
168
+ version: number
169
+ }): Promise<Record<string, unknown> | null> {
170
+ const version = await (props.ctx.db as any)
171
+ .query("vex_versions")
172
+ .withIndex("by_document_version", (q: any) =>
173
+ q
174
+ .eq("collection", props.collection)
175
+ .eq("documentId", props.documentId)
176
+ .eq("version", props.version),
177
+ )
178
+ .first()
179
+
180
+ return version as Record<string, unknown> | null
181
+ }
182
+
183
+ /**
184
+ * Finds the latest autosave version for a document and updates it in-place.
185
+ * If no autosave exists, creates a new one.
186
+ */
187
+ export async function coalesceAutosave<DataModel extends GenericDataModel>(props: {
188
+ ctx: GenericMutationCtx<DataModel>
189
+ collection: string
190
+ documentId: string
191
+ snapshot: Record<string, unknown>
192
+ createdBy: string | null
193
+ }): Promise<{ versionId: string; version: number }> {
194
+ const existing = await (props.ctx.db as any)
195
+ .query("vex_versions")
196
+ .withIndex("by_autosave", (q: any) =>
197
+ q
198
+ .eq("collection", props.collection)
199
+ .eq("documentId", props.documentId)
200
+ .eq("isAutosave", true),
201
+ )
202
+ .order("desc")
203
+ .first()
204
+
205
+ if (existing) {
206
+ await (props.ctx.db as any).patch(existing._id, {
207
+ snapshot: props.snapshot,
208
+ createdAt: Date.now(),
209
+ })
210
+ return { versionId: existing._id as string, version: existing.version as number }
211
+ }
212
+
213
+ return createVersion({
214
+ ctx: props.ctx,
215
+ collection: props.collection,
216
+ documentId: props.documentId,
217
+ snapshot: props.snapshot,
218
+ status: "autosave",
219
+ createdBy: props.createdBy,
220
+ })
221
+ }
222
+
223
+ /**
224
+ * Cleans up old versions exceeding the maxPerDoc limit.
225
+ * Preserves all published versions regardless of limit.
226
+ * Never deletes the most recent version.
227
+ */
228
+ export async function cleanupOldVersions<DataModel extends GenericDataModel>(props: {
229
+ ctx: GenericMutationCtx<DataModel>
230
+ collection: string
231
+ documentId: string
232
+ maxPerDoc: number
233
+ }): Promise<void> {
234
+ if (props.maxPerDoc === 0) return
235
+
236
+ const allVersions = await (props.ctx.db as any)
237
+ .query("vex_versions")
238
+ .withIndex("by_document_latest", (q: any) =>
239
+ q.eq("collection", props.collection).eq("documentId", props.documentId),
240
+ )
241
+ .order("desc")
242
+ .collect()
243
+
244
+ if (allVersions.length <= props.maxPerDoc) return
245
+
246
+ const toKeep = allVersions.slice(0, props.maxPerDoc)
247
+ const candidates = allVersions.slice(props.maxPerDoc)
248
+
249
+ for (const candidate of candidates) {
250
+ // Always preserve published versions
251
+ if (candidate.status === "published") continue
252
+ await (props.ctx.db as any).delete(candidate._id)
253
+ }
254
+ }
@@ -0,0 +1,53 @@
1
+ import type { DataModel } from "@convex/_generated/dataModel"
2
+
3
+ import { mutation, query } from "@convex/_generated/server"
4
+ import {
5
+ upsertPreviewSnapshot,
6
+ deletePreviewSnapshot,
7
+ getPreviewSnapshot,
8
+ } from "@vexcms/core"
9
+ import { v } from "convex/values"
10
+
11
+ export const upsert = mutation({
12
+ args: {
13
+ collectionSlug: v.string(),
14
+ documentId: v.string(),
15
+ snapshot: v.any(),
16
+ },
17
+ handler: async (ctx, { collectionSlug, documentId, snapshot }) => {
18
+ await upsertPreviewSnapshot<DataModel>({
19
+ ctx,
20
+ collection: collectionSlug,
21
+ documentId,
22
+ snapshot: snapshot as Record<string, unknown>,
23
+ })
24
+ },
25
+ })
26
+
27
+ export const remove = mutation({
28
+ args: {
29
+ collectionSlug: v.string(),
30
+ documentId: v.string(),
31
+ },
32
+ handler: async (ctx, { collectionSlug, documentId }) => {
33
+ await deletePreviewSnapshot<DataModel>({
34
+ ctx,
35
+ collection: collectionSlug,
36
+ documentId,
37
+ })
38
+ },
39
+ })
40
+
41
+ export const get = query({
42
+ args: {
43
+ collectionSlug: v.string(),
44
+ documentId: v.string(),
45
+ },
46
+ handler: async (ctx, { collectionSlug, documentId }) => {
47
+ return await getPreviewSnapshot<DataModel>({
48
+ ctx,
49
+ collection: collectionSlug,
50
+ documentId,
51
+ })
52
+ },
53
+ })