create-vexcms 0.0.19 → 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 (154) 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 -22
  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 +98 -11
  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/components/motion-primitives/animated-group.tsx +2 -2
  85. package/templates/marketing-site/src/lib/metadata.ts +51 -72
  86. package/templates/marketing-site/src/vex.config.ts +35 -0
  87. package/templates/marketing-site/src/vex.types.ts +634 -0
  88. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  89. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  90. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  91. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  92. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  93. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  94. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  95. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  96. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  97. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  98. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  99. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  100. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
  101. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  102. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  103. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  104. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  105. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  106. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  107. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  108. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  109. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  110. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  111. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  112. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  113. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  114. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  115. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  116. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  117. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  118. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  119. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  120. package/templates/base-nextjs/convex/vex/collections.ts +0 -321
  121. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  122. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  123. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  124. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  125. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  126. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  127. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  128. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  129. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  130. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  131. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  132. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  133. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  134. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  135. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  136. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  137. package/templates/base-nextjs/src/db/types.ts +0 -6
  138. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  139. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  140. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  141. package/templates/base-nextjs/vex.config.ts +0 -25
  142. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  143. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  144. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  145. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  146. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  147. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  148. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  149. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  150. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  151. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  152. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  153. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  154. package/templates/marketing-site/vex.config.ts +0 -32
