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.
- package/README.md +926 -0
- package/dist/components/index.mjs +937 -0
- package/dist/error-D4GuI0ot.mjs +71 -0
- package/dist/file-field-BqVgy8xY.mjs +205 -0
- package/dist/form-BXJK_j10.d.mts +99 -0
- package/dist/index.d.mts +433 -0
- package/dist/index.mjs +1 -0
- package/dist/index2.d.mts +5 -0
- package/dist/index3.d.mts +35 -0
- package/dist/index4.d.mts +101 -0
- package/dist/index5.d.mts +842 -0
- package/dist/next/index.mjs +151 -0
- package/dist/org-CmJBb8z-.d.mts +56 -0
- package/dist/react/index.mjs +158 -0
- package/dist/retry.d.mts +12 -0
- package/dist/retry.mjs +35 -0
- package/dist/schema.d.mts +23 -0
- package/dist/schema.mjs +15 -0
- package/dist/server/index.mjs +2572 -0
- package/dist/types-DWBVRtit.d.mts +322 -0
- package/dist/use-online-status-CMr73Jlk.mjs +155 -0
- package/dist/use-upload-DtELytQi.mjs +95 -0
- package/dist/zod.d.mts +18 -0
- package/dist/zod.mjs +87 -0
- package/package.json +40 -0
- package/src/components/editors-section.tsx +86 -0
- package/src/components/fields.tsx +884 -0
- package/src/components/file-field.tsx +234 -0
- package/src/components/form.tsx +191 -0
- package/src/components/index.ts +11 -0
- package/src/components/offline-indicator.tsx +15 -0
- package/src/components/org-avatar.tsx +13 -0
- package/src/components/permission-guard.tsx +36 -0
- package/src/components/role-badge.tsx +14 -0
- package/src/components/suspense-wrap.tsx +8 -0
- package/src/index.ts +40 -0
- package/src/next/active-org.ts +33 -0
- package/src/next/auth.ts +9 -0
- package/src/next/image.ts +134 -0
- package/src/next/index.ts +3 -0
- package/src/react/form-meta.ts +53 -0
- package/src/react/form.ts +201 -0
- package/src/react/index.ts +8 -0
- package/src/react/org.tsx +96 -0
- package/src/react/use-active-org.ts +48 -0
- package/src/react/use-bulk-selection.ts +47 -0
- package/src/react/use-online-status.ts +21 -0
- package/src/react/use-optimistic.ts +54 -0
- package/src/react/use-upload.ts +101 -0
- package/src/retry.ts +47 -0
- package/src/schema.ts +30 -0
- package/src/server/cache-crud.ts +175 -0
- package/src/server/check-schema.ts +29 -0
- package/src/server/child.ts +98 -0
- package/src/server/crud.ts +384 -0
- package/src/server/db.ts +7 -0
- package/src/server/error.ts +39 -0
- package/src/server/file.ts +372 -0
- package/src/server/helpers.ts +214 -0
- package/src/server/index.ts +12 -0
- package/src/server/org-crud.ts +307 -0
- package/src/server/org-helpers.ts +54 -0
- package/src/server/org.ts +572 -0
- package/src/server/schema-helpers.ts +107 -0
- package/src/server/setup.ts +138 -0
- package/src/server/test-crud.ts +211 -0
- package/src/server/test.ts +554 -0
- package/src/server/types.ts +392 -0
- package/src/server/unique.ts +28 -0
- 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 }
|