entropy
entropy8mo ago

Is possible to export schema from nested folder?

Hello, I'm trying to achieve something like the setup below but I keep getting errors with my functions when I move the schema file. Is this possible? convex/schema/index.ts
import { defineSchema } from 'convex/server'
import { Users } from './users'

export default defineSchema({
users: Users.table.index('by_clerkId', ['clerkId'])
})
import { defineSchema } from 'convex/server'
import { Users } from './users'

export default defineSchema({
users: Users.table.index('by_clerkId', ['clerkId'])
})
convex/schema/users.ts
import { Table } from 'convex-helpers/server'
import { v } from 'convex/values'

export const Users = Table('users', {
// user fields here...
})
import { Table } from 'convex-helpers/server'
import { v } from 'convex/values'

export const Users = Table('users', {
// user fields here...
})
No description
19 Replies
ian
ian8mo ago
What I would do is: convex/schema.ts
import { defineSchema } from 'convex/server'
import usersTable from "./schemas/users";

export default defineSchema({
...usersTable,
})
import { defineSchema } from 'convex/server'
import usersTable from "./schemas/users";

export default defineSchema({
...usersTable,
})
convex/schemas/users.ts
import { Table } from 'convex-helpers/server'
import { v } from 'convex/values'

export const Users = Table('users', {
// user fields here...
});
export default { users: Users.table..index('by_clerkId', ['clerkId']) };
import { Table } from 'convex-helpers/server'
import { v } from 'convex/values'

