@noxify/casl-drizzle 0.0.1-beta.3 β†’ 0.1.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 CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2026 Marcus Reinhardt
4
+ Portions Copyright (c) Guilherme AraΓΊjo
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,107 +1,163 @@
1
- > work in progress :)
2
-
3
- # casl-drizzle
1
+ # @noxify/casl-drizzle
4
2
 
5
3
  CASL integration for Drizzle ORM - Add type-safe authorization to your database queries
6
4
 
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
12
+
7
13
  ## Install
8
14
 
9
15
  ```sh
10
- npm install ucastle @casl/ability
16
+ npm install @noxify/casl-drizzle @casl/ability drizzle-orm@beta
17
+ ```
18
+
19
+ ```sh
20
+ pnpm add @noxify/casl-drizzle @casl/ability drizzle-orm@beta
11
21
  ```
12
22
 
13
- ## Quick Start
23
+ ## Setup
14
24
 
15
- Define your abilities using Drizzle's query types directly:
25
+ Define your Drizzle schema and relations:
16
26
 
17
27
  ```typescript
18
- import type { QueryInput } from "ucastle"
19
- import { integer, pgTable, text } from "drizzle-orm/pg-core"
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 schema = { users }
30
-
31
- // Extract query types for your tables
32
- type UserQuery = QueryInput<typeof schema, "users">
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
- // Use with accessibleBy to get database filters
42
- const filters = accessibleBy(ability, "read")
43
- const readableUsers = await db.query.users.findMany({ where: filters.users })
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
- ## Features
48
+ ## Usage
47
49
 
48
- - πŸ”’ **Type-safe authorization** - Full TypeScript support with Drizzle types
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
- ## With Relations
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
- For schemas with relations, use `RelationalQueryInput`:
57
+ type PostQuery = QueryInput<typeof relations, "posts">
58
+ type UserQuery = QueryInput<typeof relations, "users">
58
59
 
59
- ```typescript
60
- import type { RelationalQueryInput } from "ucastle"
61
- import { defineRelations } from "drizzle-orm"
60
+ const currentUserId = 1
62
61
 
