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,547 @@
1
+ import type { BetterAuthDBSchema } from "better-auth/db"
2
+ import type {
3
+ DocumentByName,
4
+ GenericDataModel,
5
+ GenericQueryCtx,
6
+ PaginationOptions,
7
+ PaginationResult,
8
+ SchemaDefinition,
9
+ TableNamesInDataModel,
10
+ } from "convex/server"
11
+ import type { GenericId } from "convex/values"
12
+
13
+ import { asyncMap } from "convex-helpers"
14
+ import { mergedStream, stream } from "convex-helpers/server/stream"
15
+
16
+ export type QueryArgs = {
17
+ limit?: number
18
+ model: string
19
+ offset?: number
20
+ select?: string[]
21
+ sortBy?: {
22
+ direction: "asc" | "desc"
23
+ field: string
24
+ }
25
+ where?: WhereClause[]
26
+ }
27
+
28
+ export type WhereClause = {
29
+ connector?: "AND" | "OR"
30
+ field: string
31
+ operator?:
32
+ | "contains"
33
+ | "ends_with"
34
+ | "eq"
35
+ | "gt"
36
+ | "gte"
37
+ | "in"
38
+ | "lt"
39
+ | "lte"
40
+ | "ne"
41
+ | "not_in"
42
+ | "starts_with"
43
+ value: boolean | null | number | number[] | string | string[]
44
+ }
45
+
46
+ export const isUniqueField = (
47
+ betterAuthSchema: BetterAuthDBSchema,
48
+ model: string,
49
+ field: string
50
+ ): boolean => {
51
+ const fields = betterAuthSchema[model]?.fields
52
+ if (!fields) {
53
+ return false
54
+ }
55
+ return Object.entries(fields)
56
+ .filter(([, value]) => value.unique)
57
+ .map(([key]) => key)
58
+ .includes(field)
59
+ }
60
+
61
+ export const hasUniqueFields = (
62
+ betterAuthSchema: BetterAuthDBSchema,
63
+ model: string,
64
+ input: Record<string, any>
65
+ ): boolean => {
66
+ for (const field of Object.keys(input)) {
67
+ if (isUniqueField(betterAuthSchema, model, field)) {
68
+ return true
69
+ }
70
+ }
71
+ return false
72
+ }
73
+
74
+ export const findIndex = (schema: SchemaDefinition<any, any>, args: QueryArgs) => {
75
+ if ((args.where?.length ?? 0) > 1 && args.where?.some((w) => w.connector === "OR")) {
76
+ throw new Error(
77
+ `OR connector not supported with multiple where statements in findIndex, split up the where statements before calling findIndex: ${JSON.stringify(args.where)}`
78
+ )
79
+ }
80
+ const where = args.where?.filter((w) => {
81
+ return (
82
+ (!w.operator || ["eq", "gt", "gte", "in", "lt", "lte", "not_in"].includes(w.operator)) &&
83
+ w.field !== "_id"
84
+ )
85
+ })
86
+ if (!where?.length && !args.sortBy) {
87
+ return
88
+ }
89
+ const lowerBounds = where?.filter((w) => w.operator === "lt" || w.operator === "lte") ?? []
90
+ if (lowerBounds.length > 1) {
91
+ throw new Error(`cannot have more than one lower bound where clause: ${JSON.stringify(where)}`)
92
+ }
93
+ const upperBounds = where?.filter((w) => w.operator === "gt" || w.operator === "gte") ?? []
94
+ if (upperBounds.length > 1) {
95
+ throw new Error(`cannot have more than one upper bound where clause: ${JSON.stringify(where)}`)
96
+ }
97
+ const lowerBound = lowerBounds[0]
98
+ const upperBound = upperBounds[0]
99
+ if (lowerBound && upperBound && lowerBound.field !== upperBound.field) {
100
+ throw new Error(
101
+ `lower bound and upper bound must have the same field: ${JSON.stringify(where)}`
102
+ )
103
+ }
104
+ const boundField = lowerBound?.field ?? upperBound?.field
105
+ if (
106
+ boundField &&
107
+ where?.some((w) => w.field === boundField && w !== lowerBound && w !== upperBound)
108
+ ) {
109
+ throw new Error(`too many where clauses on the bound field: ${JSON.stringify(where)}`)
110
+ }
111
+ const indexEqFields =
112
+ where
113
+ ?.filter((w) => !w.operator || w.operator === "eq")
114
+ .sort((a, b) => {
115
+ return a.field.localeCompare(b.field)
116
+ })
117
+ .map((w) => [w.field, w.value]) ?? []
118
+ if (!indexEqFields?.length && !boundField && !args.sortBy) {
119
+ return
120
+ }
121
+ const table = schema.tables[args.model as keyof typeof schema.tables]
122
+ if (!table) {
123
+ throw new Error(`Table ${args.model} not found`)
124
+ }
125
+ const indexes = table[" indexes"]()
126
+ const sortField = args.sortBy?.field
127
+
128
+ // We internally use _creationTime in place of Better Auth's createdAt
129
+ const indexFields = indexEqFields
130
+ .map(([field]) => field)
131
+ .concat(
132
+ boundField && boundField !== "createdAt"
133
+ ? `${indexEqFields.length ? "_" : ""}${boundField}`
134
+ : ""
135
+ )
136
+ .concat(
137
+ sortField && sortField !== "createdAt" && boundField !== sortField
138
+ ? `${indexEqFields.length || boundField ? "_" : ""}${sortField}`
139
+ : ""
140
+ )
141
+ .filter(Boolean)
142
+ if (!indexFields.length && !boundField && !sortField) {
143
+ return
144
+ }
145
+ // Use the built in _creationTime index if bounding or sorting by createdAt
146
+ // with no other fields
147
+ const index = !indexFields.length
148
+ ? {
149
+ fields: [],
150
+ indexDescriptor: "by_creation_time",
151
+ }
152
+ : indexes.find(({ fields }: { fields: string[] }) => {
153
+ const fieldsMatch = indexFields.every((field, idx) => field === fields[idx])
154
+ // If sorting by createdAt, no intermediate fields can be on the index
155
+ // as they may override the createdAt sort order.
156
+ const boundFieldMatch =
157
+ boundField === "createdAt" || sortField === "createdAt"
158
+ ? indexFields.length === fields.length
159
+ : true
160
+ return fieldsMatch && boundFieldMatch
161
+ })
162
+ if (!index) {
163
+ return { indexFields }
164
+ }
165
+ return {
166
+ boundField,
167
+ index: {
168
+ fields: [...index.fields, "_creationTime"],
169
+ indexDescriptor: index.indexDescriptor,
170
+ },
171
+ sortField,
172
+ values: {
173
+ eq: indexEqFields.map(([, value]) => value),
174
+ gt: upperBound?.operator === "gt" ? upperBound.value : undefined,
175
+ gte: upperBound?.operator === "gte" ? upperBound.value : undefined,
176
+ lt: lowerBound?.operator === "lt" ? lowerBound.value : undefined,
177
+ lte: lowerBound?.operator === "lte" ? lowerBound.value : undefined,
178
+ },
179
+ }
180
+ }
181
+
182
+ export const checkUniqueFields = async <Schema extends SchemaDefinition<any, any>>(
183
+ ctx: GenericQueryCtx<GenericDataModel>,
184
+ schema: Schema,
185
+ betterAuthSchema: BetterAuthDBSchema,
186
+ table: string,
187
+ input: Record<string, any>,
188
+ doc?: Record<string, any>
189
+ ) => {
190
+ if (!hasUniqueFields(betterAuthSchema, table, input)) {
191
+ return
192
+ }
193
+ for (const field of Object.keys(input)) {
194
+ if (!isUniqueField(betterAuthSchema, table, field)) {
195
+ continue
196
+ }
197
+ const { index } =
198
+ findIndex(schema, {
199
+ model: table,
200
+ where: [{ field, operator: "eq", value: input[field] }],
201
+ }) ?? {}
202
+ if (!index) {
203
+ throw new Error(`No index found for ${table}.${field}`)
204
+ }
205
+ const existingDoc = await ctx.db
206
+ .query(table as any)
207
+ .withIndex(index.indexDescriptor, (q) => q.eq(field, input[field]))
208
+ .unique()
209
+ if (existingDoc && existingDoc._id !== doc?._id) {
210
+ throw new Error(`${table} ${field} already exists`)
211
+ }
212
+ }
213
+ }
214
+
215
+ export const selectFields = <
216
+ T extends TableNamesInDataModel<GenericDataModel>,
217
+ D extends DocumentByName<GenericDataModel, T>,
218
+ >(
219
+ doc: D | null,
220
+ select?: string[]
221
+ ): D | null => {
222
+ if (!doc) {
223
+ return null
224
+ }
225
+ if (!select?.length) {
226
+ return doc
227
+ }
228
+ return select.reduce((acc, field) => {
229
+ ;(acc as any)[field] = doc[field]
230
+ return acc
231
+ }, {} as D)
232
+ }
233
+
234
+ export const filterByWhere = <
235
+ T extends TableNamesInDataModel<GenericDataModel>,
236
+ D extends DocumentByName<GenericDataModel, T>,
237
+ >(
238
+ doc: D | null,
239
+ where?: WhereClause[],
240
+ // Optionally filter which where clauses to apply.
241
+ filterWhere?: (w: WhereClause) => any
242
+ ): boolean => {
243
+ if (!doc) {
244
+ return false
245
+ }
246
+ for (const w of where ?? []) {
247
+ if (filterWhere && !filterWhere(w)) {
248
+ continue
249
+ }
250
+ const value = doc[w.field as keyof typeof doc] as WhereClause["value"]
251
+ const isLessThan = (val: typeof value, wVal: typeof w.value) => {
252
+ if (!wVal) {
253
+ return false
254
+ }
255
+ if (!val) {
256
+ return true
257
+ }
258
+ return val < wVal
259
+ }
260
+ const isGreaterThan = (val: typeof value, wVal: typeof w.value) => {
261
+ if (!val) {
262
+ return false
263
+ }
264
+ if (!wVal) {
265
+ return true
266
+ }
267
+ return val > wVal
268
+ }
269
+ const filter = (w: WhereClause) => {
270
+ switch (w.operator) {
271
+ case "contains": {
272
+ return typeof value === "string" && value.includes(w.value as string)
273
+ }
274
+ case "ends_with": {
275
+ return typeof value === "string" && value.endsWith(w.value as string)
276
+ }
277
+ case "eq":
278
+ case undefined: {
279
+ return value === w.value
280
+ }
281
+ case "gt": {
282
+ return isGreaterThan(value, w.value)
283
+ }
284
+ case "gte": {
285
+ return value === w.value || isGreaterThan(value, w.value)
286
+ }
287
+ case "in": {
288
+ return Array.isArray(w.value) && (w.value as any[]).includes(value)
289
+ }
290
+ case "lt": {
291
+ return isLessThan(value, w.value)
292
+ }
293
+ case "lte": {
294
+ return value === w.value || isLessThan(value, w.value)
295
+ }
296
+ case "ne": {
297
+ return value !== w.value
298
+ }
299
+ case "not_in": {
300
+ return Array.isArray(w.value) && !(w.value as any[]).includes(value)
301
+ }
302
+ case "starts_with": {
303
+ return typeof value === "string" && value.startsWith(w.value as string)
304
+ }
305
+ }
306
+ }
307
+ if (!filter(w)) {
308
+ return false
309
+ }
310
+ }
311
+ return true
312
+ }
313
+
314
+ const generateQuery = (
315
+ ctx: GenericQueryCtx<GenericDataModel>,
316
+ schema: SchemaDefinition<any, any>,
317
+ args: QueryArgs
318
+ ) => {
319
+ const { boundField, index, indexFields, values } = findIndex(schema, args) ?? {}
320
+ const query = stream(ctx.db as any, schema).query(args.model as any)
321
+ const hasValues = values?.eq?.length ?? values?.lt ?? values?.lte ?? values?.gt ?? values?.gte
322
+ const indexedQuery =
323
+ index && index.indexDescriptor !== "by_creation_time"
324
+ ? query.withIndex(
325
+ index.indexDescriptor,
326
+ hasValues
327
+ ? (q: any) => {
328
+ for (const [idx, value] of (values?.eq ?? []).entries()) {
329
+ q = q.eq(index.fields[idx], value)
330
+ }
331
+ if (values?.lt) {
332
+ q = q.lt(boundField, values.lt)
333
+ }
334
+ if (values?.lte) {
335
+ q = q.lte(boundField, values.lte)
336
+ }
337
+ if (values?.gt) {
338
+ q = q.gt(boundField, values.gt)
339
+ }
340
+ if (values?.gte) {
341
+ q = q.gte(boundField, values.gte)
342
+ }
343
+ return q
344
+ }
345
+ : undefined
346
+ )
347
+ : query
348
+ const orderedQuery = args.sortBy
349
+ ? indexedQuery.order(args.sortBy.direction === "desc" ? "desc" : "asc")
350
+ : indexedQuery
351
+ const filteredQuery = orderedQuery.filterWith(async (doc) => {
352
+ if (!index && indexFields?.length) {
353
+ console.warn(
354
+ `Querying without an index on table "${args.model}".\n` +
355
+ `This can cause performance issues, and may hit the document read limit.\n` +
356
+ `To fix, add an index that begins with the following fields in order:\n` +
357
+ `[${indexFields.join(", ")}]`
358
+ )
359
+ // No index, handle all where clauses statically.
360
+ return filterByWhere(doc, args.where)
361
+ }
362
+ return filterByWhere(
363
+ doc,
364
+ args.where,
365
+ // Index used for all eq and range clauses, apply remaining clauses
366
+ // incompatible with Convex statically.
367
+ (w) =>
368
+ w.operator && ["contains", "ends_with", "ne", "not_in", "starts_with"].includes(w.operator)
369
+ )
370
+ })
371
+ return filteredQuery
372
+ }
373
+
374
+ export const paginate = async <
375
+ Doc extends DocumentByName<GenericDataModel, T>,
376
+ T extends TableNamesInDataModel<GenericDataModel>,
377
+ >(
378
+ ctx: GenericQueryCtx<GenericDataModel>,
379
+ schema: SchemaDefinition<any, any>,
380
+ betterAuthSchema: BetterAuthDBSchema,
381
+ args: QueryArgs & {
382
+ paginationOpts: PaginationOptions
383
+ }
384
+ ): Promise<PaginationResult<Doc>> => {
385
+ if (args.offset) {
386
+ throw new Error(`offset not supported: ${JSON.stringify(args.offset)}`)
387
+ }
388
+ if (args.where?.some((w) => w.connector === "OR") && args.where?.length > 1) {
389
+ throw new Error(
390
+ `OR connector not supported with multiple where statements in paginate, split up the where statements before calling paginate: ${JSON.stringify(args.where)}`
391
+ )
392
+ }
393
+ if (
394
+ args.where?.some(
395
+ (w) => w.field === "_id" && w.operator && !["eq", "in", "not_in"].includes(w.operator)
396
+ )
397
+ ) {
398
+ throw new Error(
399
+ `_id can only be used with eq, in, or not_in operator: ${JSON.stringify(args.where)}`
400
+ )
401
+ }
402
+ // If any where clause is "eq" (or missing operator) on a unique field,
403
+ // we can only return a single document, so we get it and use any other
404
+ // where clauses as static filters.
405
+ const uniqueWhere = args.where?.find(
406
+ (w) =>
407
+ (!w.operator || w.operator === "eq") &&
408
+ (isUniqueField(betterAuthSchema, args.model, w.field) || w.field === "_id")
409
+ )
410
+ if (uniqueWhere) {
411
+ const { index } =
412
+ findIndex(schema, {
413
+ model: args.model,
414
+ where: [uniqueWhere],
415
+ }) ?? {}
416
+
417
+ let doc
418
+ if (uniqueWhere.field === "_id") {
419
+ doc = await ctx.db.get(uniqueWhere.value as GenericId<T>)
420
+ } else if (index?.indexDescriptor) {
421
+ doc = await ctx.db
422
+ .query(args.model as any)
423
+ .withIndex(index.indexDescriptor, (q) => q.eq(index.fields[0], uniqueWhere.value))
424
+ .unique()
425
+ } else {
426
+ // No index found - fall back to collect() for unique field lookup
427
+ const results = await ctx.db.query(args.model as any).collect()
428
+ doc = results.find((d: any) => d[uniqueWhere.field] === uniqueWhere.value) ?? null
429
+ }
430
+
431
+ // Apply all other clauses as static filters to our 0 or 1 result.
432
+ if (filterByWhere(doc, args.where, (w) => w !== uniqueWhere)) {
433
+ return {
434
+ continueCursor: "",
435
+ isDone: true,
436
+ page: [selectFields(doc, args.select)].filter(Boolean) as Doc[],
437
+ }
438
+ }
439
+ return {
440
+ continueCursor: "",
441
+ isDone: true,
442
+ page: [],
443
+ }
444
+ }
445
+
446
+ const paginationOpts = {
447
+ ...args.paginationOpts,
448
+ // If maximumRowsRead is not at least 1 higher than numItems, bad cursors
449
+ // and incorrect paging will result (at least with convex-test).
450
+ maximumRowsRead: Math.max((args.paginationOpts.numItems ?? 0) + 1, 200),
451
+ }
452
+
453
+ // Large queries using "in" clause will crash, but these are only currently
454
+ // possible with the organization plugin listing all members with a high
455
+ // limit. For cases like this we need to create proper convex queries in
456
+ // the component as an alternative to using Better Auth api's.
457
+ const inWhere = args.where?.find((w) => w.operator === "in")
458
+ if (inWhere) {
459
+ if (!Array.isArray(inWhere.value)) {
460
+ throw new Error("in clause value must be an array")
461
+ }
462
+ // For ids, just use asyncMap + .get()
463
+ if (inWhere.field === "_id") {
464
+ const docs = await asyncMap(inWhere.value as any[], async (value) => {
465
+ return ctx.db.get(value as GenericId<T>)
466
+ })
467
+ const filteredDocs = docs
468
+ .flatMap((doc) => (doc ? [doc] : []))
469
+ .filter((doc) => filterByWhere(doc, args.where, (w) => w !== inWhere))
470
+
471
+ return {
472
+ continueCursor: "",
473
+ isDone: true,
474
+ page: filteredDocs.sort((a, b) => {
475
+ if (args.sortBy?.field === "createdAt") {
476
+ return args.sortBy.direction === "asc"
477
+ ? (a._creationTime as number) - (b._creationTime as number)
478
+ : (b._creationTime as number) - (a._creationTime as number)
479
+ }
480
+ if (args.sortBy) {
481
+ const aValue = a[args.sortBy.field]
482
+ const bValue = b[args.sortBy.field]
483
+ if (aValue === bValue) {
484
+ return 0
485
+ }
486
+ return args.sortBy.direction === "asc"
487
+ ? aValue! > bValue!
488
+ ? 1
489
+ : -1
490
+ : aValue! > bValue!
491
+ ? -1
492
+ : 1
493
+ }
494
+ return 0
495
+ }) as Doc[],
496
+ }
497
+ }
498
+ const streams = inWhere.value.map((value) => {
499
+ return generateQuery(ctx, schema, {
500
+ ...args,
501
+ where: args.where?.map((w) => {
502
+ if (w === inWhere) {
503
+ return { ...w, operator: "eq" as const, value }
504
+ }
505
+ return w
506
+ }),
507
+ })
508
+ })
509
+ const result = await mergedStream(
510
+ streams,
511
+ [args.sortBy?.field !== "createdAt" && args.sortBy?.field, "_creationTime"].flatMap((f) =>
512
+ f ? [f] : []
513
+ )
514
+ ).paginate(paginationOpts)
515
+ return {
516
+ ...result,
517
+ page: await asyncMap(result.page, (doc) => selectFields(doc, args.select)),
518
+ }
519
+ }
520
+
521
+ const query = generateQuery(ctx, schema, args)
522
+ const result = await query.paginate(paginationOpts)
523
+ return {
524
+ ...result,
525
+ page: await asyncMap(result.page, (doc) => selectFields(doc, args.select)),
526
+ }
527
+ }
528
+
529
+ export const listOne = async <
530
+ Doc extends DocumentByName<GenericDataModel, T>,
531
+ T extends TableNamesInDataModel<GenericDataModel>,
532
+ >(
533
+ ctx: GenericQueryCtx<GenericDataModel>,
534
+ schema: SchemaDefinition<any, any>,
535
+ betterAuthSchema: BetterAuthDBSchema,
536
+ args: QueryArgs
537
+ ): Promise<Doc | null> => {
538
+ return (
539
+ await paginate(ctx, schema, betterAuthSchema, {
540
+ ...args,
541
+ paginationOpts: {
542
+ cursor: null,
543
+ numItems: 1,
544
+ },
545
+ })
546
+ ).page[0] as Doc | null
547
+ }
@@ -0,0 +1,8 @@
1
+ import { query } from "../_generated/server"
2
+
3
+ export const identifyCurrentUser = query({
4
+ args: {},
5
+ handler: async (ctx) => {
6
+ return ctx.auth.getUserIdentity()
7
+ },
8
+ })
@@ -0,0 +1,10 @@
1
+ const config = {
2
+ providers: [
3
+ {
4
+ applicationID: "convex",
5
+ domain: process.env.CONVEX_SITE_URL,
6
+ },
7
+ ],
8
+ }
9
+
10
+ export default config