@noxify/casl-drizzle 0.0.1-beta.3 β 0.2.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/LICENSE +1 -0
- package/README.md +118 -62
- package/dist/index.d.mts +287 -110
- package/dist/index.mjs +1 -1
- package/package.json +47 -50
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,107 +1,163 @@
|
|
|
1
|
-
|
|
1
|
+
# @noxify/casl-drizzle
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CASL v7 integration for Drizzle ORM ( 1.0.0-rc.3 ) - Add type-safe authorization to your database queries
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- π **Type-safe** - Full TypeScript support with Drizzle types
|
|
8
|
+
- π― **Relation support** - Filter by related table conditions
|
|
9
|
+
- π **Many-to-many support** - Filter across join-table relations
|
|
10
|
+
- π **Query operators** - All Drizzle operators (eq, gt, like, etc.)
|
|
11
|
+
- π‘ **IDE autocomplete** - Subject-specific field suggestions
|
|
6
12
|
|
|
7
13
|
## Install
|
|
8
14
|
|
|
9
15
|
```sh
|
|
10
|
-
npm install
|
|
16
|
+
npm install @noxify/casl-drizzle @casl/ability drizzle-orm@1.0.0-rc.3
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
pnpm add @noxify/casl-drizzle @casl/ability drizzle-orm@1.0.0-rc.3
|
|
11
21
|
```
|
|
12
22
|
|
|
13
|
-
##
|
|
23
|
+
## Setup
|
|
14
24
|
|
|
15
|
-
Define your
|
|
25
|
+
Define your Drizzle schema and relations:
|
|
16
26
|
|
|
17
27
|
```typescript
|
|
18
|
-
import
|
|
19
|
-
import { integer,
|
|
20
|
-
import { accessibleBy, defineAbility } from "ucastle"
|
|
28
|
+
import { defineRelations, pgTable } from "drizzle-orm"
|
|
29
|
+
import { integer, text } from "drizzle-orm/pg-core"
|
|
21
30
|
|
|
22
|
-
// Define your Drizzle schema
|
|
23
31
|
const users = pgTable("users", {
|
|
24
32
|
id: integer().primaryKey(),
|
|
25
33
|
name: text().notNull(),
|
|
26
|
-
email: text().notNull(),
|
|
27
34
|
})
|
|
28
35
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Create abilities with subject-specific autocomplete
|
|
35
|
-
const ability = defineAbility<{ users: UserQuery }>((can, cannot) => {
|
|
36
|
-
can("read", "users", { id: 1 }) // β
Autocomplete shows only user fields!
|
|
37
|
-
can("update", "users", { id: 1 })
|
|
38
|
-
cannot("delete", "users")
|
|
36
|
+
const posts = pgTable("posts", {
|
|
37
|
+
id: integer().primaryKey(),
|
|
38
|
+
title: text().notNull(),
|
|
39
|
+
authorId: integer().notNull(),
|
|
39
40
|
})
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
export const relations = defineRelations({ users, posts }, (r) => ({
|
|
43
|
+
users: { posts: r.many.posts() },
|
|
44
|
+
posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id }) },
|
|
45
|
+
}))
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
##
|
|
48
|
+
## Usage
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
- π― **CASL integration** - Leverage CASL's powerful rule system
|
|
50
|
-
- ποΈ **DB agnostic** - Works with PostgreSQL, MySQL, SQLite, etc.
|
|
51
|
-
- π **Relation support** - Filter by related table conditions
|
|
52
|
-
- π¦ **Zero overhead** - Direct type composition, no runtime wrappers
|
|
53
|
-
- π‘ **Smart autocomplete** - Subject-specific field suggestions with `defineAbility()`
|
|
50
|
+
Create type-safe abilities with Drizzle query conditions:
|
|
54
51
|
|
|
55
|
-
|
|
52
|
+
```typescript
|
|
53
|
+
import type { QueryInput } from "@noxify/casl-drizzle"
|
|
54
|
+
import { accessibleBy, createDrizzleAbility, some } from "@noxify/casl-drizzle"
|
|
55
|
+
import { sql } from "drizzle-orm"
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
type PostQuery = QueryInput<typeof relations, "posts">
|
|
58
|
+
type UserQuery = QueryInput<typeof relations, "users">
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
import type { RelationalQueryInput } from "ucastle"
|
|
61
|
-
import { defineRelations } from "drizzle-orm"
|
|
60
|
+
const currentUserId = 1
|
|
62
61
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
const ability = createDrizzleAbility<
|
|
63
|
+
{ posts: PostQuery; users: UserQuery },
|
|
64
|
+
"read" | "create" | "update" | "delete"
|
|
65
|
+
>((can) => {
|
|
66
|
+
// Simple field filtering
|
|
67
|
+
can("read", "posts", { published: true })
|
|
68
|
+
|
|
69
|
+
// Filter by related table (author)
|
|
70
|
+
can("read", "posts", { author: { id: currentUserId } })
|
|
71
|
+
|
|
72
|
+
// Many-to-many filtering (example: users by groups)
|
|
73
|
+
can("read", "users", { groups: some(sql`name = 'Admins'`) })
|
|
74
|
+
|
|
75
|
+
// Complex conditions with operators
|
|
76
|
+
can("update", "posts", {
|
|
77
|
+
author: { id: currentUserId },
|
|
78
|
+
createdAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Raw SQL for complex queries
|
|
82
|
+
can("delete", "posts", {
|
|
83
|
+
RAW: sql`author_id = ${currentUserId} AND published = false`,
|
|
84
|
+
})
|
|
67
85
|
})
|
|
68
86
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
})
|
|
87
|
+
// Convert abilities to database filters
|
|
88
|
+
const filters = accessibleBy(ability, "read")
|
|
89
|
+
const posts = await db.query.posts.findMany({ where: filters.posts })
|
|
90
|
+
const users = await db.query.users.findMany({ where: filters.users })
|
|
91
|
+
```
|
|
73
92
|
|
|
74
|
-
|
|
93
|
+
## Behavior Notes
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
### `every()` - All Related Records Must Match
|
|
96
|
+
|
|
97
|
+
`every()` filters records where **all related records** satisfy the condition. Important: it currently requires that related records exist:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// β
Returns users who have AT LEAST ONE post, and ALL their posts have views > 100
|
|
101
|
+
can("read", "users", {
|
|
102
|
+
posts: every({ views: { gt: 100 } }),
|
|
79
103
|
})
|
|
104
|
+
|
|
105
|
+
// β Returns no users if they have NO posts at all
|
|
106
|
+
// (even though "all zero posts have >100 views" is technically true)
|
|
80
107
|
```
|
|
81
108
|
|
|
82
|
-
|
|
109
|
+
Use when you need to enforce a condition across all related records that exist. For "users with no posts" scenarios, use `none()` instead.
|
|
83
110
|
|
|
84
|
-
|
|
111
|
+
### `none()` - No Related Records Match
|
|
112
|
+
|
|
113
|
+
`none()` filters records where **no related records** satisfy the condition. Works reliably for simple cases but may behave unexpectedly in complex relation chains.
|
|
114
|
+
|
|
115
|
+
**β
Simple case (recommended):**
|
|
85
116
|
|
|
86
117
|
```typescript
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
import { createDrizzleAbilityFor } from "ucastle"
|
|
118
|
+
can("read", "posts", { comments: none() })
|
|
119
|
+
```
|
|
90
120
|
|
|
91
|
-
|
|
121
|
+
**β οΈ Complex paths - Known Issue:**
|
|
122
|
+
When filtering through nested relations, `none()` semantics can be inverted:
|
|
92
123
|
|
|
93
|
-
|
|
124
|
+
```typescript
|
|
125
|
+
// β Behavior may be unexpected:
|
|
126
|
+
can("read", "posts", {
|
|
127
|
+
comments: none({ author: { id: adminId } }),
|
|
128
|
+
})
|
|
129
|
+
```
|
|
94
130
|
|
|
95
|
-
|
|
96
|
-
cannot("delete", "users")
|
|
131
|
+
**β
Solution - Use Drizzle's type-safe subquery:**
|
|
97
132
|
|
|
98
|
-
|
|
133
|
+
```typescript
|
|
134
|
+
import { notExists, eq, and } from "drizzle-orm"
|
|
135
|
+
|
|
136
|
+
can("read", "posts", {
|
|
137
|
+
RAW: notExists(
|
|
138
|
+
db
|
|
139
|
+
.select()
|
|
140
|
+
.from(comments)
|
|
141
|
+
.where(and(eq(comments.postId, posts.id), eq(comments.authorId, adminId)))
|
|
142
|
+
),
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
β οΈ **Current limitation**: Due to Drizzle's alias handling in subqueries, both `notExists()` and raw SQL with outer table references currently fail. For now, the most reliable approach is to avoid complex `none()` filters and use simpler patterns or application-level filtering for edge cases.
|
|
147
|
+
|
|
148
|
+
**For simple cases without outer table references:**
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// This works: Simple static condition without referencing outer table
|
|
152
|
+
can("read", "users", {
|
|
153
|
+
posts: none(), // All users with no posts
|
|
154
|
+
})
|
|
99
155
|
```
|
|
100
156
|
|
|
101
|
-
|
|
157
|
+
For critical authorization rules involving complex relation filters, always use explicit `RAW` SQL to ensure predictable behavior.
|
|
102
158
|
|
|
103
|
-
|
|
159
|
+
## Acknowledgements
|
|
104
160
|
|
|
105
|
-
|
|
161
|
+
This project was heavily inspired by [ucastle](https://github.com/araujogui/ucastle) by Guilherme Araujo and evolved through substantial refactoring and extension for Drizzle relation support.
|
|
106
162
|
|
|
107
|
-
|
|
163
|
+
If you are looking for the original foundation and ideas, please also check the ucastle repository.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { AbilityOptions, AbilityOptionsOf, AbilityTuple, AnyAbility, ForcedSubject,
|
|
2
|
-
import
|
|
3
|
-
import { DBQueryConfig,
|
|
1
|
+
import { Ability, AbilityOptions, AbilityOptionsOf, AbilityTuple, AnyAbility, ForcedSubject, RawRuleFrom, RawRuleOf, hkt } from "@casl/ability";
|
|
2
|
+
import { SQL, Table, operators } from "drizzle-orm";
|
|
3
|
+
import { DBQueryConfig, TablesRelationalConfig } from "drizzle-orm/relations";
|
|
4
4
|
import { KnownKeysOnly } from "drizzle-orm/utils";
|
|
5
5
|
|
|
6
6
|
//#region src/drizzle-query.d.ts
|
|
7
7
|
declare const drizzleQuery: (query: Record<string, unknown>, ...args: unknown[]) => {
|
|
8
8
|
(...args: any[]): any;
|
|
9
|
-
ast:
|
|
9
|
+
ast: import("@ucast/core").Condition;
|
|
10
10
|
};
|
|
11
11
|
type Model<T, TName extends string> = T & ForcedSubject<TName>;
|
|
12
12
|
type Subjects<T extends Partial<Record<string, Record<string, unknown>>>> = keyof T | { [K in keyof T]: Model<T[K], K & string> }[keyof T];
|
|
@@ -20,149 +20,321 @@ type ExtractModelName<TObject, TModelName extends PropertyKey> = TObject extends
|
|
|
20
20
|
} ? TObject["__typename"] : TModelName;
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/types.d.ts
|
|
23
|
+
/**
|
|
24
|
+
* Unique symbol used by CASL's HKT (Higher-Kinded Types) system.
|
|
25
|
+
* This is used internally to mark types that belong to the Drizzle query system.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
declare const Ι΅drizzleTypes: unique symbol;
|
|
29
|
+
/**
|
|
30
|
+
* Internal marker interface for CASL's HKT system.
|
|
31
|
+
* Used to identify types that are part of the Drizzle query system.
|
|
32
|
+
* This enables type-safe composition of abilities with subject-specific conditions.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
23
35
|
interface BaseDrizzleQuery {
|
|
24
36
|
[Ι΅drizzleTypes]?: Record<string, unknown>;
|
|
25
37
|
}
|
|
26
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Internal factory type used by CASL to create ability instances.
|
|
40
|
+
* Combines a record base with CASL's HKT container interface and the Drizzle marker.
|
|
41
|
+
* Used internally by `createDrizzleAbilityFor()` and `defineAbility()`.
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
27
44
|
type DrizzleQueryFactory = Record<string, unknown> & hkt.Container<hkt.GenericFactory> & BaseDrizzleQuery;
|
|
28
|
-
type DrizzleModel = Model<Record<string, unknown>, string>;
|
|
29
|
-
type WhereInput = Record<string, unknown>;
|
|
30
45
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
46
|
+
* Internal type representing a Drizzle model for CASL.
|
|
47
|
+
* Used by the query system to enforce type safety for model-based permissions.
|
|
48
|
+
* @internal
|
|
33
49
|
*/
|
|
34
|
-
type
|
|
50
|
+
type DrizzleModel = Model<Record<string, unknown>, string>;
|
|
35
51
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```ts
|
|
41
|
-
* import { defineRelations } from "drizzle-orm"
|
|
42
|
-
* import type { RelationalQueryInput } from "ucastle"
|
|
43
|
-
*
|
|
44
|
-
* const relations = defineRelations({ users, posts }, ...)
|
|
45
|
-
* type UserQuery = RelationalQueryInput<typeof relations, "users">
|
|
46
|
-
*
|
|
47
|
-
* // Now includes: field conditions + relation filters + AND/OR/NOT
|
|
48
|
-
* ```
|
|
52
|
+
* Internal type representing query conditions extracted from a query input.
|
|
53
|
+
* This is the return type of `accessibleBy()` and is used internally by the query interpreter.
|
|
54
|
+
* @internal
|
|
49
55
|
*/
|
|
50
|
-
type
|
|
56
|
+
type WhereInput = Record<string, unknown>;
|
|
51
57
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
58
|
+
* Typed query input for Drizzle tables with full operator support.
|
|
59
|
+
* Extracts the complete query type from Drizzle's RQB v2, including all field operators
|
|
60
|
+
* (comparison, membership, string, null, relation, and compound operators), nested relations,
|
|
61
|
+
* and raw SQL conditions.
|
|
62
|
+
*
|
|
63
|
+
* @template TSchema - The Drizzle relations configuration object
|
|
64
|
+
* @template TTableName - The key of the table within TSchema
|
|
54
65
|
*
|
|
55
66
|
* @example
|
|
56
67
|
* ```ts
|
|
57
|
-
* import
|
|
58
|
-
*
|
|
59
|
-
* const schema = { users, posts }
|
|
60
|
-
* type UserQuery = QueryInput<typeof schema, "users">
|
|
68
|
+
* import { relations, sql } from "drizzle-orm"
|
|
69
|
+
* import type { QueryInput } from "@noxify/casl-drizzle"
|
|
61
70
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
71
|
+
* const relations = defineRelations({ users, posts }, (r) => ({
|
|
72
|
+
* users: { posts: r.many(posts) },
|
|
73
|
+
* posts: { author: r.one(users) }
|
|
74
|
+
* }))
|
|
64
75
|
*
|
|
65
|
-
*
|
|
66
|
-
* const q2: UserQuery = { id: { gte: 1 }, AND: [{ name: "John" }] }
|
|
76
|
+
* type PostQuery = QueryInput<typeof relations, "posts">
|
|
67
77
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
78
|
+
* const ability = defineAbility<{ posts: PostQuery }>((can) => {
|
|
79
|
+
* can("read", "posts", { published: true })
|
|
80
|
+
* can("update", "posts", { authorId: 1 })
|
|
81
|
+
* // Raw SQL for complex conditions
|
|
82
|
+
* can("delete", "posts", {
|
|
83
|
+
* RAW: sql`EXISTS (SELECT 1 FROM contributors WHERE post_id = posts.id)`
|
|
84
|
+
* })
|
|
85
|
+
* })
|
|
70
86
|
* ```
|
|
71
87
|
*/
|
|
72
|
-
type QueryInput<TSchema extends
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
type QueryInput<TSchema extends TablesRelationalConfig, TTableName extends keyof TSchema> = Exclude<KnownKeysOnly<DBQueryConfig<"many", TSchema, TSchema[TTableName]>, DBQueryConfig<"many", TSchema, TSchema[TTableName]>>["where"], undefined> & {
|
|
89
|
+
/**
|
|
90
|
+
* Optional raw SQL condition for complex queries that can't be expressed with field operators.
|
|
91
|
+
* The SQL template literal will be passed directly to Drizzle's where clause.
|
|
92
|
+
*
|
|
93
|
+
* **IMPORTANT**: Ensure the SQL is properly parameterized to prevent SQL injection.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* { RAW: sql`${table.age} BETWEEN 25 AND 35` }
|
|
98
|
+
* { RAW: sql`EXISTS (SELECT 1 FROM related WHERE ...)` }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
RAW?: unknown;
|
|
102
|
+
};
|
|
79
103
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
104
|
+
* Creates a union of all possible CASL subjects from a query type mapping.
|
|
105
|
+
* Each table name becomes a subject, plus model objects for relation-based filtering.
|
|
106
|
+
* Used to define subject-specific abilities with type-safe condition validation.
|
|
107
|
+
*
|
|
108
|
+
* @template T - An object mapping table names to their query input types
|
|
82
109
|
*
|
|
83
110
|
* @example
|
|
84
111
|
* ```ts
|
|
85
|
-
* import {
|
|
86
|
-
* import type {
|
|
112
|
+
* import { Ability } from "@casl/ability"
|
|
113
|
+
* import type { QueryInput, Subjects } from "@noxify/casl-drizzle"
|
|
87
114
|
*
|
|
88
|
-
* type
|
|
89
|
-
*
|
|
115
|
+
* type QueryMap = {
|
|
116
|
+
* users: QueryInput<typeof relations, "users">
|
|
117
|
+
* posts: QueryInput<typeof relations, "posts">
|
|
118
|
+
* }
|
|
90
119
|
*
|
|
91
|
-
* type AppAbility =
|
|
92
|
-
* users: UserQuery,
|
|
93
|
-
* posts: PostQuery
|
|
94
|
-
* }>]>
|
|
120
|
+
* type AppAbility = Ability<[string, Subjects<QueryMap>]>
|
|
95
121
|
* ```
|
|
96
122
|
*/
|
|
97
123
|
type Subjects$1<T> = { [K in keyof T]: Model<T[K], K & string> }[keyof T] | Extract<keyof T, string>;
|
|
98
124
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
125
|
+
* CASL ability type with Drizzle query conditions and typed actions.
|
|
126
|
+
* Provides type-safe permission checking with action and subject validation.
|
|
127
|
+
* Use with `defineAbility()` for automatic type inference and autocomplete.
|
|
101
128
|
*
|
|
102
|
-
*
|
|
129
|
+
* @template T - An object mapping subject names to their query input types
|
|
130
|
+
* @template TActions - A union of action strings (e.g. "read" | "update")
|
|
103
131
|
*
|
|
104
132
|
* @example
|
|
105
133
|
* ```ts
|
|
106
|
-
* import type { DrizzleAbility } from "
|
|
134
|
+
* import type { DrizzleAbility, QueryInput } from "@noxify/casl-drizzle"
|
|
135
|
+
*
|
|
136
|
+
* type AllowedAction = "read" | "create" | "update" | "delete"
|
|
107
137
|
*
|
|
108
138
|
* type SubjectMap = {
|
|
109
|
-
* users:
|
|
110
|
-
* posts:
|
|
139
|
+
* users: QueryInput<typeof relations, "users">
|
|
140
|
+
* posts: QueryInput<typeof relations, "posts">
|
|
111
141
|
* }
|
|
112
142
|
*
|
|
113
|
-
* type AppAbility = DrizzleAbility<SubjectMap>
|
|
143
|
+
* type AppAbility = DrizzleAbility<SubjectMap, AllowedAction>
|
|
114
144
|
* ```
|
|
115
145
|
*/
|
|
116
|
-
type DrizzleAbility
|
|
146
|
+
type DrizzleAbility<T, TActions extends string = string> = Ability<[TActions, Subjects$1<T>], T[keyof T]>;
|
|
117
147
|
/**
|
|
118
|
-
* Helper type
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
148
|
+
* Helper type for defining abilities with full type inference for actions and subjects.
|
|
149
|
+
* When used with `createDrizzleAbility()`, enables IDE autocomplete for `can()` and `cannot()`
|
|
150
|
+
* showing only the relevant actions and subject fields.
|
|
151
|
+
*
|
|
152
|
+
* @template T - An object mapping subject names to their query input types
|
|
153
|
+
* @template TActions - A union of action strings (e.g. "read" | "update")
|
|
122
154
|
*
|
|
123
155
|
* @example
|
|
124
156
|
* ```ts
|
|
125
|
-
* import {
|
|
157
|
+
* import { createDrizzleAbility, type DefineDrizzleAbility } from "@noxify/casl-drizzle"
|
|
158
|
+
* import type { QueryInput } from "@noxify/casl-drizzle"
|
|
126
159
|
*
|
|
127
|
-
*
|
|
128
|
-
* type SubjectMap = {
|
|
129
|
-
* users: UserQuery
|
|
130
|
-
* posts: PostQuery
|
|
131
|
-
* }
|
|
160
|
+
* type AllowedAction = "read" | "create" | "update" | "delete"
|
|
132
161
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* posts: PostQuery
|
|
162
|
+
* type SubjectMap = {
|
|
163
|
+
* users: QueryInput<typeof relations, "users">
|
|
164
|
+
* posts: QueryInput<typeof relations, "posts">
|
|
137
165
|
* }
|
|
138
166
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* can("
|
|
142
|
-
*
|
|
167
|
+
* const ability = createDrizzleAbility<SubjectMap, AllowedAction>((can, cannot) => {
|
|
168
|
+
* can("read", "users", { id: 1 })
|
|
169
|
+
* can("update", "posts", { authorId: 1 })
|
|
170
|
+
* cannot("delete", "posts", { published: true })
|
|
143
171
|
* })
|
|
144
172
|
* ```
|
|
145
173
|
*/
|
|
146
|
-
type
|
|
174
|
+
type DefineDrizzleAbility<T, TActions extends string = string> = DrizzleAbility<T, TActions>;
|
|
147
175
|
//#endregion
|
|
148
176
|
//#region src/factories/accessible-by.d.ts
|
|
149
177
|
/**
|
|
150
178
|
* @deprecated use accessibleBy directly instead. It will infer the types from passed Ability instance.
|
|
151
179
|
*/
|
|
152
|
-
declare const createAccessibleByFactory: <TResult extends Record<string, unknown>, TDrizzleQuery>() => <TAbility extends
|
|
153
|
-
declare function accessibleBy<
|
|
180
|
+
declare const createAccessibleByFactory: <TResult extends Record<string, unknown>, TDrizzleQuery>() => <TAbility extends Ability<any, TDrizzleQuery>>(ability: TAbility, action?: TAbility["rules"][number]["action"]) => TResult;
|
|
181
|
+
declare function accessibleBy<TSubjectMap, TActions extends string = string>(ability: DrizzleAbility<TSubjectMap, TActions>, action?: TActions): Record<Extract<keyof TSubjectMap, string>, WhereInput>;
|
|
182
|
+
declare function accessibleBy<TAbility extends Ability<any, any>>(ability: TAbility, action?: TAbility["rules"][number]["action"]): Record<string, WhereInput>;
|
|
154
183
|
//#endregion
|
|
155
184
|
//#region src/factories/create-ability.d.ts
|
|
156
185
|
declare function createAbilityFactory<TModelName extends string, TDrizzleQuery extends Record<string, any>>(): {
|
|
157
|
-
<T extends
|
|
158
|
-
<A extends AbilityTuple = [string, TModelName], C extends TDrizzleQuery = TDrizzleQuery>(rules?: RawRuleFrom<A, C>[], options?: AbilityOptions<A, C>):
|
|
186
|
+
<T extends Ability<any, TDrizzleQuery>>(rules?: RawRuleOf<T>[], options?: AbilityOptionsOf<T>): T;
|
|
187
|
+
<A extends AbilityTuple = [string, TModelName], C extends TDrizzleQuery = TDrizzleQuery>(rules?: RawRuleFrom<A, C>[], options?: AbilityOptions<A, C>): Ability<A, C>;
|
|
159
188
|
};
|
|
160
189
|
//#endregion
|
|
161
190
|
//#region src/query-error.d.ts
|
|
162
191
|
declare class ParsingQueryError extends Error {
|
|
192
|
+
name: string;
|
|
163
193
|
static invalidArgument(operatorName: string, value: unknown, expectValueType: string): ParsingQueryError;
|
|
164
194
|
}
|
|
165
195
|
//#endregion
|
|
196
|
+
//#region src/factories/relation-helpers.d.ts
|
|
197
|
+
/**
|
|
198
|
+
* Type for the builder function parameter with operators and column proxies
|
|
199
|
+
*/
|
|
200
|
+
type RelationHelperBuilder = typeof operators & {
|
|
201
|
+
/**
|
|
202
|
+
* Proxy to access columns from the related table.
|
|
203
|
+
* Each property access returns a SQL reference to that column.
|
|
204
|
+
*
|
|
205
|
+
* TypeScript limitation: Due to how Proxy types work, columns is typed as Record<string, any>.
|
|
206
|
+
* At runtime, each property access returns a valid SQL instance.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* some(({ eq, columns }) => eq(columns.name, 'Alice'))
|
|
211
|
+
* // Generates: name = 'Alice' (in relation subquery context)
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
columns: Record<string, any>;
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Helper to create a RAW condition for "some" relation filtering.
|
|
218
|
+
* Generates an EXISTS subquery that returns true if at least one related record matches the condition.
|
|
219
|
+
*
|
|
220
|
+
* Use this when you need to filter by relations with complex conditions not supported by QueryInput.
|
|
221
|
+
* Supports both raw SQL and builder function syntax with column proxies.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* import { some } from "@noxify/casl-drizzle"
|
|
226
|
+
* import { sql } from "drizzle-orm"
|
|
227
|
+
*
|
|
228
|
+
* // Using raw SQL
|
|
229
|
+
* can('update', 'documents', {
|
|
230
|
+
* contributors: some(sql`user_id = ${userId}`),
|
|
231
|
+
* })
|
|
232
|
+
*
|
|
233
|
+
* // Using builder function with operators and columns (untyped)
|
|
234
|
+
* can('update', 'documents', {
|
|
235
|
+
* contributors: some(({ eq, columns }) => eq(columns.userId, userId)),
|
|
236
|
+
* })
|
|
237
|
+
*
|
|
238
|
+
* // Type-safe version with table reference:
|
|
239
|
+
* can('read', 'posts', {
|
|
240
|
+
* author: some(schema.users, ({ eq, columns }) => eq(columns.name, 'Alice')),
|
|
241
|
+
* // columns is now type-safe with full autocomplete!
|
|
242
|
+
* })
|
|
243
|
+
* ```
|
|
244
|
+
*
|
|
245
|
+
* @param conditionOrTable - Raw SQL WHERE condition, builder function, or table reference
|
|
246
|
+
* @param maybeCondition - Optional builder function when first param is a table
|
|
247
|
+
* @returns A RAW condition object for use in ability definitions
|
|
248
|
+
*/
|
|
249
|
+
declare function some(condition: SQL | ((builder: RelationHelperBuilder) => SQL)): {
|
|
250
|
+
RAW: SQL;
|
|
251
|
+
};
|
|
252
|
+
declare function some<T extends Table>(table: T, condition: (builder: Omit<RelationHelperBuilder, "columns"> & {
|
|
253
|
+
columns: T["_"]["columns"];
|
|
254
|
+
}) => SQL): {
|
|
255
|
+
RAW: SQL;
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Helper to create a RAW condition for "every" relation filtering.
|
|
259
|
+
* Checks if ALL related records match the condition.
|
|
260
|
+
*
|
|
261
|
+
* Supports both raw SQL and builder function syntax with column proxies.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```ts
|
|
265
|
+
* // Only allow updates if ALL comments are approved
|
|
266
|
+
* can('update', 'documents', {
|
|
267
|
+
* comments: every(sql`status = 'approved'`)
|
|
268
|
+
* })
|
|
269
|
+
*
|
|
270
|
+
* // Using builder function with operators
|
|
271
|
+
* can('update', 'documents', {
|
|
272
|
+
* comments: every(({ eq, columns }) => eq(columns.status, 'approved'))
|
|
273
|
+
* })
|
|
274
|
+
*
|
|
275
|
+
* // Type-safe version with table reference:
|
|
276
|
+
* can('update', 'documents', {
|
|
277
|
+
* comments: every(schema.comments, ({ eq, columns }) => eq(columns.status, 'approved'))
|
|
278
|
+
* })
|
|
279
|
+
* ```
|
|
280
|
+
*
|
|
281
|
+
* @param conditionOrTable - Raw SQL WHERE condition, builder function, or table reference
|
|
282
|
+
* @param maybeCondition - Optional builder function when first param is a table
|
|
283
|
+
* @returns A RAW condition object for use in ability definitions
|
|
284
|
+
*/
|
|
285
|
+
declare function every(condition: SQL | ((builder: RelationHelperBuilder) => SQL)): {
|
|
286
|
+
RAW: SQL;
|
|
287
|
+
};
|
|
288
|
+
declare function every<T extends Table>(table: T, condition: (builder: Omit<RelationHelperBuilder, "columns"> & {
|
|
289
|
+
columns: T["_"]["columns"];
|
|
290
|
+
}) => SQL): {
|
|
291
|
+
RAW: SQL;
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Helper to create a RAW condition for "none" relation filtering.
|
|
295
|
+
* Checks if NO related records match the condition (or exist at all).
|
|
296
|
+
*
|
|
297
|
+
* Supports both raw SQL and builder function syntax with column proxies.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* // Using raw SQL
|
|
302
|
+
* can('delete', 'documents', {
|
|
303
|
+
* comments: none(sql`status = 'pending'`)
|
|
304
|
+
* })
|
|
305
|
+
*
|
|
306
|
+
* // Using builder function with operators
|
|
307
|
+
* can('delete', 'documents', {
|
|
308
|
+
* comments: none(({ eq, columns }) => eq(columns.status, 'pending'))
|
|
309
|
+
* })
|
|
310
|
+
*
|
|
311
|
+
* // Check that no related records exist
|
|
312
|
+
* can('delete', 'documents', {
|
|
313
|
+
* comments: none()
|
|
314
|
+
* })
|
|
315
|
+
*
|
|
316
|
+
* // Type-safe version with table reference:
|
|
317
|
+
* can('delete', 'documents', {
|
|
318
|
+
* comments: none(schema.comments, ({ eq, columns }) => eq(columns.status, 'pending'))
|
|
319
|
+
* })
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* @param conditionOrTable - Raw SQL WHERE condition, builder function, table reference, or undefined
|
|
323
|
+
* @param maybeCondition - Optional builder function when first param is a table
|
|
324
|
+
* @returns A RAW condition object for use in ability definitions
|
|
325
|
+
*/
|
|
326
|
+
declare function none(): {
|
|
327
|
+
RAW: SQL;
|
|
328
|
+
};
|
|
329
|
+
declare function none(condition: SQL | ((builder: RelationHelperBuilder) => SQL)): {
|
|
330
|
+
RAW: SQL;
|
|
331
|
+
};
|
|
332
|
+
declare function none<T extends Table>(table: T, condition?: (builder: Omit<RelationHelperBuilder, "columns"> & {
|
|
333
|
+
columns: T["_"]["columns"];
|
|
334
|
+
}) => SQL): {
|
|
335
|
+
RAW: SQL;
|
|
336
|
+
};
|
|
337
|
+
//#endregion
|
|
166
338
|
//#region src/index.d.ts
|
|
167
339
|
/**
|
|
168
340
|
* Factory function to create a DrizzleAbility instance.
|
|
@@ -171,44 +343,49 @@ declare class ParsingQueryError extends Error {
|
|
|
171
343
|
* @example
|
|
172
344
|
* ```ts
|
|
173
345
|
* import { AbilityBuilder } from "@casl/ability"
|
|
174
|
-
* import { createDrizzleAbilityFor, type
|
|
346
|
+
* import { createDrizzleAbilityFor, type DefineDrizzleAbility } from "@noxify/casl-drizzle"
|
|
347
|
+
*
|
|
348
|
+
* type AllowedAction = "read" | "create" | "update" | "delete"
|
|
175
349
|
*
|
|
176
|
-
* type AppAbility =
|
|
350
|
+
* type AppAbility = DefineDrizzleAbility<{
|
|
177
351
|
* users: UserQuery
|
|
178
352
|
* posts: PostQuery
|
|
179
|
-
* }>
|
|
353
|
+
* }, AllowedAction>
|
|
180
354
|
*
|
|
181
|
-
* const { can, build } = new AbilityBuilder<AppAbility>(
|
|
355
|
+
* const { can, build } = new AbilityBuilder<AppAbility>(
|
|
356
|
+
* createDrizzleAbilityFor<{
|
|
357
|
+
* users: UserQuery
|
|
358
|
+
* posts: PostQuery
|
|
359
|
+
* }, AllowedAction>(),
|
|
360
|
+
* )
|
|
182
361
|
* ```
|
|
183
362
|
*/
|
|
184
|
-
declare function createDrizzleAbilityFor(): new (...args: ConstructorParameters<typeof
|
|
363
|
+
declare function createDrizzleAbilityFor(): new (...args: ConstructorParameters<typeof Ability>) => AnyAbility;
|
|
364
|
+
declare function createDrizzleAbilityFor<TSubject, TActions extends string = string>(): new (...args: ConstructorParameters<typeof Ability>) => DrizzleAbility<TSubject, TActions>;
|
|
185
365
|
/**
|
|
186
|
-
* Create a type-safe ability with subject-specific conditions.
|
|
187
|
-
*
|
|
366
|
+
* Create a type-safe ability with subject and action-specific conditions.
|
|
367
|
+
* Use this with a pre-defined ability type for full type inference.
|
|
368
|
+
*
|
|
369
|
+
* @template TAbility - A DrizzleAbility type (includes both actions and subjects)
|
|
188
370
|
*
|
|
189
371
|
* @example
|
|
190
372
|
* ```ts
|
|
191
|
-
* import {
|
|
373
|
+
* import { createDrizzleAbility } from "@noxify/casl-drizzle"
|
|
374
|
+
* import type { QueryInput } from "@noxify/casl-drizzle"
|
|
192
375
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
376
|
+
* type AllowedAction = "read" | "create" | "update" | "delete"
|
|
377
|
+
*
|
|
378
|
+
* type SubjectMap = {
|
|
379
|
+
* users: QueryInput<typeof relations, "users">
|
|
380
|
+
* posts: QueryInput<typeof relations, "posts">
|
|
381
|
+
* }
|
|
382
|
+
*
|
|
383
|
+
* const ability = createDrizzleAbility<SubjectMap, AllowedAction>((can, cannot) => {
|
|
384
|
+
* can("read", "users", { id: 1 })
|
|
385
|
+
* can("update", "posts", { authorId: 1 })
|
|
199
386
|
* })
|
|
200
387
|
* ```
|
|
201
388
|
*/
|
|
202
|
-
declare function
|
|
203
|
-
/**
|
|
204
|
-
* Uses conditional type to support union distribution
|
|
205
|
-
*/
|
|
206
|
-
type ExtendedAbilityTuple<T extends AbilityTuple> = T extends AbilityTuple ? [T[0], "all" | T[1]] : never;
|
|
207
|
-
/**
|
|
208
|
-
* @deprecated use createDrizzleAbilityFor instead
|
|
209
|
-
*/
|
|
210
|
-
declare class DrizzleAbility<A extends AbilityTuple = [string, string], C extends Record<string, unknown> = Record<string, unknown>> extends PureAbility<ExtendedAbilityTuple<A>, C> {
|
|
211
|
-
constructor(rules?: RawRuleFrom<ExtendedAbilityTuple<A>, C>[], options?: AbilityOptions<ExtendedAbilityTuple<A>, C>);
|
|
212
|
-
}
|
|
389
|
+
declare function createDrizzleAbility<TSubject, TActions extends string = string>(define: (can: <S extends keyof TSubject & string>(action: TActions, subject: S, conditions?: TSubject[S]) => void, cannot: <S extends keyof TSubject & string>(action: TActions, subject: S, conditions?: TSubject[S]) => void) => void): DrizzleAbility<TSubject, TActions>;
|
|
213
390
|
//#endregion
|
|
214
|
-
export { type BaseDrizzleQuery, type
|
|
391
|
+
export { type BaseDrizzleQuery, type DefineDrizzleAbility, type DrizzleModel, type DrizzleQueryFactory, type Subjects as DrizzleSubjects, type ExtractModelName, type Model, ParsingQueryError, type QueryInput, type Subjects$1 as Subjects, type WhereInput, accessibleBy, type createAbilityFactory, type createAccessibleByFactory, createDrizzleAbility, createDrizzleAbilityFor, drizzleQuery, every, none, some };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{Ability as e,AbilityBuilder as t,ForbiddenError as n,fieldPatternMatcher as r}from"@casl/ability";import{CompoundCondition as i,FieldCondition as a,NULL_CONDITION as o,ObjectQueryParser as ee,buildAnd as te,createTranslatorFactory as s}from"@ucast/core";import{and as c,compare as l,createJsInterpreter as u,gt as ne,gte as re,lt as ie,lte as ae,or as d,within as oe}from"@ucast/js";import{rulesToCondition as se}from"@casl/ability/extra";import{operators as f,sql as p}from"drizzle-orm";const ce=(e,t,{get:n})=>n(t,e.field).startsWith(e.value),le=(e,t,{get:n})=>n(t,e.field).toLowerCase().startsWith(e.value.toLowerCase()),m=(e,t,{get:n})=>n(t,e.field).endsWith(e.value),ue=(e,t,{get:n})=>n(t,e.field).toLowerCase().endsWith(e.value.toLowerCase()),h=(e,t,{get:n})=>n(t,e.field).includes(e.value),g=(e,t,{get:n})=>n(t,e.field).toLowerCase().includes(e.value.toLowerCase()),_=e=>{let t=`^${e.replaceAll(/[.*+?^${}()|[\]\\]/gu,`\\$&`).replaceAll(`%`,`.*`).replaceAll(`_`,`.`)}$`;return new RegExp(t,`u`)},v=(e,t,{get:n})=>{let r=n(t,e.field);return typeof r==`string`?_(e.value).test(r):!1},y=(e,t,{get:n})=>{let r=n(t,e.field);return typeof r==`string`?_(e.value.toLowerCase()).test(r.toLowerCase()):!1},b=(e,t,{get:n})=>{let r=n(t,e.field);return(Array.isArray(r)&&r.length===0)===e.value},de=(e,t,{get:n})=>{let r=n(t,e.field);return Array.isArray(r)&&r.includes(e.value)},fe=(e,t,{get:n})=>{let r=n(t,e.field);return Array.isArray(r)&&e.value.some(e=>r.includes(e))},pe=(e,t,{get:n})=>{let r=n(t,e.field);return Array.isArray(r)&&e.value.every(e=>r.includes(e))},me=(e,t,{get:n})=>{let r=n(t,e.field);return Array.isArray(r)&&e.value.some(e=>r.includes(e))},he=(e,t,{get:n})=>{let r=n(t,e.field);return Array.isArray(r)&&e.value.every(e=>r.includes(e))},x=(e,t,{get:n})=>{let r=n(t,e.field);return Array.isArray(r)&&r.every(t=>e.value.includes(t))},S=(e,t,{get:n,interpret:r})=>{let i=n(t,e.field);return Array.isArray(i)&&i.length>0&&i.every(t=>r(e.value,t))},C=(e,t,{get:n,interpret:r})=>{let i=n(t,e.field);return Array.isArray(i)&&i.some(t=>r(e.value,t))},w=(e,t,{get:n,interpret:r})=>{let i=n(t,e.field);return typeof i==`object`&&!!i&&r(e.value,i)},T=(e,t,{interpret:n})=>e.value.every(e=>!n(e,t)),E=(e,t,{get:n})=>n(t,e.field)!==void 0===e.value,D=(e,t,{get:n})=>n(t,e.field)===null===e.value,O=(e,t,{get:n})=>n(t,e.field)!==null===e.value;function k(e){return e&&typeof e==`object`?e.valueOf():e}const A=e=>typeof e==`object`&&!!e&&(Object.getPrototypeOf(e)===Object.prototype||Object.getPrototypeOf(e)===null),j=(e,t)=>{if(Object.is(e,t))return!0;if(e instanceof Date&&t instanceof Date)return e.getTime()===t.getTime();if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,n)=>j(e,t[n]));if(A(e)&&A(t)){let n=Object.keys(e),r=Object.keys(t);return n.length===r.length&&n.every(n=>Object.hasOwn(t,n)&&j(e[n],t[n]))}return!1},M=e=>{let t=typeof e;return e===null||t===`string`||t===`number`||t===`boolean`||t===`bigint`||e instanceof Date},N=(e,t,{get:n,compare:r})=>{let i=n(t,e.field),a=e.value;return j(i,a)?!0:M(i)&&M(a)?r(i,a)===0:!1},P=u({eq:N,equals:N,notEquals:(e,t,n)=>!N(e,t,n),in:oe,lt:ie,lte:ae,gt:ne,gte:re,startsWith:ce,istartsWith:le,endsWith:m,iendsWith:ue,contains:h,icontains:g,like:v,ilike:y,isEmpty:b,has:de,hasSome:fe,hasEvery:pe,arrayOverlaps:me,arrayContained:x,arrayContains:he,and:c,or:d,AND:c,OR:d,NOT:T,every:S,some:C,is:w,isSet:E,isNull:D,isNotNull:O,RAW:()=>!0},{get:(e,t)=>e[t],compare:(e,t)=>l(k(e),k(t))});var F=class extends Error{name=`ParsingQueryError`;static invalidArgument(e,t,n){let r=`${typeof t}(${JSON.stringify(t,null,2)})`;return new this(`"${e}" expects to receive ${n} but instead got "${r}"`)}};const I=e=>typeof e==`object`&&!!e&&(Object.getPrototypeOf(e)===Object.prototype||Object.getPrototypeOf(e)===null),L={type:`field`},R={type:`field`,validate:void L.validate,parse(e,t,{field:n}){return new a(`notEquals`,n,t)}},ge={type:`field`,parse:((e,t,{hasOperators:n,field:r,parse:o})=>Array.isArray(t)||!I(t)||!n(t)?new a(`notEquals`,r,t):new i(`NOT`,[o(t,{field:r})]))},z={type:`field`,validate(e,t){if(!Array.isArray(t))throw F.invalidArgument(e.name,t,`an array`)}},B={type:`field`,validate(e,t){let n=typeof t;if(!(n===`string`||n===`number`&&Number.isFinite(t)||t instanceof Date))throw F.invalidArgument(e.name,t,`comparable value`)}},V=new Set([`insensitive`,`default`]),_e={type:`field`,validate(e,t){if(!V.has(t))throw F.invalidArgument(e.name,t,`one of ${[...V].join(`, `)}`)},parse:()=>o},H={type:`field`,validate(e,t){if(typeof t!=`string`)throw F.invalidArgument(e.name,t,`string`)},parse(e,t,{query:n,field:r}){return new a(n.mode===`insensitive`?`i${e.name}`:e.name,r,t)}},U={type:`field`,validate(e,t){if(typeof t!=`string`)throw F.invalidArgument(e.name,t,`string`)},parse(e,t,{query:n,field:r}){return e.name===`ilike`||n.mode===`insensitive`?new a(`ilike`,r,t):new a(`like`,r,t)}},W={type:`compound`,validate(e,t){if(!t||typeof t!=`object`)throw F.invalidArgument(e.name,t,`an array or object`)},parse(e,t,{parse:n}){let r=(Array.isArray(t)?t:[t]).map(e=>n(e));return new i(e.name,r)}},G={type:`field`,validate(e,t){if(typeof t!=`boolean`)throw F.invalidArgument(e.name,t,`a boolean`)}},ve={type:`field`},K={type:`field`,validate(e,t){if(!Array.isArray(t))throw F.invalidArgument(e.name,t,`an array`)}},q={type:`field`,validate(e,t){if(!Array.isArray(t))throw F.invalidArgument(e.name,t,`an array`)}},J={type:`field`,parse(e,t,{field:n,parse:r}){if(!I(t))throw F.invalidArgument(e.name,t,`a query for nested relation`);return new a(e.name,n,r(t))}},Y=(e,t)=>{let n=t.parse?.bind(t);return n?{...t,parse(t,r,a){let o=n(t,r,a);if(o.operator!==t.name)throw Error(`Cannot invert "${e}" operator parser because it returns a complex Condition`);return o.operator=e,new i(`NOT`,[o])}}:{...t,parse(t,n,r){return new i(`NOT`,[new a(e,r.field,n)])}}},ye={eq:L,ne:R,not:ge,in:z,notIn:Y(`in`,z),lt:B,lte:B,gt:B,gte:B,$lt:B,$lte:B,$gt:B,$gte:B,$in:z,$nin:Y(`in`,z),mode:_e,startsWith:H,endsWith:H,contains:H,like:U,ilike:U,notLike:{type:`field`,parse:((e,t,{field:n,parse:r})=>new i(`NOT`,[r({like:t},{field:n})]))},notIlike:{type:`field`,parse:((e,t,{field:n,parse:r})=>new i(`NOT`,[r({ilike:t},{field:n})]))},isNull:G,isNotNull:G,isEmpty:G,has:ve,hasSome:K,hasEvery:K,arrayOverlaps:q,arrayContained:q,arrayContains:q,NOT:W,AND:W,OR:W,every:J,some:J,none:Y(`some`,J),is:J,isNot:Y(`is`,J),isSet:G,RAW:{type:`field`,parse(e,t){return new a(`RAW`,`RAW`,t)}}},X=s(new class extends ee{constructor(){super(ye,{defaultOperatorName:`eq`})}normalizeEqOperator(e){if(Array.isArray(e))return e.map(e=>this.normalizeEqOperator(e));if(!I(e))return e;let t=Object.keys(e);if(t.length===1&&t[0]===`eq`)return this.normalizeEqOperator(e.eq);let n={};for(let[t,r]of Object.entries(e))n[t]=this.normalizeEqOperator(r);return n}parse(e,t){let n=this.normalizeEqOperator(e);return t?.field?te(this.parseFieldOperators(t.field,n)):super.parse(n)}}().parse,P);function Z(e){if(typeof e!=`object`||!e)return e;if(Array.isArray(e))return e.map(Z);let t={};for(let[n,r]of Object.entries(e)){if(n===`OR`||n===`AND`){t[n]=Z(r);continue}if(n===`RAW`){t[n]=r;continue}if(n.startsWith(`$`)){let e=n.slice(1);t[e]=Z(r);continue}t[n]=typeof r==`object`&&r&&!Array.isArray(r)?Z(r):r}return t}const be={get(e,t){let r=se(e._ability.rulesFor(e._action,t),e=>e.inverted?{NOT:e.conditions}:e.conditions,{and:e=>({AND:e}),or:e=>({OR:e}),empty:()=>({})});if(r===null){let r=n.from(e._ability).setMessage(`It's not allowed to run "${e._action}" on "${t}"`);throw r.action=e._action,r.subjectType=t,r.subject=t,r}let i=Object.create(null);if(r.OR&&Array.isArray(r.OR)&&r.OR.length===1){let[e]=r.OR;Object.assign(i,e)}else r.OR&&(i.OR=r.OR);return Z(i)}};function xe(e,t){return new Proxy({_ability:e,_action:t},be)}function Q(){function t(t=[],n={}){return new e(t,{...n,conditionsMatcher:X,fieldMatcher:r})}return t}const $=()=>new Proxy({},{get:(e,t)=>p.raw(String(t))});function Se(e,t){if(e&&typeof e==`object`&&`_`in e&&`columns`in e._){if(!t)throw Error(`Condition is required when table is provided`);let{columns:n}=e._;return{RAW:t({...f,columns:n})}}let n=e;if(typeof n==`function`){let e=$();return{RAW:n({...f,columns:e})}}return{RAW:n}}function Ce(e,t){if(e&&typeof e==`object`&&`_`in e&&`columns`in e._){if(!t)throw Error(`Condition is required when table is provided`);let{columns:n}=e._;return{RAW:t({...f,columns:n})}}let n=e;if(typeof n==`function`){let e=$();return{RAW:n({...f,columns:e})}}return{RAW:n}}function we(e,t){if(!e)return{RAW:p`1=0`};if(e&&typeof e==`object`&&`_`in e&&`columns`in e._){if(!t)return{RAW:p`1=0`};let{columns:n}=e._;return{RAW:t({...f,columns:n})}}let n=e;if(typeof n==`function`){let e=$();return{RAW:n({...f,columns:e})}}return{RAW:n}}function Te(){return Q()}function Ee(e){let n=new t(Q());return e(n.can.bind(n),n.cannot.bind(n)),n.build()}export{F as ParsingQueryError,xe as accessibleBy,Ee as createDrizzleAbility,Te as createDrizzleAbilityFor,X as drizzleQuery,Ce as every,we as none,Se as some};
|
package/package.json
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noxify/casl-drizzle",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Drizzle-ORM adapter for CASL - generate Drizzle where inputs from CASL abilities",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"ability",
|
|
7
|
+
"acl",
|
|
8
|
+
"authorization",
|
|
9
|
+
"casl",
|
|
6
10
|
"drizzle",
|
|
7
11
|
"drizzle-orm",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
12
|
+
"permissions",
|
|
13
|
+
"query-builder",
|
|
14
|
+
"rbac",
|
|
15
|
+
"relational",
|
|
16
|
+
"type-safe",
|
|
17
|
+
"typescript"
|
|
10
18
|
],
|
|
19
|
+
"license": "MIT",
|
|
11
20
|
"repository": {
|
|
12
21
|
"type": "git",
|
|
13
22
|
"url": "https://github.com/noxify/casl-drizzle"
|
|
14
23
|
},
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
"name": "Marcus Reinhardt",
|
|
19
|
-
"email": "webstone@gmail.com"
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"name": "Guilherme AraΓΊjo",
|
|
23
|
-
"email": "arauujogui@gmail.com"
|
|
24
|
-
}
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
25
26
|
],
|
|
26
27
|
"type": "module",
|
|
28
|
+
"types": "./dist/index.d.mts",
|
|
27
29
|
"exports": {
|
|
28
30
|
".": {
|
|
29
31
|
"types": "./dist/index.d.mts",
|
|
@@ -31,61 +33,56 @@
|
|
|
31
33
|
"default": "./src/index.ts"
|
|
32
34
|
}
|
|
33
35
|
},
|
|
34
|
-
"types": "./dist/index.d.mts",
|
|
35
|
-
"files": [
|
|
36
|
-
"dist"
|
|
37
|
-
],
|
|
38
36
|
"dependencies": {
|
|
39
|
-
"@ucast/core": "
|
|
40
|
-
"@ucast/js": "
|
|
37
|
+
"@ucast/core": "2.0.0",
|
|
38
|
+
"@ucast/js": "4.0.1"
|
|
41
39
|
},
|
|
42
|
-
"prettier": "./prettier.config.mjs",
|
|
43
40
|
"devDependencies": {
|
|
44
|
-
"@casl/ability": "
|
|
45
|
-
"@changesets/cli": "2.
|
|
46
|
-
"@
|
|
47
|
-
"@eslint/js": "9.39.2",
|
|
48
|
-
"@ianvs/prettier-plugin-sort-imports": "4.7.1",
|
|
49
|
-
"@testcontainers/postgresql": "^11.11.0",
|
|
41
|
+
"@casl/ability": "7.0.0",
|
|
42
|
+
"@changesets/cli": "2.31.0",
|
|
43
|
+
"@electric-sql/pglite": "0.4.6",
|
|
50
44
|
"@types/js-yaml": "4.0.9",
|
|
51
|
-
"@types/node": "24.
|
|
52
|
-
"@vitest/coverage-v8": "4.
|
|
53
|
-
"dedent": "1.7.
|
|
54
|
-
"drizzle-kit": "
|
|
55
|
-
"drizzle-orm": "
|
|
56
|
-
"eslint": "9.39.2",
|
|
57
|
-
"eslint-plugin-import": "2.32.0",
|
|
58
|
-
"eslint-plugin-package-json": "0.88.2",
|
|
45
|
+
"@types/node": "24.12.4",
|
|
46
|
+
"@vitest/coverage-v8": "4.1.7",
|
|
47
|
+
"dedent": "1.7.2",
|
|
48
|
+
"drizzle-kit": "1.0.0-rc.3",
|
|
49
|
+
"drizzle-orm": "1.0.0-rc.3",
|
|
59
50
|
"json-schema-to-typescript": "15.0.4",
|
|
60
|
-
"jsonc-eslint-parser": "2.4.2",
|
|
61
51
|
"node-pty": "1.1.0",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"tsdown": "0.
|
|
65
|
-
"tsx": "4.
|
|
66
|
-
"typescript": "
|
|
67
|
-
"
|
|
68
|
-
"vitest": "4.
|
|
52
|
+
"oxfmt": "0.52.0",
|
|
53
|
+
"oxlint": "1.67.0",
|
|
54
|
+
"tsdown": "0.22.1",
|
|
55
|
+
"tsx": "4.22.3",
|
|
56
|
+
"typescript": "6.0.3",
|
|
57
|
+
"ultracite": "7.8.0",
|
|
58
|
+
"vitest": "4.1.7"
|
|
69
59
|
},
|
|
70
60
|
"peerDependencies": {
|
|
71
|
-
"@casl/ability": "^
|
|
72
|
-
"drizzle-orm": ">=1.0.0-
|
|
61
|
+
"@casl/ability": "^7.0.0",
|
|
62
|
+
"drizzle-orm": ">=1.0.0-rc.3"
|
|
73
63
|
},
|
|
74
64
|
"engines": {
|
|
75
|
-
"node": ">=
|
|
65
|
+
"node": ">=24"
|
|
76
66
|
},
|
|
67
|
+
"authors": [
|
|
68
|
+
{
|
|
69
|
+
"name": "Marcus Reinhardt",
|
|
70
|
+
"email": "webstone@gmail.com"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
77
73
|
"scripts": {
|
|
78
74
|
"build": "tsdown",
|
|
79
75
|
"ci:publish": "pnpm build && pnpm publish -r --access public --publish-branch main && pnpm changeset tag",
|
|
80
76
|
"ci:version": "pnpm changeset version && pnpm install --no-frozen-lockfile && git add .",
|
|
81
77
|
"cs": "changeset",
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
78
|
+
"lint": "ultracite check",
|
|
79
|
+
"lint:fix": "ultracite fix && pnpm format:fix",
|
|
80
|
+
"format": "oxfmt --check",
|
|
81
|
+
"format:fix": "oxfmt .",
|
|
86
82
|
"test": "vitest --run",
|
|
87
83
|
"test:coverage": "vitest --coverage",
|
|
88
84
|
"test:watch": "vitest",
|
|
89
|
-
"typecheck": "tsc --noEmit"
|
|
85
|
+
"typecheck": "tsc --noEmit",
|
|
86
|
+
"deps:update": "pnpm update -i -L -r"
|
|
90
87
|
}
|
|
91
88
|
}
|