export const Users = Table('users', {
// user fields here...
});
export default { users: Users.table..index('by_clerkId', ['clerkId']) };
entropy
entropyOP8mo ago
I see thank you! also sorry one more question. This is my first time trying out convex and I was wondering if there was an easy way of writing a mutation that lets me update any field? I was attempting the method below but I would want all fields to be optional.
export const updateUser = mutation({
args: { id: v.id('users'), ...Users.withoutSystemFields },
handler: async (ctx, args) => {
const authUser = await ctx.auth.getUserIdentity()
if (!authUser) throw new ConvexError('Unauthorized!')

const { id, ...rest } = args
await ctx.db.patch(args.id, { ...rest })
}
})
export const updateUser = mutation({
args: { id: v.id('users'), ...Users.withoutSystemFields },
handler: async (ctx, args) => {
const authUser = await ctx.auth.getUserIdentity()
if (!authUser) throw new ConvexError('Unauthorized!')

const { id, ...rest } = args
await ctx.db.patch(args.id, { ...rest })
}
})
ian
ian8mo ago
check out the partial helper in convex-helpers/validators I believe There's also the crud helper which might be useful to you. Though I would caution that updateUser could allow a malicious client to edit any other user's details right now. You probably want to assure the logged in user is the one being edited. In this way, the crud helper doesn't help since it doesn't have any access controls built in (better for internal functions)
entropy
entropyOP8mo ago
Yep the partial was exactly what I was looking for and gotcha will probably look into the row level security stuff from the convex helpers. Thank you for all the help!
ian
ian8mo ago
I'm planning to update the interface for RLS to something like:
// in a customCtx where you've already looked up the user, e.g.
{
users: ({ ctx, op, doc, update }) => {
switch(op) {
case "create":
return true;
case "read":
return doc.teamId && doc.teamId === user.teamId;
case "update":
case "delete":
return doc._id === user._id;
}
}
}
// in a customCtx where you've already looked up the user, e.g.
{
users: ({ ctx, op, doc, update }) => {
switch(op) {
case "create":
return true;
case "read":
return doc.teamId && doc.teamId === user.teamId;
case "update":
case "delete":
return doc._id === user._id;
}
}
}
Would love to know what you think, once you see the current API
entropy
entropyOP8mo ago
Of course, I was looking at this blog post about creating custom queries. I was confused because everywhere I looked I couldn't find any reference to what wrapDatabaseReader is actually doing. I assume it's just applying the rules passed to it using the user data on the current database ctx. I was also looking at the RLS blog post but it only documents doing this with wrapper method. Not sure if I'm missing something?
Customizing serverless functions without middleware
Re-use code and centralize request handler definitions with discoverability and type safety and without the indirection of middleware or nesting of wr...
Row Level Security: Wrappers as "Middleware"
Implementing row-level security on Convex as a library. Wrap access to the database with access checks written in plain old JS / TS.
ian
ian8mo ago
GitHub
convex-helpers/convex/rowLevelSecurityExample.ts at main · get-conv...
A collection of useful code to complement the official packages. - get-convex/convex-helpers
entropy
entropyOP8mo ago
Thanks!
ian
ian8mo ago
And as you're using it, I'd love it if you gave me your take on whether it'd be more convenient to structure your rules like
{
users: ({ ctx, op, doc, update }) => {
switch(op) {
case "create":
return true;
case "read":
return doc.teamId && doc.teamId === user.teamId;
case "update":
case "delete":
return doc._id === user._id;
}
}
}
{
users: ({ ctx, op, doc, update }) => {
switch(op) {
case "create":
return true;
case "read":
return doc.teamId && doc.teamId === user.teamId;
case "update":
case "delete":
return doc._id === user._id;
}
}
}
entropy
entropyOP8mo ago
Sorry for the late reply, I took a break for a bit. After using it a bit I do think the above api would be much easier to understand. Plus I think being forced to always pass true or false for each case makes it much more explicit as to what's happening.
adam
adam8mo ago
It seems this file has now moved? I found the wrapdb branch that includes a helpful example, though not sure if this will be merged into main? https://github.com/get-convex/convex-helpers/blob/wrapdb/convex/lib/rowLevelSecurity.ts I like the idea of having one wrapDB compared to separating out the wrapDatabaseReader and wrapDatabaseWriter.
GitHub
convex-helpers/convex/lib/rowLevelSecurity.ts at wrapdb · get-conve...
A collection of useful code to complement the official packages. - get-convex/convex-helpers
ian
ian8mo ago
I'm currently iterating on the API. My current strategy is to change that branch to start with a more RLS-specific version. The plan is still to unify the wrappers so there isn't a *Reader & *Writer. @adam some example usage of the old ones for now is in rls.test.ts sorry about moving them. I'm in part starting to hide away the older syntax to make way for the newer one. I got sidetracked working on work stealing and rate limiting but this is what I'll hopefully be focusing on today / next Tuesday when I'm back from camping.
David Alonso
David Alonso5mo ago
Hey Ian, what's the latest on this? This is the only example I'm working with:
const userQuery = customQuery(
query, // The base function we're extending
// Here we're using a `customCtx` helper because our modification
// only modifies the `ctx` argument to the function.
customCtx(async (ctx) => {
// Look up the logged in user
const user = await getUser(ctx);
if (!user) throw new Error("Authentication required");
// Pass in a user to use in evaluating rules,
// which validate data access at access / write time.
const db = wrapDatabaseReader({ user }, ctx.db, rules);
// This new ctx will be applied to the function's.
// The user is a new field, the db replaces ctx.db
return { user, db };
})
);
const userQuery = customQuery(
query, // The base function we're extending
// Here we're using a `customCtx` helper because our modification
// only modifies the `ctx` argument to the function.
customCtx(async (ctx) => {
// Look up the logged in user
const user = await getUser(ctx);
if (!user) throw new Error("Authentication required");
// Pass in a user to use in evaluating rules,
// which validate data access at access / write time.
const db = wrapDatabaseReader({ user }, ctx.db, rules);
// This new ctx will be applied to the function's.
// The user is a new field, the db replaces ctx.db
return { user, db };
})
);
but I don't even know what rules is in this case Right now I'm doing something like:
export const createPageBlock = authenticatedMutation({
args: {
title: v.string(),
parentBlockId: v.optional(v.id("blocks")),
},
handler: async (ctx, args) => {
// Permissions
const { hasPermission, resourceAccessDoc, resourceDoc } =
await checkPermissionByResource(ctx, {
resourceType: "page",
resourceId: args.parentBlockId,
requiredPermissions: ["page:write"],
});
export const createPageBlock = authenticatedMutation({
args: {
title: v.string(),
parentBlockId: v.optional(v.id("blocks")),
},
handler: async (ctx, args) => {
// Permissions
const { hasPermission, resourceAccessDoc, resourceDoc } =
await checkPermissionByResource(ctx, {
resourceType: "page",
resourceId: args.parentBlockId,
requiredPermissions: ["page:write"],
});
where ctx contains a role, user and access document that is used by the checkPermissionByResource function. It would be a lot more robust of course if these checks could be done at the ctx.db level, so I'm wondering what the recommended approach is The wrapdb interface example linked above makes sense to me though
ian
ian5mo ago
Sorry, the types & API ended up having some issues and it got deprioritized. Until I get back to this, I would use the rls that's in convex-helpers currently. Here's a utility to apply rules to a bunch of query/mutations at the same time:
export function BasicRowLevelSecurity(
rules: Rules<GenericQueryCtx<DataModel>, DataModel>
) {
return {
queryWithRLS: customQuery(
query,
customCtx((ctx) => ({ db: wrapDatabaseReader(ctx, ctx.db, rules) }))
),

mutationWithRLS: customMutation(
mutation,
customCtx((ctx) => ({ db: wrapDatabaseWriter(ctx, ctx.db, rules) }))
),

internalQueryWithRLS: customQuery(
internalQuery,
customCtx((ctx) => ({ db: wrapDatabaseReader(ctx, ctx.db, rules) }))
),

internalMutationWithRLS: customMutation(
internalMutation,
customCtx((ctx) => ({ db: wrapDatabaseWriter(ctx, ctx.db, rules) }))
),
};
}
export function BasicRowLevelSecurity(
rules: Rules<GenericQueryCtx<DataModel>, DataModel>
) {
return {
queryWithRLS: customQuery(
query,
customCtx((ctx) => ({ db: wrapDatabaseReader(ctx, ctx.db, rules) }))
),

mutationWithRLS: customMutation(
mutation,
customCtx((ctx) => ({ db: wrapDatabaseWriter(ctx, ctx.db, rules) }))
),

internalQueryWithRLS: customQuery(
internalQuery,
customCtx((ctx) => ({ db: wrapDatabaseReader(ctx, ctx.db, rules) }))
),

internalMutationWithRLS: customMutation(
internalMutation,
customCtx((ctx) => ({ db: wrapDatabaseWriter(ctx, ctx.db, rules) }))
),
};
}
Rules look like

{
cookies: {
read: async ({auth}, cookie) => !cookie.eaten,
modify: async ({auth, db}, cookie) => {
const user = await getUser(auth, db);
return user.isParent; // only parents can reach the cookies.
},
}

{
cookies: {
read: async ({auth}, cookie) => !cookie.eaten,
modify: async ({auth, db}, cookie) => {
const user = await getUser(auth, db);
return user.isParent; // only parents can reach the cookies.
},
}
Where Rules is imported from convex-helpers/server/rowLevelSecurity, and looks like
type Rule<Ctx, D> = (ctx: Ctx, doc: D) => Promise<boolean>;

export type Rules<Ctx, DataModel extends GenericDataModel> = {
[T in TableNamesInDataModel<DataModel>]?: {
read?: Rule<Ctx, DocumentByName<DataModel, T>>;
modify?: Rule<Ctx, DocumentByName<DataModel, T>>;
insert?: Rule<Ctx, WithoutSystemFields<DocumentByName<DataModel, T>>>;
};
};
type Rule<Ctx, D> = (ctx: Ctx, doc: D) => Promise<boolean>;

export type Rules<Ctx, DataModel extends GenericDataModel> = {
[T in TableNamesInDataModel<DataModel>]?: {
read?: Rule<Ctx, DocumentByName<DataModel, T>>;
modify?: Rule<Ctx, DocumentByName<DataModel, T>>;
insert?: Rule<Ctx, WithoutSystemFields<DocumentByName<DataModel, T>>>;
};
};
The read rules get applied on modifications automatically, so you don't have to duplicate there.
David Alonso
David Alonso5mo ago
Thanks Ian, will give this a try! So I should be able to update the rule type to use AuthQueryCtx | AuthMutationCtx instead of Ctx right?
erquhart
erquhart5mo ago
I'd assume so, are you seeing any issues trying it?
ian
ian5mo ago
The current one is generic to whatver ctx you pass into wrapDatabaseWriter. You could pass in something custom like { db, userId }, you're not constrained to off-the-shelf ones
David Alonso
David Alonso5mo ago
Ah right, awesome! I think I should be off to the races now, thanks for the support guys 🙏 can I ask why there's read modify and insert but no delete? How can we detect if a document is being updated vs deleted?
ian
ian5mo ago
I don't think you can distinguish it with this version - and I believe the version of doc passed in is the existing one. That's one thing I liked about a newer API - have control over what they are allowed to modify things to. But concretely is there something you want to allow editing but not deleting? And the more I've been actually making apps, RLS just feels like a clunkier level to be describing those rules. Yes it's nice to feel there's a safeguard, but if there's an error it's at too granular a level to return a meaningful error, and you have to get around places where you don't have auth - like scheduled functions. doing a semantic check in a customQuery like that a user has a certain role or ownership, then just using the DB directly. I feel like RLS was introduced for usecases where you're exposing DB queries to clients directly (where clients could be a browser or another less-trusted service)

Did you find this page helpful?