@@ -1,321 +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 { findCollectionBySlug, hasPermission } from "@vexcms/core"
6
- import { paginationOptsValidator } from "convex/server"
7
- import { ConvexError, v } from "convex/values"
8
-
9
- import { TABLE_SLUG_USERS } from "~/db/constants"
10
-
11
- import config from "../../vex.config"
12
- import { access } from "../../src/vexcms/access"
13
- import * as Collections from "./model/collections"
14
-
15
- function requireCollection(slug: string) {
16
- const match = findCollectionBySlug({ slug, config })
17
- if (!match) {
18
- throw new ConvexError(`Collection not found: ${slug}`)
19
- }
20
- return match
21
- }
22
-
23
- /**
24
- * Try to get the current user. Returns null if not authenticated.
25
- * Use this for queries where auth is optional (read filtering).
26
- */
27
- async function getUser(ctx: GenericQueryCtx<DataModel>) {
28
- const identity = await ctx.auth.getUserIdentity()
29
- if (!identity?.email) return null
30
-
31
- const user = await ctx.db
32
- .query(TABLE_SLUG_USERS)
33
- .withIndex("by_email", (q) => q.eq("email", identity.email!))
34
- .first()
35
-
36
- if (!user) return null
37
-
38
- return {
39
- user: user as Record<string, unknown>,
40
- roles: (user.role as string[]) ?? [],
41
- }
42
- }
43
-
44
- /**
45
- * Get the current user, throwing if not authenticated.
46
- * Use this for mutations where auth is required.
47
- */
48
- async function requireUser(ctx: GenericQueryCtx<DataModel>) {
49
- const result = await getUser(ctx)
50
- if (!result) {
51
- throw new ConvexError("Not authenticated")
52
- }
53
- return result
54
- }
55
-
56
- /**
57
- * Check permission and throw ConvexError if denied.
58
- * Error message includes which roles would grant the permission.
59
- */
60
- function checkPermission(props: Parameters<typeof hasPermission>[0]) {
61
- const result = hasPermission(props)
62
- const denied =
63
- result === false ||
64
- (typeof result === "object" && !Object.values(result).some(Boolean))
65
-
66
- if (denied) {
67
- const grantingRoles: string[] = []
68
- if (props.access) {
69
- for (const role of props.access.roles) {
70
- const check = hasPermission({ ...props, userRoles: [role] })
71
- const allowed =
72
- check === true ||
73
- (typeof check === "object" && Object.values(check).some(Boolean))
74
- if (allowed) grantingRoles.push(role)
75
- }
76
- }
77
-
78
- const rolesHint =
79
- grantingRoles.length > 0
80
- ? ` Requires one of: ${grantingRoles.join(", ")}`
81
- : ""
82
- throw new ConvexError(
83
- `Access denied: "${props.action}" on "${props.resource}".${rolesHint}`,
84
- )
85
- }
86
-
87
- return result
88
- }
89
-
90
- export const listDocuments = query({
91
- args: {
92
- collectionSlug: v.string(),
93
- order: v.optional(v.union(v.literal("asc"), v.literal("desc"))),
94
- paginationOpts: paginationOptsValidator,
95
- },
96
- handler: async (ctx, args) => {
97
- const result = await Collections.listDocuments<DataModel>({
98
- args: {
99
- collectionSlug: args.collectionSlug as TableNamesInDataModel<DataModel>,
100
- order: args.order,
101
- paginationOpts: args.paginationOpts,
102
- },
103
- ctx,
104
- })
105
-
106
- // Filter by read permission if user is authenticated
107
- const auth = await getUser(ctx)
108
- if (!auth) return result
109
-
110
- const filteredPage = result.page.filter((doc: Record<string, unknown>) => {
111
- const readAllowed = hasPermission({
112
- access,
113
- user: auth.user,
114
- userRoles: auth.roles,
115
- resource: args.collectionSlug,
116
- action: "read",
117
- data: doc,
118
- })
119
- return readAllowed === true
120
- })
121
-
122
- return { ...result, page: filteredPage }
123
- },
124
- })
125
-
126
- export const countDocuments = query({
127
- args: { collectionSlug: v.string() },
128
- handler: async (ctx, { collectionSlug }) => {
129
- return await Collections.countDocuments<DataModel>({
130
- args: { collectionSlug: collectionSlug as TableNamesInDataModel<DataModel> },
131
- ctx,
132
- })
133
- },
134
- })
135
-
136
- export const getDocument = query({
137
- args: {
138
- collectionSlug: v.string(),
139
- documentId: v.string(),
140
- preview: v.optional(v.boolean()),
141
- },
142
- handler: async (ctx, { collectionSlug, documentId, preview }) => {
143
- const doc = await Collections.getDocument<DataModel>({
144
- args: {
145
- collectionSlug: collectionSlug as TableNamesInDataModel<DataModel>,
146
- documentId,
147
- preview,
148
- },
149
- ctx,
150
- })
151
-
152
- if (doc) {
153
- const auth = await getUser(ctx)
154
- if (auth) {
155
- const readAllowed = hasPermission({
156
- access,
157
- user: auth.user,
158
- userRoles: auth.roles,
159
- resource: collectionSlug,
160
- action: "read",
161
- data: doc as Record<string, unknown>,
162
- })
163
- if (readAllowed !== true) return null
164
- }
165
- }
166
-
167
- return doc
168
- },
169
- })
170
-
171
- export const updateDocument = mutation({
172
- args: {
173
- collectionSlug: v.string(),
174
- documentId: v.string(),
175
- fields: v.any(),
176
- },
177
- handler: async (ctx, { collectionSlug, documentId, fields }) => {
178
- const match = requireCollection(collectionSlug)
179
- const { user, roles } = await requireUser(ctx)
180
-
181
- const fieldNames = Object.keys(fields as Record<string, unknown>)
182
- checkPermission({
183
- access,
184
- user,
185
- userRoles: roles,
186
- resource: collectionSlug,
187
- action: "update",
188
- fields: fieldNames,
189
- data: fields as Record<string, unknown>,
190
- })
191
-
192
- return await Collections.updateDocument<DataModel>({
193
- args: {
194
- collectionFields: match.fields,
195
- collectionSlug: collectionSlug as TableNamesInDataModel<DataModel>,
196
- documentId,
197
- fields: fields as Record<string, unknown>,
198
- },
199
- ctx,
200
- })
201
- },
202
- })
203
-
204
- export const createDocument = mutation({
205
- args: {
206
- collectionSlug: v.string(),
207
- fields: v.any(),
208
- },
209
- handler: async (ctx, { collectionSlug, fields }) => {
210
- const match = requireCollection(collectionSlug)
211
- const { user, roles } = await requireUser(ctx)
212
-
213
- const fieldNames = Object.keys(fields as Record<string, unknown>)
214
- checkPermission({
215
- access,
216
- user,
217
- userRoles: roles,
218
- resource: collectionSlug,
219
- action: "create",
220
- fields: fieldNames,
221
- data: fields as Record<string, unknown>,
222
- })
223
-
224
- return await Collections.createDocument<DataModel>({
225
- args: {
226
- collectionFields: match.fields,
227
- collectionSlug: collectionSlug as TableNamesInDataModel<DataModel>,
228
- fields: fields as Record<string, unknown>,
229
- kind: match.kind,
230
- },
231
- ctx,
232
- })
233
- },
234
- })
235
-
236
- export const deleteDocument = mutation({
237
- args: {
238
- collectionSlug: v.string(),
239
- documentId: v.string(),
240
- },
241
- handler: async (ctx, { collectionSlug, documentId }) => {
242
- const match = requireCollection(collectionSlug)
243
- const { user, roles } = await requireUser(ctx)
244
-
245
- checkPermission({
246
- access,
247
- user,
248
- userRoles: roles,
249
- resource: collectionSlug,
250
- action: "delete",
251
- })
252
-
253
- await Collections.deleteDocument<DataModel>({
254
- args: {
255
- collectionSlug: collectionSlug as TableNamesInDataModel<DataModel>,
256
- documentId,
257
- kind: match.kind,
258
- },
259
- ctx,
260
- })
261
- },
262
- })
263
-
264
- export const bulkDeleteDocuments = mutation({
265
- args: {
266
- collectionSlug: v.string(),
267
- documentIds: v.array(v.string()),
268
- },
269
- handler: async (ctx, { collectionSlug, documentIds }) => {
270
- const { user, roles } = await requireUser(ctx)
271
-
272
- checkPermission({
273
- access,
274
- user,
275
- userRoles: roles,
276
- resource: collectionSlug,
277
- action: "delete",
278
- })
279
-
280
- return await Collections.bulkDeleteDocuments<DataModel>({
281
- args: { documentIds },
282
- ctx,
283
- })
284
- },
285
- })
286
-
287
- export const searchDocuments = query({
288
- args: {
289
- collectionSlug: v.string(),
290
- query: v.string(),
291
- searchField: v.string(),
292
- searchIndexName: v.string(),
293
- },
294
- handler: async (ctx, { collectionSlug, query: searchQuery, searchField, searchIndexName }) => {
295
- const results = await Collections.searchDocuments<DataModel>({
296
- args: {
297
- collectionSlug: collectionSlug as TableNamesInDataModel<DataModel>,
298
- query: searchQuery,
299
- searchField,
300
- searchIndexName,
301
- },
302
- ctx,
303
- })
304
-
305
- // Filter by read permission if user is authenticated
306
- const auth = await getUser(ctx)
307
- if (!auth) return results
308
-
309
- return (results as Record<string, unknown>[]).filter((doc) => {
310
- const readAllowed = hasPermission({
311
- access,
312
- user: auth.user,
313
- userRoles: auth.roles,
314
- resource: collectionSlug,
315
- action: "read",
316
- data: doc,
317
- })
318
- return readAllowed === true
319
- })
320
- },
321
- })
@@ -1,33 +0,0 @@
1
- import { createVexQuery } from "@vexcms/core"
2
-
3
- import { query } from "../_generated/server"
4
-
5
- /**
6
- * Typed vexQuery builder — use this instead of `query` for queries
7
- * that need draft/snapshot support (live preview, versioning).
8
- *
9
- * Automatically adds `_vexDrafts` arg and provides `ctx.drafts`
10
- * with full return type inference from your DataModel.
11
- *
12
- * @example
13
- * ```ts
14
- * import { vexQuery } from "./vex/helpers"
15
- * import { getPreviewSnapshot } from "@vexcms/core"
16
- *
17
- * export const getBySlug = vexQuery({
18
- * args: { slug: v.string() },
19
- * handler: async (ctx, args) => {
20
- * const doc = await ctx.db.query("pages")
21
- * .withIndex("by_slug", (q) => q.eq("slug", args.slug))
22
- * .first()
23
- * if (!doc) return null
24
- * if (ctx.drafts === "snapshot") {
25
- * const snapshot = await getPreviewSnapshot({ ctx, collection: "pages", documentId: doc._id })
26
- * if (snapshot) return { ...doc, ...snapshot }
27
- * }
28
- * return doc
29
- * },
30
- * })
31
- * ```
32
- */
33
- export const vexQuery = createVexQuery(query)
@@ -1,51 +0,0 @@
1
- import type { DataModel } from "@convex/_generated/dataModel"
2
- import type { GenericQueryCtx } from "convex/server"
3
-
4
- import { query } from "@convex/_generated/server"
5
-
6
- import { TABLE_SLUG_USERS } from "~/db/constants"
7
-
8
- import { access } from "../../src/vexcms/access"
9
-
10
- async function getUser(ctx: GenericQueryCtx<DataModel>) {
11
- const identity = await ctx.auth.getUserIdentity()
12
- if (!identity?.email) return null
13
-
14
- const user = await ctx.db
15
- .query(TABLE_SLUG_USERS)
16
- .withIndex("by_email", (q) => q.eq("email", identity.email!))
17
- .first()
18
-
19
- return user ?? null
20
- }
21
-
22
- /**
23
- * List users available for impersonation.
24
- * Only accessible by users with adminRoles.
25
- */
26
- export const listImpersonatableUsers = query({
27
- args: {},
28
- handler: async (ctx) => {
29
- const currentUser = await getUser(ctx)
30
- if (!currentUser) return []
31
-
32
- const roles = (currentUser.role as string[]) ?? []
33
-
34
- // Check if user has an adminRole
35
- const isAdmin = roles.some((r) => access.adminRoles.includes(r))
36
- if (!isAdmin) return []
37
-
38
- // Fetch all users except the current one
39
- const allUsers = await ctx.db.query(TABLE_SLUG_USERS).collect()
40
-
41
- return allUsers
42
- .filter((u) => u._id !== currentUser._id)
43
- .map((u) => ({
44
- id: u._id as string,
45
- name: u.name as string,
46
- email: u.email as string,
47
- avatar: (u.image as string) ?? undefined,
48
- roles: (u.role as string[]) ?? [],
49
- }))
50
- },
51
- })
@@ -1,74 +0,0 @@
1
- import { mutation } from "../_generated/server";
2
- import { v } from "convex/values";
3
-
4
- /**
5
- * Generic backfill mutation called by the Vex CLI during auto-migration.
6
- * Patches existing documents that are missing a field with a default value.
7
- *
8
- * The CLI calls this in a loop with cursor pagination until `isDone` is true.
9
- */
10
- export const backfillField = mutation({
11
- args: {
12
- table: v.string(),
13
- field: v.string(),
14
- value: v.any(),
15
- cursor: v.optional(v.string()),
16
- batchSize: v.optional(v.number()),
17
- },
18
- handler: async (ctx, { table, field, value, cursor, batchSize = 100 }) => {
19
- const results = await ctx.db
20
- .query(table as any)
21
- .paginate({ cursor: cursor ?? null, numItems: batchSize });
22
-
23
- let patched = 0;
24
- for (const doc of results.page) {
25
- if ((doc as any)[field] === undefined) {
26
- await ctx.db.patch(doc._id, { [field]: value } as any);
27
- patched++;
28
- }
29
- }
30
-
31
- return {
32
- patched,
33
- isDone: results.isDone,
34
- cursor: results.continueCursor,
35
- };
36
- },
37
- });
38
-
39
- /**
40
- * Generic field removal mutation called by the Vex CLI during auto-migration.
41
- * Unsets a field from existing documents so the new schema (without the field)
42
- * can be deployed without validation errors.
43
- *
44
- * The CLI calls this in a loop with cursor pagination until `isDone` is true.
45
- */
46
- export const removeField = mutation({
47
- args: {
48
- table: v.string(),
49
- field: v.string(),
50
- cursor: v.optional(v.string()),
51
- batchSize: v.optional(v.number()),
52
- },
53
- handler: async (ctx, { table, field, cursor, batchSize = 100 }) => {
54
- const results = await ctx.db
55
- .query(table as any)
56
- .paginate({ cursor: cursor ?? null, numItems: batchSize });
57
-
58
- let patched = 0;
59
- for (const doc of results.page) {
60
- if ((doc as any)[field] !== undefined) {
61
- // Replace the entire document without the removed field
62
- const { _id, _creationTime, [field]: _removed, ...rest } = doc as any;
63
- await ctx.db.replace(_id, rest);
64
- patched++;
65
- }
66
- }
67
-
68
- return {
69
- patched,
70
- isDone: results.isDone,
71
- cursor: results.continueCursor,
72
- };
73
- },
74
- });
@@ -1,196 +0,0 @@
1
- import type {
2
- GenericDataModel,
3
- GenericMutationCtx,
4
- GenericQueryCtx,
5
- PaginationOptions,
6
- TableNamesInDataModel,
7
- } from "convex/server"
8
-
9
- import { ConvexError } from "convex/values"
10
- import { generateFormSchema, getPreviewSnapshot } from "@vexcms/core"
11
- import type { VexField, CollectionKind } from "@vexcms/core"
12
-
13
- async function resolveStorageUrl(
14
- ctx: { storage: { getUrl: (id: any) => Promise<string | null> } },
15
- doc: any,
16
- ) {
17
- if (doc?.storageId) {
18
- const url = await ctx.storage.getUrl(doc.storageId)
19
- if (url) return { ...doc, url }
20
- }
21
- return doc
22
- }
23
-
24
- export async function listDocuments<DataModel extends GenericDataModel>(props: {
25
- args: {
26
- collectionSlug: TableNamesInDataModel<DataModel>
27
- paginationOpts: PaginationOptions
28
- order?: "asc" | "desc"
29
- }
30
- ctx: GenericQueryCtx<DataModel>
31
- }) {
32
- const { args, ctx } = props
33
- const q = args.order === "desc"
34
- ? ctx.db.query(args.collectionSlug).order("desc")
35
- : ctx.db.query(args.collectionSlug)
36
- const result = await q.paginate(args.paginationOpts)
37
- const resolvedPage = await Promise.all(
38
- result.page.map((doc: any) => resolveStorageUrl(ctx, doc)),
39
- )
40
- return { ...result, page: resolvedPage }
41
- }
42
-
43
- export async function countDocuments<DataModel extends GenericDataModel>(props: {
44
- ctx: GenericQueryCtx<DataModel>
45
- args: { collectionSlug: TableNamesInDataModel<DataModel> }
46
- }): Promise<number> {
47
- return await (props.ctx.db.query(props.args.collectionSlug) as any).count()
48
- }
49
-
50
- export async function getDocument<DataModel extends GenericDataModel>(props: {
51
- ctx: GenericQueryCtx<DataModel>
52
- args: {
53
- collectionSlug: TableNamesInDataModel<DataModel>
54
- documentId: string
55
- /** When true, merges the transient preview snapshot (from admin live preview) */
56
- preview?: boolean
57
- }
58
- }) {
59
- const doc = await props.ctx.db.get(props.args.documentId as any)
60
- if (!doc) return null
61
- const resolved = await resolveStorageUrl(props.ctx, doc)
62
-
63
- // Merge preview snapshot when explicitly requested (live preview iframe).
64
- if (props.args.preview) {
65
- const snapshot = await getPreviewSnapshot<DataModel>({
66
- ctx: props.ctx,
67
- collection: props.args.collectionSlug as string,
68
- documentId: props.args.documentId,
69
- })
70
- if (snapshot) {
71
- return { ...resolved, ...snapshot }
72
- }
73
- }
74
-
75
- return resolved
76
- }
77
-
78
- export async function updateDocument<DataModel extends GenericDataModel>(props: {
79
- ctx: GenericMutationCtx<DataModel>
80
- args: {
81
- collectionSlug: TableNamesInDataModel<DataModel>
82
- documentId: string
83
- fields: Record<string, unknown>
84
- collectionFields: Record<string, VexField>
85
- }
86
- }) {
87
- const f = { ...props.args.fields }
88
-
89
- // Resolve the file URL from storageId when replacing a media file
90
- if (f.storageId && f.url === "") {
91
- const url = await props.ctx.storage.getUrl(f.storageId as any)
92
- if (url) f.url = url
93
- }
94
-
95
- const schema = generateFormSchema({
96
- fields: props.args.collectionFields,
97
- }).partial()
98
-
99
- const result = schema.safeParse(f)
100
- if (!result.success) {
101
- throw new ConvexError({
102
- message: "Validation failed",
103
- errors: result.error.flatten(),
104
- })
105
- }
106
-
107
- await props.ctx.db.patch(props.args.documentId as any, result.data as any)
108
- return props.args.documentId
109
- }
110
-
111
- export async function createDocument<DataModel extends GenericDataModel>(props: {
112
- ctx: GenericMutationCtx<DataModel>
113
- args: {
114
- collectionSlug: TableNamesInDataModel<DataModel>
115
- fields: Record<string, unknown>
116
- collectionFields: Record<string, VexField>
117
- kind: CollectionKind
118
- }
119
- }): Promise<string> {
120
- if (props.args.kind === "global") {
121
- const existing = await props.ctx.db.query(props.args.collectionSlug).first()
122
- if (existing) {
123
- throw new ConvexError(
124
- `Global "${props.args.collectionSlug}" already exists. Globals can only have one document.`,
125
- )
126
- }
127
- }
128
-
129
- const schema = generateFormSchema({
130
- fields: props.args.collectionFields,
131
- })
132
-
133
- const result = schema.safeParse(props.args.fields)
134
- if (!result.success) {
135
- throw new ConvexError({
136
- message: "Validation failed",
137
- errors: result.error.flatten(),
138
- })
139
- }
140
-
141
- const data = result.data as Record<string, unknown>
142
- // Default vex_status to "published" for all user collections
143
- if (!data.vex_status) {
144
- data.vex_status = "published"
145
- }
146
- const id = await props.ctx.db.insert(props.args.collectionSlug as any, data as any)
147
- return id as string
148
- }
149
-
150
- export async function deleteDocument<DataModel extends GenericDataModel>(props: {
151
- ctx: GenericMutationCtx<DataModel>
152
- args: {
153
- collectionSlug: TableNamesInDataModel<DataModel>
154
- documentId: string
155
- kind: CollectionKind
156
- }
157
- }): Promise<void> {
158
- if (props.args.kind === "global") {
159
- const existing = await props.ctx.db.get(props.args.documentId as any)
160
- if (!existing) {
161
- throw new ConvexError(
162
- `Global "${props.args.collectionSlug}" document not found. Cannot delete a non-existent global.`,
163
- )
164
- }
165
- }
166
-
167
- await props.ctx.db.delete(props.args.documentId as any)
168
- }
169
-
170
- export async function bulkDeleteDocuments<DataModel extends GenericDataModel>(props: {
171
- ctx: GenericMutationCtx<DataModel>
172
- args: {
173
- documentIds: string[]
174
- }
175
- }): Promise<{ deleted: number }> {
176
- for (const id of props.args.documentIds) {
177
- await props.ctx.db.delete(id as any)
178
- }
179
- return { deleted: props.args.documentIds.length }
180
- }
181
-
182
- export async function searchDocuments<DataModel extends GenericDataModel>(props: {
183
- args: {
184
- collectionSlug: TableNamesInDataModel<DataModel>
185
- searchIndexName: string
186
- searchField: string
187
- query: string
188
- }
189
- ctx: GenericQueryCtx<DataModel>
190
- }) {
191
- const { args, ctx } = props
192
- const docs = await (ctx.db.query(args.collectionSlug) as any)
193
- .withSearchIndex(args.searchIndexName, (q: any) => q.search(args.searchField, args.query))
194
- .take(50)
195
- return Promise.all(docs.map((doc: any) => resolveStorageUrl(ctx, doc)))
196
- }