Skip to content

Commit c8361d3

Browse files
committed
feat(nuxt-drizzle): allowTables
1 parent 5e32f67 commit c8361d3

File tree

4 files changed

+147
-10
lines changed

4 files changed

+147
-10
lines changed

docs/plugins/nuxt-drizzle.md

Lines changed: 119 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,33 @@ const { data: users } = await store.users.query(q => q.many({
150150
</script>
151151
```
152152

153+
## Allowing tables
154+
155+
By default, all tables in your Drizzle schema are exposed through the API. You can restrict the tables that are exposed by using the `allowTables` function.
156+
157+
::: tip
158+
Put this code in a Nitro plugin in `server/plugins` so it's executed once when the server starts.
159+
:::
160+
161+
```ts
162+
import * as tables from 'path-to-your-drizzle-schema'
163+
164+
export default defineNitroPlugin(() => {
165+
allowTables([
166+
tables.todos,
167+
])
168+
})
169+
```
170+
171+
Any table that is not explicitly listed will throw on all API endpoints. `allowTables` can be called multiple times, and the allowed tables will be merged.
172+
153173
## Hooks
154174

155-
You can use hooks to run code before or after certain actions on the collections. You can register global hooks for all collections using the `rstoreDrizzleHooks` import, or specific hooks for a given table using the `hooksForTable` function.
175+
You can use hooks to run code before or after certain actions on the collections. You can register global hooks for all collections using the `rstoreDrizzleHooks` import, or specific hooks for a given table using the `hooksForTable` function (recommended).
176+
177+
::: tip
178+
Put this code in a Nitro plugin in `server/plugins` so it's executed once when the server starts.
179+
:::
156180

157181
You can use the following hooks:
158182

@@ -169,6 +193,28 @@ You can use the following hooks:
169193

170194
If you throw an error in a `before` hook, the action will be aborted and the error will be returned to the client.
171195

196+
```ts
197+
import * as tables from 'path-to-your-drizzle-schema'
198+
199+
export default defineNitroPlugin(() => {
200+
hooksForTable(tables.todos, {
201+
'index.get.before': async (payload) => {
202+
console.log('Specific hook for todos - index.get.before', payload.collection, payload.query, payload.params)
203+
},
204+
'index.get.after': async (payload) => {
205+
console.log('Specific hook for todos - index.get.after', payload.collection, payload.result.map(r => r.id))
206+
},
207+
'item.patch.after': async (payload) => {
208+
console.log('Specific hook for todos - item.patch.after', payload.collection, payload.result.id)
209+
},
210+
})
211+
})
212+
```
213+
214+
<details>
215+
216+
<summary>You can also register global hooks for all tables.</summary>
217+
172218
```ts
173219
export default defineNitroPlugin(() => {
174220
rstoreDrizzleHooks.hook('index.get.before', async (payload) => {
@@ -204,19 +250,82 @@ export default defineNitroPlugin(() => {
204250
})
205251
```
206252

207-
```ts
208-
import * as tables from 'path-to-your-drizzle-schema'
253+
</details>
254+
255+
## Recipes
256+
257+
### Permission check with a query
258+
259+
Throwing an error in a `before` hook to prevent the action:
209260

261+
```ts
210262
export default defineNitroPlugin(() => {
211-
hooksForTable(tables.todos, {
212-
'index.get.before': async (payload) => {
213-
console.log('Specific hook for todos - index.get.before', payload.collection, payload.query, payload.params)
263+
hooksForTable(tables.projects, {
264+
'index.post.before': async ({ body }) => {
265+
const session = await requireUserSession(event)
266+
// Check that the user is a member of the team
267+
// they are trying to create the project for
268+
const teamId = body.teamId
269+
270+
// Check that the user is a member of the team
271+
const membership = await useDrizzle()
272+
.select()
273+
.from(tables.teamsToUsers)
274+
.where(and(
275+
eq(tables.teamsToUsers.teamId, teamId),
276+
eq(tables.teamsToUsers.userId, session.user.id),
277+
))
278+
.limit(1)
279+
.get()
280+
281+
if (!membership) {
282+
throw createError({
283+
statusCode: 403,
284+
statusMessage: 'You are not a member of this team'
285+
})
286+
}
214287
},
215-
'index.get.after': async (payload) => {
216-
console.log('Specific hook for todos - index.get.after', payload.collection, payload.result.map(r => r.id))
288+
})
289+
})
290+
```
291+
292+
### Implicit permission check
293+
294+
By adding a SQL condition to the `where` clause of a query:
295+
296+
```ts
297+
export default defineNitroPlugin(() => {
298+
hooksForTable(tables.projects, {
299+
'index.get.before': async ({ event, transformQuery }) => {
300+
const session = await requireUserSession(event)
301+
// Create a subquery to restrict the projects
302+
// to those that belong to teams the user is a member of
303+
const sq = useDrizzle()
304+
.select({ id: tables.projects.id })
305+
.from(tables.teamsToUsers)
306+
.innerJoin(tables.projects, eq(tables.projects.teamId, tables.teamsToUsers.teamId))
307+
.where(eq(tables.teamsToUsers.userId, session.user.id))
308+
// Use the subquery in the main query
309+
transformQuery(q => q.where(inArray(tables.projects.id, sq)))
217310
},
218-
'item.patch.after': async (payload) => {
219-
console.log('Specific hook for todos - item.patch.after', payload.collection, payload.result.id)
311+
})
312+
})
313+
```
314+
315+
### Automatic query
316+
317+
Automatically execute a query after a specific action:
318+
319+
```ts
320+
export default defineNitroPlugin(() => {
321+
hooksForTable(tables.teams, {
322+
'index.post.after': async ({ event, result }) => {
323+
const session = await requireUserSession(event)
324+
// Add the user as a member of the newly created team
325+
await useDrizzle().insert(tables.teamsToUsers).values({
326+
teamId: result.id,
327+
userId: session.user.id,
328+
})
220329
},
221330
})
222331
})

packages/nuxt-drizzle/src/module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export default defineNuxtModule<ModuleOptions>({
9696
addServerImports([
9797
'rstoreDrizzleHooks',
9898
'hooksForTable',
99+
'allowTables',
99100
].map(name => ({ from: resolve('./runtime/server/utils/hooks'), name })))
100101

101102
const jiti = createJiti(import.meta.url, {

packages/nuxt-drizzle/src/runtime/server/utils/hooks.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,26 @@ export function hooksForTable<TTableConfig extends TableConfig, TTable extends T
8282
})
8383
}
8484
}
85+
86+
let allowedCollections: Set<string> | null = null
87+
88+
export function allowTables(tables: Table[]) {
89+
const collectionNames = tables.map(getDrizzleCollectionNameFromTable)
90+
91+
if (!allowedCollections) {
92+
allowedCollections = new Set(collectionNames)
93+
94+
for (const hookName of ['index.get.before', 'index.post.before', 'item.get.before', 'item.patch.before', 'item.delete.before'] as (keyof RstoreDrizzleHooks)[]) {
95+
rstoreDrizzleHooks.hook(hookName, async (payload: RstoreDrizzleBeforeHookPayload) => {
96+
if (!allowedCollections!.has(payload.collection)) {
97+
throw new Error(`Collection "${payload.collection}" is not allowed.`)
98+
}
99+
})
100+
}
101+
}
102+
else {
103+
for (const name of collectionNames) {
104+
allowedCollections.add(name)
105+
}
106+
}
107+
}

packages/playground-drizzle/server/plugins/hooks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,8 @@ export default defineNitroPlugin(() => {
4343
console.log('Specific hook for todos - item.patch.after', payload.collection, payload.result.id)
4444
},
4545
})
46+
47+
allowTables([
48+
tables.todos,
49+
])
4650
})

0 commit comments

Comments
 (0)