lazyconvex 0.0.0

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 (70) hide show
  1. package/README.md +926 -0
  2. package/dist/components/index.mjs +937 -0
  3. package/dist/error-D4GuI0ot.mjs +71 -0
  4. package/dist/file-field-BqVgy8xY.mjs +205 -0
  5. package/dist/form-BXJK_j10.d.mts +99 -0
  6. package/dist/index.d.mts +433 -0
  7. package/dist/index.mjs +1 -0
  8. package/dist/index2.d.mts +5 -0
  9. package/dist/index3.d.mts +35 -0
  10. package/dist/index4.d.mts +101 -0
  11. package/dist/index5.d.mts +842 -0
  12. package/dist/next/index.mjs +151 -0
  13. package/dist/org-CmJBb8z-.d.mts +56 -0
  14. package/dist/react/index.mjs +158 -0
  15. package/dist/retry.d.mts +12 -0
  16. package/dist/retry.mjs +35 -0
  17. package/dist/schema.d.mts +23 -0
  18. package/dist/schema.mjs +15 -0
  19. package/dist/server/index.mjs +2572 -0
  20. package/dist/types-DWBVRtit.d.mts +322 -0
  21. package/dist/use-online-status-CMr73Jlk.mjs +155 -0
  22. package/dist/use-upload-DtELytQi.mjs +95 -0
  23. package/dist/zod.d.mts +18 -0
  24. package/dist/zod.mjs +87 -0
  25. package/package.json +40 -0
  26. package/src/components/editors-section.tsx +86 -0
  27. package/src/components/fields.tsx +884 -0
  28. package/src/components/file-field.tsx +234 -0
  29. package/src/components/form.tsx +191 -0
  30. package/src/components/index.ts +11 -0
  31. package/src/components/offline-indicator.tsx +15 -0
  32. package/src/components/org-avatar.tsx +13 -0
  33. package/src/components/permission-guard.tsx +36 -0
  34. package/src/components/role-badge.tsx +14 -0
  35. package/src/components/suspense-wrap.tsx +8 -0
  36. package/src/index.ts +40 -0
  37. package/src/next/active-org.ts +33 -0
  38. package/src/next/auth.ts +9 -0
  39. package/src/next/image.ts +134 -0
  40. package/src/next/index.ts +3 -0
  41. package/src/react/form-meta.ts +53 -0
  42. package/src/react/form.ts +201 -0
  43. package/src/react/index.ts +8 -0
  44. package/src/react/org.tsx +96 -0
  45. package/src/react/use-active-org.ts +48 -0
  46. package/src/react/use-bulk-selection.ts +47 -0
  47. package/src/react/use-online-status.ts +21 -0
  48. package/src/react/use-optimistic.ts +54 -0
  49. package/src/react/use-upload.ts +101 -0
  50. package/src/retry.ts +47 -0
  51. package/src/schema.ts +30 -0
  52. package/src/server/cache-crud.ts +175 -0
  53. package/src/server/check-schema.ts +29 -0
  54. package/src/server/child.ts +98 -0
  55. package/src/server/crud.ts +384 -0
  56. package/src/server/db.ts +7 -0
  57. package/src/server/error.ts +39 -0
  58. package/src/server/file.ts +372 -0
  59. package/src/server/helpers.ts +214 -0
  60. package/src/server/index.ts +12 -0
  61. package/src/server/org-crud.ts +307 -0
  62. package/src/server/org-helpers.ts +54 -0
  63. package/src/server/org.ts +572 -0
  64. package/src/server/schema-helpers.ts +107 -0
  65. package/src/server/setup.ts +138 -0
  66. package/src/server/test-crud.ts +211 -0
  67. package/src/server/test.ts +554 -0
  68. package/src/server/types.ts +392 -0
  69. package/src/server/unique.ts +28 -0
  70. package/src/zod.ts +141 -0