63
- const posts = pgTable("posts", {
64
- id: integer().primaryKey(),
65
- title: text().notNull(),
66
- authorId: integer().notNull(),
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
- const relations = defineRelations({ users, posts }, (r) => ({
70
- users: { posts: r.many(posts) },
71
- posts: { author: r.one(users, { fields: [posts.authorId], references: [users.id] }) },
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
- type PostQuery = RelationalQueryInput<typeof relations, "posts">
93
+ ## Behavior Notes
75
94
 
76
- const ability = defineAbility<{ posts: PostQuery }>((can) => {
77
- can("read", "posts", { published: true })
78
- can("update", "posts", { authorId: 1 })
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
- ## Alternative: AbilityBuilder
109
+ Use when you need to enforce a condition across all related records that exist. For "users with no posts" scenarios, use `none()` instead.
110
+
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.
83
114
 
84
- If you prefer the traditional AbilityBuilder pattern:
115
+ **βœ… Simple case (recommended):**
85
116
 
86
117
  ```typescript
87
- import type { DefineAbility } from "ucastle"
88
- import { AbilityBuilder } from "@casl/ability"
89
- import { createDrizzleAbilityFor } from "ucastle"
118
+ can("read", "posts", { comments: none() })
119
+ ```
120
+
121
+ **⚠️ Complex paths - Known Issue:**
122
+ When filtering through nested relations, `none()` semantics can be inverted:
123
+
124
+ ```typescript
125
+ // ❌ Behavior may be unexpected:
126
+ can("read", "posts", {
127
+ comments: none({ author: { id: adminId } }),
128
+ })
129
+ ```
90
130
 
91
- type AppAbility = DefineAbility<{ users: UserQuery }>
131
+ **βœ… Solution - Use Drizzle's type-safe subquery:**
92
132
 
93
- const { can, cannot, build } = new AbilityBuilder<AppAbility>(createDrizzleAbilityFor())
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
+ ```
94
145
 
95
- can("read", "users", { id: 1 })
96
- cannot("delete", "users")
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.
97
147
 
98
- const ability = build()
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
- ## Documentation
157
+ For critical authorization rules involving complex relation filters, always use explicit `RAW` SQL to ensure predictable behavior.
102
158
 
103
- See [SIMPLIFIED_API.md](./SIMPLIFIED_API.md) for detailed examples and patterns.
159
+ ## Acknowledgements
104
160
 
105
- ## License
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
- MIT
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
1
  import { AbilityOptions, AbilityOptionsOf, AbilityTuple, AnyAbility, ForcedSubject, PureAbility, RawRuleFrom, RawRuleOf, hkt } from "@casl/ability";
2
- import * as _ucast_core0 from "@ucast/core";
3
- import { DBQueryConfig, SchemaEntry, TablesRelationalConfig } from "drizzle-orm/relations";
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: _ucast_core0.Condition;
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,136 +20,165 @@ 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
- declare const Ι΅drizzleTypes: unique symbol;
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
- * Utility type to recursively remove the RAW property from Drizzle query types.
32
- * RAW is used internally by Drizzle for SQL functions but shouldn't be exposed in our API.
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 OmitRaw<T> = T extends object ? { [K in keyof T as K extends "RAW" ? never : K]: T[K] extends object ? T[K] extends any[] ? OmitRaw<T[K][number]>[] : OmitRaw<T[K]> : T[K] } : T;
50
+ type DrizzleModel = Model<Record<string, unknown>, string>;
35
51
  /**
36
- * Query input for tables WITH relations.
37
- * Extracts the complete where type including relation filters from Drizzle's DBQueryConfig.
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 RelationalQueryInput<TSchema extends TablesRelationalConfig, TTableName extends keyof TSchema> = OmitRaw<Exclude<KnownKeysOnly<DBQueryConfig<"many", TSchema, TSchema[TTableName]>, DBQueryConfig<"many", TSchema, TSchema[TTableName]>>["where"], undefined>>;
56
+ type WhereInput = Record<string, unknown>;
51
57
  /**
52
- * Query input for tables WITHOUT relations.
53
- * Supports both field conditions and compound operators (AND/OR/NOT) at root level.
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 type { QueryInput } from "ucastle"
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
- * // Field conditions
63
- * const q1: UserQuery = { id: 1, name: "John" }
71
+ * const relations = defineRelations({ users, posts }, (r) => ({
72
+ * users: { posts: r.many(posts) },
73
+ * posts: { author: r.one(users) }
74
+ * }))
64
75
  *
65
- * // With operators
66
- * const q2: UserQuery = { id: { gte: 1 }, AND: [{ name: "John" }] }
76
+ * type PostQuery = QueryInput<typeof relations, "posts">
67
77
  *
68
- * // Compound operators
69
- * const q3: UserQuery = { OR: [{ age: 18 }, { role: "admin" }] }
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 Record<string, SchemaEntry>, TTableName extends keyof TSchema> = OmitRaw<{ [K in keyof (TSchema[TTableName] extends {
73
- $inferSelect: infer S;
74
- } ? S : never)]?: any } & {
75
- AND?: QueryInput<TSchema, TTableName>[];
76
- OR?: QueryInput<TSchema, TTableName>[];
77
- NOT?: QueryInput<TSchema, TTableName>;
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
- * Helper type to create CASL subjects from a mapping of table names to query types.
81
- * Similar to @casl/prisma's Subjects pattern.
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
112
  * import { PureAbility } from "@casl/ability"
86
- * import type { RelationalQueryInput, Subjects } from "ucastle"
113
+ * import type { QueryInput, Subjects } from "@noxify/casl-drizzle"
87
114
  *
88
- * type UserQuery = RelationalQueryInput<typeof relations, "users">
89
- * type PostQuery = RelationalQueryInput<typeof relations, "posts">
115
+ * type QueryMap = {
116
+ * users: QueryInput<typeof relations, "users">
117
+ * posts: QueryInput<typeof relations, "posts">
118
+ * }
90
119
  *
91
- * type AppAbility = PureAbility<[string, Subjects<{
92
- * users: UserQuery,
93
- * posts: PostQuery
94
- * }>]>
120
+ * type AppAbility = PureAbility<[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
- * Custom Ability type with subject-specific conditions.
100
- * Similar to @casl/prisma's PrismaAbility.
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
- * This type ensures that conditions are properly typed based on the subject mapping.
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 "ucastle"
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: UserQuery
110
- * posts: PostQuery
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$1<T> = PureAbility<[string, Subjects$1<T>], T[keyof T]>;
146
+ type DrizzleAbility<T, TActions extends string = string> = PureAbility<[TActions, Subjects$1<T>], T[keyof T]>;
117
147
  /**
118
- * Helper type to create a fully-typed DrizzleAbility from a subject mapping.
119
- * Provides subject-specific autocomplete in `can()` and `cannot()` methods
120
- * when used with `defineAbility()`.
121
- * Works with both `type` and `interface`.
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 { defineAbility, type DefineAbility } from "ucastle"
157
+ * import { createDrizzleAbility, type DefineDrizzleAbility } from "@noxify/casl-drizzle"
158
+ * import type { QueryInput } from "@noxify/casl-drizzle"
126
159
  *
127
- * // Works with type
128
- * type SubjectMap = {
129
- * users: UserQuery
130
- * posts: PostQuery
131
- * }
160
+ * type AllowedAction = "read" | "create" | "update" | "delete"
132
161
  *
133
- * // Also works with interface
134
- * interface SubjectMap {
135
- * users: UserQuery
136
- * posts: PostQuery
162
+ * type SubjectMap = {
163
+ * users: QueryInput<typeof relations, "users">
164
+ * posts: QueryInput<typeof relations, "posts">
137
165
  * }
138
166
  *
139
- * // Use with defineAbility for subject-specific autocomplete:
140
- * const ability = defineAbility<SubjectMap>((can, cannot) => {
141
- * can("read", "users", { id: 1 }) // βœ… Only user fields!
142
- * can("read", "posts", { authorId: 1 }) // βœ… Only post fields!
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 DefineAbility<T> = DrizzleAbility$1<T>;
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
180
  declare const createAccessibleByFactory: <TResult extends Record<string, unknown>, TDrizzleQuery>() => <TAbility extends PureAbility<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>;
153
182
  declare function accessibleBy<TAbility extends PureAbility<any, any>>(ability: TAbility, action?: TAbility["rules"][number]["action"]): Record<string, WhereInput>;
154
183
  //#endregion
155
184
  //#region src/factories/create-ability.d.ts
@@ -160,9 +189,152 @@ declare function createAbilityFactory<TModelName extends string, TDrizzleQuery e
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 DefineAbility } from "ucastle"
346
+ * import { createDrizzleAbilityFor, type DefineDrizzleAbility } from "@noxify/casl-drizzle"
347
+ *
348
+ * type AllowedAction = "read" | "create" | "update" | "delete"
175
349
  *
176
- * type AppAbility = DefineAbility<{
350
+ * type AppAbility = DefineDrizzleAbility<{
177
351
  * users: UserQuery
178
352
  * posts: PostQuery
179
- * }>
353
+ * }, AllowedAction>
180
354
  *
181
- * const { can, build } = new AbilityBuilder<AppAbility>(createDrizzleAbilityFor())
355
+ * const { can, build } = new AbilityBuilder<AppAbility>(
356
+ * createDrizzleAbilityFor<{
357
+ * users: UserQuery
358
+ * posts: PostQuery
359
+ * }, AllowedAction>(),
360
+ * )
182
361
  * ```
183
362
  */
184
363
  declare function createDrizzleAbilityFor(): new (...args: ConstructorParameters<typeof PureAbility>) => AnyAbility;
364
+ declare function createDrizzleAbilityFor<TSubject, TActions extends string = string>(): new (...args: ConstructorParameters<typeof PureAbility>) => DrizzleAbility<TSubject, TActions>;
185
365
  /**
186
- * Create a type-safe ability with subject-specific conditions.
187
- * This provides better autocomplete than using AbilityBuilder directly.
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 { defineAbility } from "ucastle"
373
+ * import { createDrizzleAbility } from "@noxify/casl-drizzle"
374
+ * import type { QueryInput } from "@noxify/casl-drizzle"
192
375
  *
193
- * const ability = defineAbility<{
194
- * users: UserQuery
195
- * posts: PostQuery
196
- * }>((can, cannot) => {
197
- * can("read", "users", { id: 1 }) // βœ… Only user fields
198
- * can("read", "posts", { authorId: 1 }) // βœ… Only post fields
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 defineAbility<T>(define: (can: <S extends keyof T & string>(action: string, subject: S, conditions?: T[S]) => void, cannot: <S extends keyof T & string>(action: string, subject: S, conditions?: T[S]) => void) => void): PureAbility<[string, any], T[keyof T]>;
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 DefineAbility, DrizzleAbility, type DrizzleModel, type DrizzleQueryFactory, type Subjects as DrizzleSubjects, type ExtractModelName, type Model, ParsingQueryError, type QueryInput, type RelationalQueryInput, type Subjects$1 as Subjects, type WhereInput, accessibleBy, createAbilityFactory, createAccessibleByFactory, createDrizzleAbilityFor, defineAbility, drizzleQuery };
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{AbilityBuilder as e,ForbiddenError as t,PureAbility as n,fieldPatternMatcher as r}from"@casl/ability";import{CompoundCondition as i,FieldCondition as a,NULL_CONDITION as o,ObjectQueryParser as s,buildAnd as c,createTranslatorFactory as l}from"@ucast/core";import{and as u,compare as d,createJsInterpreter as f,eq as p,gt as m,gte as h,lt as ee,lte as te,ne as g,or as _,within as v}from"@ucast/js";import{rulesToQuery as y}from"@casl/ability/extra";const b=(e,t,{get:n})=>n(t,e.field).startsWith(e.value),x=(e,t,{get:n})=>n(t,e.field).toLowerCase().startsWith(e.value.toLowerCase()),ne=(e,t,{get:n})=>n(t,e.field).endsWith(e.value),S=(e,t,{get:n})=>n(t,e.field).toLowerCase().endsWith(e.value.toLowerCase()),C=(e,t,{get:n})=>n(t,e.field).includes(e.value),w=(e,t,{get:n})=>n(t,e.field).toLowerCase().includes(e.value.toLowerCase()),T=(e,t,{get:n})=>{let r=n(t,e.field);return(Array.isArray(r)&&r.length===0)===e.value},E=(e,t,{get:n})=>{let r=n(t,e.field);return Array.isArray(r)&&r.includes(e.value)},D=(e,t,{get:n})=>{let r=n(t,e.field);return Array.isArray(r)&&e.value.some(e=>r.includes(e))},O=(e,t,{get:n})=>{let r=n(t,e.field);return Array.isArray(r)&&e.value.every(e=>r.includes(e))},k=(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))},A=(e,t,{get:n,interpret:r})=>{let i=n(t,e.field);return Array.isArray(i)&&i.some(t=>r(e.value,t))},j=(e,t,{get:n,interpret:r})=>{let i=n(t,e.field);return typeof i==`object`&&!!i&&r(e.value,i)},M=(e,t,{interpret:n})=>e.value.every(e=>!n(e,t)),N=(e,t,{get:n})=>n(t,e.field)!==void 0;function P(e){return e&&typeof e==`object`?e.valueOf():e}const F=f({equals:p,notEquals:g,in:v,lt:ee,lte:te,gt:m,gte:h,startsWith:b,istartsWith:x,endsWith:ne,iendsWith:S,contains:C,icontains:w,isEmpty:T,has:E,hasSome:D,hasEvery:O,and:u,or:_,AND:u,OR:_,NOT:M,every:k,some:A,is:j,isSet:N},{get:(e,t)=>e[t],compare:(e,t)=>d(P(e),P(t))});var I=class extends Error{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 L=e=>typeof e==`object`&&!!e&&(Object.getPrototypeOf(e)===Object.prototype||Object.getPrototypeOf(e)===null),re={type:`field`,validate(e,t){if(Array.isArray(t)||L(t))throw new I(`"${e.name}" does not supports comparison of arrays and objects`)}},R={type:`field`,parse:((e,t,{hasOperators:n,field:r,parse:o})=>{if(L(t)&&!n(t)||Array.isArray(t))throw new I(`"${e.name}" does not supports comparison of arrays and objects`);return L(t)?new i(`NOT`,[o(t,{field:r})]):new a(`notEquals`,r,t)})},z={type:`field`,validate(e,t){if(!Array.isArray(t))throw I.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 I.invalidArgument(e.name,t,`comparable value`)}},V=new Set([`insensitive`,`default`]),H={type:`field`,validate(e,t){if(!V.has(t))throw I.invalidArgument(e.name,t,`one of ${Array.from(V).join(`, `)}`)},parse:()=>o},U={type:`field`,validate(e,t){if(typeof t!=`string`)throw I.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)}},W={type:`compound`,validate(e,t){if(!t||typeof t!=`object`)throw I.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 I.invalidArgument(e.name,t,`a boolean`)}},K={type:`field`},q={type:`field`,validate(e,t){if(!Array.isArray(t))throw I.invalidArgument(e.name,t,`an array`)}},J={type:`field`,parse(e,t,{field:n,parse:r}){if(!L(t))throw I.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)])}}},X={equals:re,not:R,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:H,startsWith:U,endsWith:U,contains:U,isEmpty:G,has:K,hasSome:q,hasEvery:q,NOT:W,AND:W,OR:W,every:J,some:J,none:Y(`some`,J),is:J,isNot:Y(`is`,J),isSet:G},Z=l(new class extends s{constructor(){super(X,{defaultOperatorName:`equals`})}parse(e,t){return t?.field?c(this.parseFieldOperators(t.field,e)):super.parse(e)}}().parse,F);function Q(e){if(typeof e!=`object`||!e)return e;if(Array.isArray(e))return e.map(Q);let t={};for(let[n,r]of Object.entries(e)){if(n===`OR`||n===`AND`){t[n]=Q(r);continue}if(n.startsWith(`$`)){let e=n.substring(1);t[e]=Q(r);continue}typeof r==`object`&&r&&!Array.isArray(r)?t[n]=Q(r):t[n]=r}return t}const ie={get(e,n){let r=y(e._ability,e._action,n,e=>e.inverted?{NOT:e.conditions}:e.conditions);if(r===null){let r=t.from(e._ability).setMessage(`It's not allowed to run "${e._action}" on "${n}"`);throw r.action=e._action,r.subjectType=r.subject=n,r}let i=Object.create(null);if(r.$or&&Array.isArray(r.$or)&&r.$or.length===1){let e=r.$or[0];Object.assign(i,e)}else r.$or&&(i.OR=r.$or);return r.$and&&(i.AND=r.$and),Q(i)}};function ae(e,t=`read`){return new Proxy({_ability:e,_action:t},ie)}function $(){function e(e=[],t={}){return new n(e,{...t,conditionsMatcher:Z,fieldMatcher:r})}return e}function oe(){return $()}function se(t){let n=new e($());return t((e,t,r)=>n.can(e,t,r),(e,t,r)=>n.cannot(e,t,r)),n.build()}var ce=class extends n{constructor(e,t){super(e,{conditionsMatcher:Z,fieldMatcher:r,...t})}};export{ce as DrizzleAbility,I as ParsingQueryError,ae as accessibleBy,oe as createDrizzleAbilityFor,se as defineAbility,Z as drizzleQuery};
1
+ import{AbilityBuilder as e,ForbiddenError as t,PureAbility 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{rulesToQuery 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,n){let r=se(e._ability,e._action,n,e=>e.inverted?{NOT:e.conditions}:e.conditions);if(r===null){let r=t.from(e._ability).setMessage(`It's not allowed to run "${e._action}" on "${n}"`);throw r.action=e._action,r.subjectType=n,r.subject=n,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 r.$and&&(i.AND=r.$and),Z(i)}};function xe(e,t){return new Proxy({_ability:e,_action:t},be)}function Q(){function e(e=[],t={}){return new n(e,{...t,conditionsMatcher:X,fieldMatcher:r})}return e}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(t){let n=new e(Q());return t(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.1-beta.3",
3
+ "version": "0.1.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
- "casl",
9
- "ucast"
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
- "license": "MIT",
16
- "authors": [
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": "^1.10.2",
40
- "@ucast/js": "3.1.0"
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": "^6.8.0",
45
- "@changesets/cli": "2.29.8",
46
- "@eslint/compat": "2.0.2",
47
- "@eslint/js": "9.39.2",
48
- "@ianvs/prettier-plugin-sort-imports": "4.7.1",
49
- "@testcontainers/postgresql": "^11.11.0",
41
+ "@casl/ability": "6.8.1",
42
+ "@changesets/cli": "2.31.0",
43
+ "@electric-sql/pglite": "0.4.5",
50
44
  "@types/js-yaml": "4.0.9",
51
- "@types/node": "24.10.13",
52
- "@vitest/coverage-v8": "4.0.18",
53
- "dedent": "1.7.1",
45
+ "@types/node": "24.12.4",
46
+ "@vitest/coverage-v8": "4.1.6",
47
+ "dedent": "1.7.2",
54
48
  "drizzle-kit": "beta",
55
49
  "drizzle-orm": "beta",
56
- "eslint": "9.39.2",
57
- "eslint-plugin-import": "2.32.0",
58
- "eslint-plugin-package-json": "0.88.2",
59
50
  "json-schema-to-typescript": "15.0.4",
60
- "jsonc-eslint-parser": "2.4.2",
61
51
  "node-pty": "1.1.0",
62
- "postgres": "3.4.8",
63
- "prettier": "3.8.1",
64
- "tsdown": "0.20.3",
65
- "tsx": "4.21.0",
66
- "typescript": "^5.9.3",
67
- "typescript-eslint": "8.55.0",
68
- "vitest": "4.0.18"
52
+ "oxfmt": "0.50.0",
53
+ "oxlint": "1.65.0",
54
+ "tsdown": "0.22.0",
55
+ "tsx": "4.22.1",
56
+ "typescript": "^6.0.3",
57
+ "ultracite": "7.7.0",
58
+ "vitest": "4.1.6"
69
59
  },
70
60
  "peerDependencies": {
71
- "@casl/ability": "^6.8.0",
72
- "drizzle-orm": ">=1.0.0-beta.15"
61
+ "@casl/ability": "^6.8.1",
62
+ "drizzle-orm": ">=1.0.0-beta.22"
73
63
  },
74
64
  "engines": {
75
- "node": ">=22"
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
- "format": "prettier --check . --ignore-path ./.gitignore --ignore-path ./.prettierignore",
83
- "format:fix": "prettier --write . --ignore-path ./.gitignore --ignore-path ./.prettierignore",
84
- "lint": "eslint .",
85
- "lint:fix": "eslint . --fix",
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
  }