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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2409 -0
- package/package.json +37 -0
- package/templates/base-nextjs/.prettierignore +26 -0
- package/templates/base-nextjs/.prettierrc +7 -0
- package/templates/base-nextjs/README.md +256 -0
- package/templates/base-nextjs/_gitignore +2 -0
- package/templates/base-nextjs/components.json +24 -0
- package/templates/base-nextjs/convex/_generated/api.d.ts +125 -0
- package/templates/base-nextjs/convex/_generated/api.js +23 -0
- package/templates/base-nextjs/convex/_generated/dataModel.d.ts +60 -0
- package/templates/base-nextjs/convex/_generated/server.d.ts +143 -0
- package/templates/base-nextjs/convex/_generated/server.js +93 -0
- package/templates/base-nextjs/convex/auth/adapter/index.ts +230 -0
- package/templates/base-nextjs/convex/auth/adapter/utils.ts +547 -0
- package/templates/base-nextjs/convex/auth/api.ts +8 -0
- package/templates/base-nextjs/convex/auth/config.ts +10 -0
- package/templates/base-nextjs/convex/auth/db.ts +303 -0
- package/templates/base-nextjs/convex/auth/index.ts +14 -0
- package/templates/base-nextjs/convex/auth/options.ts +63 -0
- package/templates/base-nextjs/convex/auth/plugins/index.ts +20 -0
- package/templates/base-nextjs/convex/auth/sessions.ts +60 -0
- package/templates/base-nextjs/convex/auth.config.ts +7 -0
- package/templates/base-nextjs/convex/convex.config.ts +5 -0
- package/templates/base-nextjs/convex/http.ts +28 -0
- package/templates/base-nextjs/convex/schema.ts +3 -0
- package/templates/base-nextjs/convex/vex/auth.ts +38 -0
- package/templates/base-nextjs/convex/vex/collections.ts +321 -0
- package/templates/base-nextjs/convex/vex/firstUser.ts +134 -0
- package/templates/base-nextjs/convex/vex/helpers.ts +33 -0
- package/templates/base-nextjs/convex/vex/impersonation.ts +51 -0
- package/templates/base-nextjs/convex/vex/media.ts +246 -0
- package/templates/base-nextjs/convex/vex/migrate.ts +74 -0
- package/templates/base-nextjs/convex/vex/model/collections.ts +196 -0
- package/templates/base-nextjs/convex/vex/model/media.ts +87 -0
- package/templates/base-nextjs/convex/vex/model/versions.ts +254 -0
- package/templates/base-nextjs/convex/vex/previewSnapshot.ts +53 -0
- package/templates/base-nextjs/convex/vex/versions.ts +588 -0
- package/templates/base-nextjs/eslint.config.mjs +164 -0
- package/templates/base-nextjs/next.config.ts +10 -0
- package/templates/base-nextjs/package.json +67 -0
- package/templates/base-nextjs/postcss.config.mjs +7 -0
- package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
- package/templates/base-nextjs/public/file.svg +1 -0
- package/templates/base-nextjs/public/globe.svg +1 -0
- package/templates/base-nextjs/public/next.svg +1 -0
- package/templates/base-nextjs/public/vercel.svg +1 -0
- package/templates/base-nextjs/public/window.svg +1 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/page.tsx +9 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +23 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/default.tsx +3 -0
- package/templates/base-nextjs/src/app/(frontend)/@auth/page.tsx +3 -0
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/page.tsx +9 -0
- package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +11 -0
- package/templates/base-nextjs/src/app/(frontend)/layout.tsx +14 -0
- package/templates/base-nextjs/src/app/(frontend)/page.tsx +116 -0
- package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +33 -0
- package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +38 -0
- package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +101 -0
- package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +15 -0
- package/templates/base-nextjs/src/app/admin/layout.tsx +33 -0
- package/templates/base-nextjs/src/app/api/auth/[...all]/route.ts +6 -0
- package/templates/base-nextjs/src/app/favicon.ico +0 -0
- package/templates/base-nextjs/src/app/globals.css +130 -0
- package/templates/base-nextjs/src/app/layout.tsx +30 -0
- package/templates/base-nextjs/src/auth/client.tsx +41 -0
- package/templates/base-nextjs/src/auth/permissions.ts +102 -0
- package/templates/base-nextjs/src/auth/server.ts +16 -0
- package/templates/base-nextjs/src/auth/serverUtils.ts +64 -0
- package/templates/base-nextjs/src/auth/types.ts +3 -0
- package/templates/base-nextjs/src/components/component-example.tsx +474 -0
- package/templates/base-nextjs/src/components/example.tsx +52 -0
- package/templates/base-nextjs/src/components/providers/client.tsx +9 -0
- package/templates/base-nextjs/src/components/providers/convex.tsx +32 -0
- package/templates/base-nextjs/src/components/providers/server.tsx +15 -0
- package/templates/base-nextjs/src/components/providers/theme.tsx +11 -0
- package/templates/base-nextjs/src/components/ui/alert-dialog.tsx +162 -0
- package/templates/base-nextjs/src/components/ui/badge.tsx +52 -0
- package/templates/base-nextjs/src/components/ui/button.tsx +60 -0
- package/templates/base-nextjs/src/components/ui/card.tsx +92 -0
- package/templates/base-nextjs/src/components/ui/combobox.tsx +271 -0
- package/templates/base-nextjs/src/components/ui/dialog.tsx +135 -0
- package/templates/base-nextjs/src/components/ui/dropdown-menu.tsx +246 -0
- package/templates/base-nextjs/src/components/ui/field.tsx +224 -0
- package/templates/base-nextjs/src/components/ui/input-group.tsx +146 -0
- package/templates/base-nextjs/src/components/ui/input.tsx +20 -0
- package/templates/base-nextjs/src/components/ui/label.tsx +20 -0
- package/templates/base-nextjs/src/components/ui/select.tsx +189 -0
- package/templates/base-nextjs/src/components/ui/separator.tsx +21 -0
- package/templates/base-nextjs/src/components/ui/textarea.tsx +18 -0
- package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +67 -0
- package/templates/base-nextjs/src/db/constants/auth.ts +8 -0
- package/templates/base-nextjs/src/db/constants/index.ts +44 -0
- package/templates/base-nextjs/src/db/types.ts +6 -0
- package/templates/base-nextjs/src/env.mjs +48 -0
- package/templates/base-nextjs/src/lib/utils.ts +6 -0
- package/templates/base-nextjs/src/proxy.ts +23 -0
- package/templates/base-nextjs/src/vexcms/access.ts +28 -0
- package/templates/base-nextjs/src/vexcms/auth.ts +4 -0
- package/templates/base-nextjs/src/vexcms/collections/index.ts +1 -0
- package/templates/base-nextjs/src/vexcms/collections/media.ts +15 -0
- package/templates/base-nextjs/src/vexcms/collections/users.ts +46 -0
- package/templates/base-nextjs/tsconfig.json +60 -0
- package/templates/base-nextjs/vex.config.ts +23 -0
- package/templates/marketing-site/convex/pages.ts +46 -0
- package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +58 -0
- package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +69 -0
- package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +71 -0
- package/templates/marketing-site/src/db/constants/index.ts +52 -0
- package/templates/marketing-site/src/vexcms/collections/footers.ts +28 -0
- package/templates/marketing-site/src/vexcms/collections/headers.ts +35 -0
- package/templates/marketing-site/src/vexcms/collections/index.ts +7 -0
- package/templates/marketing-site/src/vexcms/collections/pages.ts +40 -0
- package/templates/marketing-site/src/vexcms/collections/site_settings.ts +31 -0
- package/templates/marketing-site/src/vexcms/collections/themes.ts +37 -0
- 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
|
+
})
|