@@ -0,0 +1,138 @@
1
+ /* eslint-disable @typescript-eslint/no-unnecessary-type-parameters */
2
+ import type { GenericDataModel, GenericMutationCtx, GenericQueryCtx } from 'convex/server'
3
+ import type { ZodObject, ZodRawShape } from 'zod/v4'
4
+
5
+ import { customCtx } from 'convex-helpers/server/customFunctions'
6
+ import { zCustomMutation, zCustomQuery } from 'convex-helpers/server/zod4'
7
+
8
+ import type { OrgCrudOptions } from './org-crud'
9
+ import type { CrudOptions, DbLike, Mb, Qb, Rec, SetupConfig } from './types'
10
+
11
+ import { makeCacheCrud } from './cache-crud'
12
+ import { makeChildCrud } from './child'
13
+ import { makeCrud } from './crud'
14
+ import { dbInsert, dbPatch } from './db'
15
+ import { err, getUser, ownGet, readCtx, time } from './helpers'
16
+ import { makeOrg } from './org'
17
+ import { makeOrgCrud } from './org-crud'
18
+ import { makeUnique } from './unique'
19
+
20
+ const setup = <DM extends GenericDataModel>(config: SetupConfig<DM>) => {
21
+ type QCtx = GenericQueryCtx<DM>
22
+ type MCtx = GenericMutationCtx<DM>
23
+ const { getAuthUserId } = config,
24
+ authId = async (c: unknown) => getAuthUserId(c as never),
25
+ asDb = (c: { db: unknown }) => c.db as never as DbLike,
26
+ pq = zCustomQuery(
27
+ config.query,
28
+ customCtx(async (c: QCtx) => {
29
+ const vid = await authId(c),
30
+ { withAuthor } = readCtx({ db: asDb(c), storage: c.storage as never, viewerId: vid })
31
+ return { viewerId: vid, withAuthor }
32
+ })
33
+ ),
34
+ q = zCustomQuery(
35
+ config.query,
36
+ customCtx(async (c: QCtx) => {
37
+ const db = asDb(c),
38
+ user = await getUser({ ctx: c as never, db, getAuthUserId }),
39
+ { viewerId, withAuthor } = readCtx({ db, storage: c.storage as never, viewerId: user._id })
40
+ return {
41
+ get: ownGet(db, user._id),
42
+ user,
43
+ viewerId,
44
+ withAuthor
45
+ }
46
+ })
47
+ ),
48
+ m = zCustomMutation(
49
+ config.mutation,
50
+ customCtx(async (c: MCtx) => {
51
+ const db = asDb(c),
52
+ now = time(),
53
+ user = await getUser({ ctx: c as never, db, getAuthUserId }),
54
+ get = ownGet(db, user._id)
55
+ return {
56
+ create: async (t: string, d: Rec) => dbInsert(db, t, { ...d, ...now, userId: user._id }),
57
+ delete: async (id: string) => {
58
+ const d = await get(id)
59
+ await db.delete(id)
60
+ return d
61
+ },
62
+ get,
63
+ patch: async (
64
+ id: string,
65
+ data: ((doc: Rec) => Partial<Rec> | Promise<Partial<Rec>>) | Partial<Rec>,
66
+ expectedUpdatedAt?: number
67
+ ) => {
68
+ const doc = await get(id)
69
+ if (expectedUpdatedAt !== undefined && doc.updatedAt !== expectedUpdatedAt) return err('CONFLICT')
70
+ const up = typeof data === 'function' ? await data(doc) : data
71
+ await dbPatch(db, id, { ...up, ...now })
72
+ return { ...doc, ...up, ...now }
73
+ },
74
+ user
75
+ }
76
+ })
77
+ ),
78
+ cq = zCustomQuery(
79
+ config.query,
80
+ customCtx(() => ({}))
81
+ ),
82
+ cm = zCustomMutation(
83
+ config.mutation,
84
+ customCtx(() => ({}))
85
+ ),
86
+ children = config.children ?? {},
87
+ crud = <S extends ZodRawShape>(table: string, schema: ZodObject<S>, opt?: CrudOptions<S>) =>
88
+ makeCrud({
89
+ builders: { children, cm, cq, m: m as never as Mb, pq: pq as never as Qb, q: q as never as Qb },
90
+ options: opt,
91
+ schema,
92
+ table
93
+ }),
94
+ childCrud = <S extends ZodRawShape>(
95
+ table: string,
96
+ meta: { foreignKey: string; index: string; parent: string; schema: ZodObject<S> }
97
+ ) => makeChildCrud({ builders: { m: m as never as Mb, q: q as never as Qb }, meta, table }),
98
+ orgCrud = <S extends ZodRawShape>(table: string, schema: ZodObject<S>, opt?: Rec) =>
99
+ makeOrgCrud({
100
+ builders: { m: m as never as Mb, q: q as never as Qb },
101
+ options: opt as OrgCrudOptions | undefined,
102
+ schema,
103
+ table
104
+ }),
105
+ cacheCrud = <S extends ZodRawShape, K extends keyof Rec>(opts: {
106
+ fetcher?: (c: unknown, key: unknown) => Promise<unknown>
107
+ key: K
108
+ schema: ZodObject<S>
109
+ table: string
110
+ ttl?: number
111
+ }) =>
112
+ makeCacheCrud({
113
+ ...opts,
114
+ builders: {
115
+ action: config.action,
116
+ cm,
117
+ cq,
118
+ internalMutation: config.internalMutation,
119
+ internalQuery: config.internalQuery,
120
+ mutation: config.mutation,
121
+ query: config.query
122
+ }
123
+ }),
124
+ uniqueCheck = (table: string, field: string) => makeUnique({ field, pq: pq as never as Qb, table }),
125
+ org = config.orgSchema
126
+ ? makeOrg({
127
+ cascadeTables: config.orgCascadeTables,
128
+ getAuthUserId: config.getAuthUserId,
129
+ mutation: config.mutation,
130
+ query: config.query,
131
+ schema: config.orgSchema
132
+ })
133
+ : undefined,
134
+ user = { me: q({ handler: (c: Rec) => c.user }) }
135
+ return { cacheCrud, childCrud, cm, cq, crud, m, org, orgCrud, pq, q, uniqueCheck, user }
136
+ }
137
+
138
+ export { setup }
@@ -0,0 +1,211 @@
1
+ /* eslint-disable no-await-in-loop, max-depth, max-statements, @typescript-eslint/max-params, @typescript-eslint/prefer-nullish-coalescing */
2
+ /** biome-ignore-all lint/performance/noAwaitInLoops: sequential deletes */
3
+ import type { GenericDataModel, MutationBuilder, QueryBuilder } from 'convex/server'
4
+
5
+ import { v } from 'convex/values'
6
+
7
+ import type { DbLike, IndexLike, Rec } from './types'
8
+
9
+ import { isTestMode } from './test'
10
+
11
+ interface OrgTestCrudConfig<DM extends GenericDataModel = GenericDataModel> {
12
+ acl?: boolean
13
+ aclFrom?: { field: string; table: string }
14
+ cascade?: { foreignKey: string; table: string }[]
15
+ mutation: MutationBuilder<DM, 'public'>
16
+ query: QueryBuilder<DM, 'public'>
17
+ table: string
18
+ }
19
+
20
+ const getOrgMembership = async (db: DbLike, orgId: string, userId: string) => {
21
+ const orgDoc = await db.get(orgId)
22
+ if (!orgDoc) return null
23
+ const isOwner = orgDoc.userId === userId,
24
+ member = await db
25
+ .query('orgMember')
26
+ .withIndex('by_org_user', ((q: IndexLike) => q.eq('orgId', orgId).eq('userId', userId)) as never)
27
+ .unique()
28
+ if (!(isOwner || member)) return null
29
+ return { isAdmin: isOwner || member?.isAdmin === true, isOwner, member, orgDoc }
30
+ },
31
+ checkAclPermission = (doc: Rec, userId: string, membership: { isAdmin: boolean }) => {
32
+ const isCreator = doc.userId === userId,
33
+ editors = (doc.editors ?? []) as string[],
34
+ isEditor = editors.includes(userId)
35
+ return isCreator || membership.isAdmin || isEditor
36
+ },
37
+ checkChildAclPermission = async (
38
+ db: DbLike,
39
+ doc: Rec,
40
+ parentField: string,
41
+ userId: string,
42
+ membership: { isAdmin: boolean }
43
+ ) => {
44
+ const isCreator = doc.userId === userId
45
+ if (isCreator || membership.isAdmin) return true
46
+ const parentId = doc[parentField] as string,
47
+ parent = parentId ? await db.get(parentId) : null,
48
+ editors = parent ? ((parent.editors ?? []) as string[]) : []
49
+ return editors.some(eid => eid === userId)
50
+ },
51
+ addEditorToDoc = async (db: DbLike, itemId: string, editorId: string, orgId: string) => {
52
+ const doc = await db.get(itemId)
53
+ if (doc?.orgId !== orgId) return { code: 'NOT_FOUND' }
54
+ const editors = (doc.editors ?? []) as string[],
55
+ alreadyEditor = editors.some((id: string) => id === editorId)
56
+ if (alreadyEditor) return doc
57
+ await db.patch(itemId, { editors: [...editors, editorId], updatedAt: Date.now() })
58
+ return db.get(itemId)
59
+ },
60
+ removeEditorFromDoc = async (db: DbLike, itemId: string, editorId: string, orgId: string) => {
61
+ const doc = await db.get(itemId)
62
+ if (doc?.orgId !== orgId) return { code: 'NOT_FOUND' }
63
+ const editors = (doc.editors ?? []) as string[],
64
+ filtered: string[] = []
65
+ for (const id of editors) if (id !== editorId) filtered.push(id)
66
+ await db.patch(itemId, { editors: filtered, updatedAt: Date.now() })
67
+ return db.get(itemId)
68
+ },
69
+ makeOrgTestCrud = <DM extends GenericDataModel>(config: OrgTestCrudConfig<DM>) => {
70
+ const { acl, aclFrom, cascade, mutation: rawMut, query: rawQry, table } = config,
71
+ mutation = rawMut as unknown as (opts: Rec) => Rec,
72
+ query = rawQry as unknown as (opts: Rec) => Rec,
73
+ hasAcl = acl || Boolean(aclFrom),
74
+ createAsUser = mutation({
75
+ /** biome-ignore lint/suspicious/noExplicitAny: test generic */
76
+ args: { data: v.any(), orgId: v.id('org'), userId: v.id('users') },
77
+ handler: async (ctx: { db: DbLike }, { data, orgId, userId }: { data: Rec; orgId: string; userId: string }) => {
78
+ if (!isTestMode()) return null
79
+ const membership = await getOrgMembership(ctx.db, orgId, userId)
80
+ if (!membership) return { code: 'NOT_ORG_MEMBER' }
81
+ return ctx.db.insert(table, { ...data, orgId, updatedAt: Date.now(), userId })
82
+ }
83
+ }),
84
+ updateAsUser = mutation({
85
+ /** biome-ignore lint/suspicious/noExplicitAny: test generic */
86
+ args: { data: v.any(), id: v.string(), orgId: v.id('org'), userId: v.id('users') },
87
+ handler: async (
88
+ ctx: { db: DbLike },
89
+ { data, id, orgId, userId }: { data: Rec; id: string; orgId: string; userId: string }
90
+ ) => {
91
+ if (!isTestMode()) return null
92
+ const doc = await ctx.db.get(id)
93
+ if (doc?.orgId !== orgId) return { code: 'NOT_FOUND' }
94
+ const membership = await getOrgMembership(ctx.db, orgId, userId)
95
+ if (!membership) return { code: 'NOT_ORG_MEMBER' }
96
+ if (hasAcl) {
97
+ const permitted = aclFrom
98
+ ? await checkChildAclPermission(ctx.db, doc, aclFrom.field, userId, membership)
99
+ : checkAclPermission(doc, userId, membership)
100
+ if (!permitted) return { code: 'FORBIDDEN' }
101
+ }
102
+ await ctx.db.patch(id, { ...data, updatedAt: Date.now() })
103
+ return { success: true }
104
+ }
105
+ }),
106
+ rmAsUser = mutation({
107
+ args: { id: v.string(), orgId: v.id('org'), userId: v.id('users') },
108
+ handler: async (ctx: { db: DbLike }, { id, orgId, userId }: { id: string; orgId: string; userId: string }) => {
109
+ if (!isTestMode()) return null
110
+ const doc = await ctx.db.get(id)
111
+ if (doc?.orgId !== orgId) return { code: 'NOT_FOUND' }
112
+ const membership = await getOrgMembership(ctx.db, orgId, userId)
113
+ if (!membership) return { code: 'NOT_ORG_MEMBER' }
114
+ if (hasAcl) {
115
+ const permitted = aclFrom
116
+ ? await checkChildAclPermission(ctx.db, doc, aclFrom.field, userId, membership)
117
+ : checkAclPermission(doc, userId, membership)
118
+ if (!permitted) return { code: 'FORBIDDEN' }
119
+ }
120
+ if (cascade)
121
+ for (const { foreignKey, table: childTable } of cascade) {
122
+ const children = await ctx.db
123
+ .query(childTable)
124
+ .withIndex('by_parent', ((q: IndexLike) => q.eq(foreignKey, id)) as never)
125
+ .collect()
126
+ for (const c of children) await ctx.db.delete(c._id as string)
127
+ }
128
+
129
+ await ctx.db.delete(id)
130
+ return { success: true }
131
+ }
132
+ }),
133
+ bulkRmAsUser = mutation({
134
+ args: { ids: v.array(v.string()), orgId: v.id('org'), userId: v.id('users') },
135
+ handler: async (ctx: { db: DbLike }, { ids, orgId, userId }: { ids: string[]; orgId: string; userId: string }) => {
136
+ if (!isTestMode()) return null
137
+ const orgDoc = await ctx.db.get(orgId)
138
+ if (!orgDoc) return { code: 'NOT_FOUND' }
139
+ const isOwner = orgDoc.userId === userId,
140
+ member = await ctx.db
141
+ .query('orgMember')
142
+ .withIndex('by_org_user', ((q: IndexLike) => q.eq('orgId', orgId).eq('userId', userId)) as never)
143
+ .unique()
144
+ if (!(isOwner || member)) return { code: 'NOT_ORG_MEMBER' }
145
+ if (!(isOwner || member?.isAdmin)) return { code: 'INSUFFICIENT_ORG_ROLE' }
146
+ let count = 0
147
+ for (const id of ids) {
148
+ const doc = await ctx.db.get(id)
149
+ if (doc?.orgId === orgId) {
150
+ if (cascade)
151
+ for (const { foreignKey, table: childTable } of cascade) {
152
+ const children = await ctx.db
153
+ .query(childTable)
154
+ .withIndex('by_parent', ((q: IndexLike) => q.eq(foreignKey, id)) as never)
155
+ .collect()
156
+ for (const c of children) await ctx.db.delete(c._id as string)
157
+ }
158
+
159
+ await ctx.db.delete(id)
160
+ count += 1
161
+ }
162
+ }
163
+ return { count }
164
+ }
165
+ }),
166
+ listAsUser = query({
167
+ args: { orgId: v.id('org'), userId: v.id('users') },
168
+ handler: async (ctx: { db: DbLike }, { orgId, userId }: { orgId: string; userId: string }) => {
169
+ if (!isTestMode()) return null
170
+ const membership = await getOrgMembership(ctx.db, orgId, userId)
171
+ if (!membership) return { code: 'NOT_ORG_MEMBER' }
172
+ return ctx.db
173
+ .query(table)
174
+ .withIndex('by_org', ((q: IndexLike) => q.eq('orgId', orgId)) as never)
175
+ .collect()
176
+ }
177
+ }),
178
+ result: Rec = { bulkRmAsUser, createAsUser, listAsUser, rmAsUser, updateAsUser }
179
+ if (hasAcl) {
180
+ result.addEditorAsUser = mutation({
181
+ args: { editorId: v.id('users'), itemId: v.string(), orgId: v.id('org'), userId: v.id('users') },
182
+ handler: async (
183
+ ctx: { db: DbLike },
184
+ { editorId, itemId, orgId, userId }: { editorId: string; itemId: string; orgId: string; userId: string }
185
+ ) => {
186
+ if (!isTestMode()) return null
187
+ const membership = await getOrgMembership(ctx.db, orgId, userId)
188
+ if (!membership) return { code: 'NOT_ORG_MEMBER' }
189
+ if (!membership.isAdmin) return { code: 'INSUFFICIENT_ORG_ROLE' }
190
+ return addEditorToDoc(ctx.db, itemId, editorId, orgId)
191
+ }
192
+ })
193
+ result.removeEditorAsUser = mutation({
194
+ args: { editorId: v.id('users'), itemId: v.string(), orgId: v.id('org'), userId: v.id('users') },
195
+ handler: async (
196
+ ctx: { db: DbLike },
197
+ { editorId, itemId, orgId, userId }: { editorId: string; itemId: string; orgId: string; userId: string }
198
+ ) => {
199
+ if (!isTestMode()) return null
200
+ const membership = await getOrgMembership(ctx.db, orgId, userId)
201
+ if (!membership) return { code: 'NOT_ORG_MEMBER' }
202
+ if (!membership.isAdmin) return { code: 'INSUFFICIENT_ORG_ROLE' }
203
+ return removeEditorFromDoc(ctx.db, itemId, editorId, orgId)
204
+ }
205
+ })
206
+ }
207
+ return result
208
+ }
209
+
210
+ export type { OrgTestCrudConfig }
211
+ export { getOrgMembership, makeOrgTestCrud }