@cfast/permissions 0.3.0 → 0.4.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/dist/{chunk-NCLN5YBQ.js → chunk-ISTOPBSB.js} +18 -1
- package/dist/{client-DKSiBBkt.d.ts → client-D6goQV8b.d.ts} +66 -9
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +47 -21
- package/dist/index.js +51 -6
- package/llms.txt +35 -16
- package/package.json +1 -1
|
@@ -49,9 +49,26 @@ var ForbiddenError = class extends Error {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
+
var PermissionRegistrationError = class extends Error {
|
|
53
|
+
/** The unresolved subject string that was passed to `grant()`. */
|
|
54
|
+
subject;
|
|
55
|
+
/** The available table names (both JS keys and SQL names) from the schema. */
|
|
56
|
+
availableTables;
|
|
57
|
+
constructor(subject, availableTables) {
|
|
58
|
+
const sorted = [...availableTables].sort();
|
|
59
|
+
const list = sorted.length === 0 ? "<empty schema>" : sorted.map((n) => `"${n}"`).join(", ");
|
|
60
|
+
super(
|
|
61
|
+
`grant() subject "${subject}" does not match any table in the schema. Available tables (by JS key and SQL name): ${list}. Did you forget to pass the schema as the second generic to definePermissions<User, typeof schema>()?`
|
|
62
|
+
);
|
|
63
|
+
this.name = "PermissionRegistrationError";
|
|
64
|
+
this.subject = subject;
|
|
65
|
+
this.availableTables = sorted;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
52
68
|
|
|
53
69
|
export {
|
|
54
70
|
getTableName,
|
|
55
71
|
CRUD_ACTIONS,
|
|
56
|
-
ForbiddenError
|
|
72
|
+
ForbiddenError,
|
|
73
|
+
PermissionRegistrationError
|
|
57
74
|
};
|
|
@@ -17,27 +17,61 @@ type DrizzleSQL = {
|
|
|
17
17
|
getSQL(): unknown;
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
|
-
* A schema map: an object mapping
|
|
20
|
+
* A schema map: an object mapping JS schema keys to Drizzle table references.
|
|
21
21
|
*
|
|
22
22
|
* Typically the result of `import * as schema from "./schema"`. Used as the
|
|
23
23
|
* `TTables` generic parameter for {@link definePermissions}, {@link can}, and
|
|
24
24
|
* the curried {@link grant} callback so that string subjects (e.g.
|
|
25
|
-
* `"projects"`) are constrained to known table names
|
|
25
|
+
* `"projects"` or `"project_versions"`) are constrained to known table names
|
|
26
|
+
* at compile time.
|
|
26
27
|
*/
|
|
27
28
|
type SchemaMap = Record<string, DrizzleTable>;
|
|
28
29
|
/**
|
|
29
|
-
*
|
|
30
|
+
* Structural shape of a Drizzle table's static metadata used purely for
|
|
31
|
+
* type-level SQL-name extraction.
|
|
30
32
|
*
|
|
31
|
-
*
|
|
32
|
-
* `
|
|
33
|
+
* Drizzle tables expose their SQL name on a readonly `_.name` field (see
|
|
34
|
+
* `drizzle-orm/table` `Table._.name`). By narrowing against this shape we can
|
|
35
|
+
* extract `"project_versions"` from `typeof projectVersions` even though the
|
|
36
|
+
* runtime value lives on a `Symbol.for("drizzle:Name")` key.
|
|
33
37
|
*/
|
|
34
|
-
type
|
|
38
|
+
type TableWithSqlName = {
|
|
39
|
+
_: {
|
|
40
|
+
name: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Extracts the SQL table name literal from a Drizzle table reference, or
|
|
45
|
+
* `never` when the argument is not a Drizzle table with a `_.name` field.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const postVersions = sqliteTable("post_versions", { ... });
|
|
50
|
+
* type N = SqlNameOf<typeof postVersions>; // "post_versions"
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
type SqlNameOf<T> = T extends TableWithSqlName ? T["_"]["name"] extends string ? T["_"]["name"] : never : never;
|
|
54
|
+
/**
|
|
55
|
+
* Extracts the string-literal union of valid subject keys from a
|
|
56
|
+
* {@link SchemaMap}, including **both** the JS schema keys (e.g.
|
|
57
|
+
* `"postVersions"`) and the SQL table names (e.g. `"post_versions"`).
|
|
58
|
+
*
|
|
59
|
+
* The runtime side of `definePermissions<User, Schema>()` builds a
|
|
60
|
+
* matching lookup table keyed by both forms, so the two string forms are
|
|
61
|
+
* fully interchangeable — whichever matches your mental model.
|
|
62
|
+
*
|
|
63
|
+
* Without this, string subjects were accidentally constrained to JS keys
|
|
64
|
+
* only (#177), causing confusion when a schema's JS key differs from its
|
|
65
|
+
* SQL table name (e.g. `documentVersions` vs `document_versions`).
|
|
66
|
+
*/
|
|
67
|
+
type TableName<TTables extends SchemaMap> = Extract<keyof TTables, string> | SqlNameOf<TTables[keyof TTables]>;
|
|
35
68
|
/**
|
|
36
69
|
* The set of acceptable subject inputs for a grant or `can()` check.
|
|
37
70
|
*
|
|
38
71
|
* - A {@link DrizzleTable} object reference (the original form, always allowed).
|
|
39
|
-
* - A string-literal table name from {@link TableName}
|
|
40
|
-
*
|
|
72
|
+
* - A string-literal table name from {@link TableName} — either the JS schema
|
|
73
|
+
* key (`"postVersions"`) or the SQL table name (`"post_versions"`) — both
|
|
74
|
+
* resolve to the same underlying table at runtime.
|
|
41
75
|
* - The literal `"all"` for grants that apply to every table.
|
|
42
76
|
*/
|
|
43
77
|
type SubjectInput<TTables extends SchemaMap = SchemaMap> = DrizzleTable | TableName<TTables> | "all";
|
|
@@ -345,5 +379,28 @@ declare class ForbiddenError extends Error {
|
|
|
345
379
|
role: string | undefined;
|
|
346
380
|
};
|
|
347
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Error thrown at **permission-definition time** when a string-form subject
|
|
384
|
+
* passed to the `grant` callback inside
|
|
385
|
+
* `definePermissions<User, typeof schema>()` cannot be resolved to a real
|
|
386
|
+
* Drizzle table reference from the schema map.
|
|
387
|
+
*
|
|
388
|
+
* This is the runtime sibling of the compile-time constraint on
|
|
389
|
+
* {@link TableName} — it catches cases where the schema generic was skipped,
|
|
390
|
+
* the string was typed wrong (typo, wrong case), or the table was removed
|
|
391
|
+
* from the schema without updating the grant.
|
|
392
|
+
*
|
|
393
|
+
* Unlike {@link ForbiddenError} (thrown per-request on a permission check),
|
|
394
|
+
* `PermissionRegistrationError` is thrown **once at startup** from
|
|
395
|
+
* `definePermissions()` — so a misconfigured grant fails fast and loud
|
|
396
|
+
* instead of silently matching nothing at query time.
|
|
397
|
+
*/
|
|
398
|
+
declare class PermissionRegistrationError extends Error {
|
|
399
|
+
/** The unresolved subject string that was passed to `grant()`. */
|
|
400
|
+
readonly subject: string;
|
|
401
|
+
/** The available table names (both JS keys and SQL names) from the schema. */
|
|
402
|
+
readonly availableTables: readonly string[];
|
|
403
|
+
constructor(subject: string, availableTables: readonly string[]);
|
|
404
|
+
}
|
|
348
405
|
|
|
349
|
-
export { CRUD_ACTIONS as C, type DrizzleTable as D, ForbiddenError as F, type Grant as G, type LookupDb as L, type PermissionsConfig as P, type SchemaMap as S, type TableName as T, type WithLookups as W, type Permissions as a, type PermissionAction as b, type SubjectInput as c, type WhereClause as d, type PermissionDescriptor as e, type PermissionCheckResult as f, type CrudAction as g, type GrantFn as h, type LookupFn as i,
|
|
406
|
+
export { CRUD_ACTIONS as C, type DrizzleTable as D, ForbiddenError as F, type Grant as G, type LookupDb as L, type PermissionsConfig as P, type SchemaMap as S, type TableName as T, type WithLookups as W, type Permissions as a, type PermissionAction as b, type SubjectInput as c, type WhereClause as d, type PermissionDescriptor as e, type PermissionCheckResult as f, type CrudAction as g, type GrantFn as h, type LookupFn as i, PermissionRegistrationError as j, type SqlNameOf as k, getTableName as l };
|
package/dist/client.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { g as CrudAction, F as ForbiddenError, b as PermissionAction, f as PermissionCheckResult, e as PermissionDescriptor } from './client-
|
|
1
|
+
export { g as CrudAction, F as ForbiddenError, b as PermissionAction, f as PermissionCheckResult, e as PermissionDescriptor } from './client-D6goQV8b.js';
|
package/dist/client.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,44 @@
|
|
|
1
|
-
import { P as PermissionsConfig, a as Permissions, S as SchemaMap, b as PermissionAction, c as SubjectInput, W as WithLookups, d as WhereClause, G as Grant, e as PermissionDescriptor, f as PermissionCheckResult } from './client-
|
|
2
|
-
export { C as CRUD_ACTIONS, g as CrudAction, D as DrizzleTable, F as ForbiddenError, h as GrantFn, L as LookupDb, i as LookupFn, T as TableName,
|
|
1
|
+
import { P as PermissionsConfig, a as Permissions, S as SchemaMap, b as PermissionAction, c as SubjectInput, W as WithLookups, d as WhereClause, G as Grant, e as PermissionDescriptor, f as PermissionCheckResult } from './client-D6goQV8b.js';
|
|
2
|
+
export { C as CRUD_ACTIONS, g as CrudAction, D as DrizzleTable, F as ForbiddenError, h as GrantFn, L as LookupDb, i as LookupFn, j as PermissionRegistrationError, k as SqlNameOf, T as TableName, l as getTableName } from './client-D6goQV8b.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Creates a permission configuration that can be shared between server-side
|
|
6
6
|
* enforcement (`@cfast/db`) and client-side introspection (`@cfast/actions`).
|
|
7
7
|
*
|
|
8
|
-
* Supports
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
8
|
+
* Supports four calling styles:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Direct:** `definePermissions(config)` — when no custom user type and
|
|
11
|
+
* no schema map is needed.
|
|
12
|
+
*
|
|
13
|
+
* 2. **Curried (user only):** `definePermissions<MyUser>()(config)` — types
|
|
14
|
+
* the `user` parameter inside `where` clauses. String subjects are still
|
|
15
|
+
* accepted but are **not** compile-time or runtime validated against a
|
|
16
|
+
* schema; they are stored as opaque strings and matched by `getTableName`.
|
|
17
|
+
*
|
|
18
|
+
* 3. **Curried (user + schema types only, no runtime schema):**
|
|
19
|
+
* `definePermissions<MyUser, typeof schema>()(config)` — constrains
|
|
20
|
+
* string subjects to `"jsKey" | "sql_name"` at compile time, but at
|
|
21
|
+
* runtime still stores strings opaquely. Prefer style (4) when you can.
|
|
22
|
+
*
|
|
23
|
+
* 4. **Curried with runtime schema (RECOMMENDED):**
|
|
24
|
+
* `definePermissions<MyUser, typeof schema>({ schema })(config)` — walks
|
|
25
|
+
* the schema at startup and resolves every string-form subject to the
|
|
26
|
+
* exact Drizzle table reference the schema uses. This is the only form
|
|
27
|
+
* that lets string-form grants work with Drizzle relational `with`
|
|
28
|
+
* queries, because Drizzle identifies tables by reference, not name
|
|
29
|
+
* (see issues #146, #175, #177).
|
|
30
|
+
*
|
|
31
|
+
* - Unknown strings throw {@link PermissionRegistrationError} **at
|
|
32
|
+
* startup** (fast, loud failure instead of silently matching nothing).
|
|
33
|
+
* - Both the JS key (`"documentVersions"`) and the SQL table name
|
|
34
|
+
* (`"document_versions"`) resolve to the same table reference.
|
|
35
|
+
* - Object-form subjects still work unchanged.
|
|
36
|
+
*
|
|
37
|
+
* @param configOrOptions - Either the permissions config (style 1) or the
|
|
38
|
+
* resolver options `{ schema }` (style 4). When omitted entirely, returns
|
|
39
|
+
* the curried form (styles 2 and 3).
|
|
40
|
+
* @returns A {@link Permissions} object, or a function that takes a config
|
|
41
|
+
* and returns one.
|
|
18
42
|
*
|
|
19
43
|
* @example
|
|
20
44
|
* ```typescript
|
|
@@ -23,31 +47,30 @@ export { C as CRUD_ACTIONS, g as CrudAction, D as DrizzleTable, F as ForbiddenEr
|
|
|
23
47
|
* import * as schema from "./schema";
|
|
24
48
|
* const { posts, comments } = schema;
|
|
25
49
|
*
|
|
26
|
-
* //
|
|
50
|
+
* // Style 1 — direct, accepts table objects
|
|
27
51
|
* const permissions = definePermissions({
|
|
28
52
|
* roles: ["anonymous", "user", "admin"] as const,
|
|
29
53
|
* grants: {
|
|
30
54
|
* anonymous: [
|
|
31
55
|
* grant("read", posts, { where: (p) => eq(p.published, true) }),
|
|
32
56
|
* ],
|
|
33
|
-
* user: [
|
|
34
|
-
* grant("read", posts),
|
|
35
|
-
* grant("create", posts),
|
|
36
|
-
* ],
|
|
57
|
+
* user: [grant("read", posts), grant("create", posts)],
|
|
37
58
|
* admin: [grant("manage", "all")],
|
|
38
59
|
* },
|
|
39
60
|
* });
|
|
40
61
|
*
|
|
41
|
-
* //
|
|
62
|
+
* // Style 4 — recommended: runtime-resolved string subjects
|
|
42
63
|
* type AuthUser = { id: string };
|
|
43
|
-
* const perms = definePermissions<AuthUser, typeof schema>()({
|
|
64
|
+
* const perms = definePermissions<AuthUser, typeof schema>({ schema })({
|
|
44
65
|
* roles: ["user", "admin"] as const,
|
|
45
66
|
* grants: (grant) => ({
|
|
46
67
|
* user: [
|
|
47
|
-
* grant("read", "posts"),
|
|
48
|
-
* grant("
|
|
68
|
+
* grant("read", "posts"), // JS key — resolves
|
|
69
|
+
* grant("create", "post_versions"), // SQL name — also resolves
|
|
70
|
+
* grant("update", posts, { // object form still works
|
|
49
71
|
* where: (p, u) => eq(p.authorId, u.id),
|
|
50
72
|
* }),
|
|
73
|
+
* // grant("read", "unknownTable"), // throws at startup
|
|
51
74
|
* ],
|
|
52
75
|
* admin: [grant("manage", "all")],
|
|
53
76
|
* }),
|
|
@@ -55,6 +78,9 @@ export { C as CRUD_ACTIONS, g as CrudAction, D as DrizzleTable, F as ForbiddenEr
|
|
|
55
78
|
* ```
|
|
56
79
|
*/
|
|
57
80
|
declare function definePermissions<TRoles extends readonly string[]>(config: PermissionsConfig<TRoles>): Permissions<TRoles>;
|
|
81
|
+
declare function definePermissions<TUser, TTables extends SchemaMap>(options: {
|
|
82
|
+
schema: TTables;
|
|
83
|
+
}): <TRoles extends readonly string[]>(config: PermissionsConfig<TRoles, TUser, TTables>) => Permissions<TRoles>;
|
|
58
84
|
declare function definePermissions<TUser, TTables extends SchemaMap = SchemaMap>(): <TRoles extends readonly string[]>(config: PermissionsConfig<TRoles, TUser, TTables>) => Permissions<TRoles>;
|
|
59
85
|
|
|
60
86
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CRUD_ACTIONS,
|
|
3
3
|
ForbiddenError,
|
|
4
|
+
PermissionRegistrationError,
|
|
4
5
|
getTableName
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ISTOPBSB.js";
|
|
6
7
|
|
|
7
8
|
// src/grant.ts
|
|
8
9
|
function grant(action, subject, options) {
|
|
@@ -15,9 +16,43 @@ function grant(action, subject, options) {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
// src/define-permissions.ts
|
|
18
|
-
function
|
|
19
|
+
function buildSchemaLookup(schema) {
|
|
20
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
21
|
+
const available = /* @__PURE__ */ new Set();
|
|
22
|
+
if (!schema) return { lookup, available: [] };
|
|
23
|
+
for (const [jsKey, table] of Object.entries(schema)) {
|
|
24
|
+
if (!table || typeof table !== "object" && typeof table !== "function") {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
lookup.set(jsKey, table);
|
|
28
|
+
available.add(jsKey);
|
|
29
|
+
const sqlName = getTableName(table);
|
|
30
|
+
if (sqlName && sqlName !== "unknown") {
|
|
31
|
+
lookup.set(sqlName, table);
|
|
32
|
+
available.add(sqlName);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { lookup, available: [...available] };
|
|
36
|
+
}
|
|
37
|
+
function makeSchemaAwareGrant(lookup, available) {
|
|
38
|
+
const schemaGrant = ((action, subject, options) => {
|
|
39
|
+
if (typeof subject !== "string" || subject === "all") {
|
|
40
|
+
return grant(action, subject, options);
|
|
41
|
+
}
|
|
42
|
+
const resolved = lookup.get(subject);
|
|
43
|
+
if (!resolved) {
|
|
44
|
+
throw new PermissionRegistrationError(subject, available);
|
|
45
|
+
}
|
|
46
|
+
return grant(action, resolved, options);
|
|
47
|
+
});
|
|
48
|
+
return schemaGrant;
|
|
49
|
+
}
|
|
50
|
+
function buildPermissions(config, schema) {
|
|
19
51
|
const { roles, hierarchy } = config;
|
|
20
|
-
const grantFn =
|
|
52
|
+
const grantFn = schema ? (() => {
|
|
53
|
+
const { lookup, available } = buildSchemaLookup(schema);
|
|
54
|
+
return makeSchemaAwareGrant(lookup, available);
|
|
55
|
+
})() : grant;
|
|
21
56
|
const grants = typeof config.grants === "function" ? config.grants(grantFn) : config.grants;
|
|
22
57
|
const resolvedGrants = resolveHierarchy(roles, grants, hierarchy);
|
|
23
58
|
return {
|
|
@@ -26,11 +61,20 @@ function buildPermissions(config) {
|
|
|
26
61
|
resolvedGrants
|
|
27
62
|
};
|
|
28
63
|
}
|
|
29
|
-
function definePermissions(
|
|
30
|
-
if (
|
|
64
|
+
function definePermissions(configOrOptions) {
|
|
65
|
+
if (configOrOptions === void 0) {
|
|
31
66
|
return (c) => buildPermissions(c);
|
|
32
67
|
}
|
|
33
|
-
|
|
68
|
+
if (isSchemaOptions(configOrOptions)) {
|
|
69
|
+
const { schema } = configOrOptions;
|
|
70
|
+
return (c) => buildPermissions(c, schema);
|
|
71
|
+
}
|
|
72
|
+
return buildPermissions(configOrOptions);
|
|
73
|
+
}
|
|
74
|
+
function isSchemaOptions(value) {
|
|
75
|
+
if (typeof value !== "object" || value === null) return false;
|
|
76
|
+
const v = value;
|
|
77
|
+
return v.schema !== void 0 && typeof v.schema === "object" && v.schema !== null && v.roles === void 0;
|
|
34
78
|
}
|
|
35
79
|
function resolveHierarchy(roles, grants, hierarchy) {
|
|
36
80
|
if (!hierarchy) {
|
|
@@ -193,6 +237,7 @@ function resolveGrants(permissions, userOrRoles) {
|
|
|
193
237
|
export {
|
|
194
238
|
CRUD_ACTIONS,
|
|
195
239
|
ForbiddenError,
|
|
240
|
+
PermissionRegistrationError,
|
|
196
241
|
can,
|
|
197
242
|
checkPermissions,
|
|
198
243
|
definePermissions,
|
package/llms.txt
CHANGED
|
@@ -32,34 +32,39 @@ const permissions = definePermissions({
|
|
|
32
32
|
});
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
**Curried form**
|
|
36
|
-
```typescript
|
|
37
|
-
const permissions = definePermissions<MyUser>()({
|
|
38
|
-
roles: [...] as const,
|
|
39
|
-
grants: (grant) => ({
|
|
40
|
-
user: [ grant("update", posts, { where: (post, user) => eq(post.authorId, user.id) }) ],
|
|
41
|
-
// ...
|
|
42
|
-
}),
|
|
43
|
-
});
|
|
44
|
-
```
|
|
35
|
+
**Curried form with schema (RECOMMENDED)** — pass the schema as a runtime argument to resolve string subjects to the exact same Drizzle table reference your schema uses. This is the only form that lets string-form grants work with Drizzle relational `with` queries, because `@cfast/db` identifies tables by reference (not name):
|
|
45
36
|
|
|
46
|
-
**Curried form with schema** to constrain string subjects to known table names at compile time:
|
|
47
37
|
```typescript
|
|
48
38
|
import * as schema from "./schema";
|
|
49
39
|
|
|
50
|
-
const permissions = definePermissions<MyUser, typeof schema>()({
|
|
40
|
+
const permissions = definePermissions<MyUser, typeof schema>({ schema })({
|
|
51
41
|
roles: ["member", "admin"] as const,
|
|
52
42
|
grants: (grant) => ({
|
|
53
43
|
member: [
|
|
54
|
-
grant("read", "projects"), // ✓
|
|
44
|
+
grant("read", "projects"), // ✓ JS key
|
|
45
|
+
grant("read", "project_versions"), // ✓ SQL name (snake_case)
|
|
55
46
|
grant("read", schema.projects), // ✓ object form still works
|
|
56
|
-
// grant("read", "unknownTable"), // ✗ TypeScript error
|
|
47
|
+
// grant("read", "unknownTable"), // ✗ TypeScript error + runtime throw
|
|
57
48
|
],
|
|
58
49
|
admin: [grant("manage", "all")],
|
|
59
50
|
}),
|
|
60
51
|
});
|
|
61
52
|
```
|
|
62
53
|
|
|
54
|
+
Both JS schema keys (e.g. `"projectVersions"`) and SQL table names (e.g. `"project_versions"`) resolve to the same table reference. Unknown strings throw `PermissionRegistrationError` at **startup** — fast, loud failure instead of silently matching nothing at query time.
|
|
55
|
+
|
|
56
|
+
**Curried form without runtime schema** — legacy shape that types the `user` parameter but does not resolve strings. String subjects are stored opaquely and matched by name only; cross-table `with` relational queries will silently return empty arrays. Prefer the recommended form above:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const permissions = definePermissions<MyUser>()({
|
|
60
|
+
roles: [...] as const,
|
|
61
|
+
grants: (grant) => ({
|
|
62
|
+
user: [ grant("update", posts, { where: (post, user) => eq(post.authorId, user.id) }) ],
|
|
63
|
+
// ...
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
63
68
|
**Returns** `Permissions<TRoles>`:
|
|
64
69
|
```typescript
|
|
65
70
|
type Permissions<TRoles> = {
|
|
@@ -79,7 +84,7 @@ grant(action: PermissionAction, subject: DrizzleTable | string, options?: {
|
|
|
79
84
|
- `PermissionAction`: `"read" | "create" | "update" | "delete" | "manage"`
|
|
80
85
|
- `WhereClause`: `(columns, user, lookups) => DrizzleSQL | undefined`
|
|
81
86
|
- `WithLookups`: `Record<string, (user, db) => Promise<unknown> | unknown>`
|
|
82
|
-
- `subject` may be a Drizzle table object, a string table name (
|
|
87
|
+
- `subject` may be a Drizzle table object, a string table name (JS key **or** SQL name), or `"all"`. When called via the `grant` callback inside `definePermissions<User, typeof schema>({ schema })`, string subjects are resolved at startup to the exact same Drizzle table reference the schema uses, and unknown strings throw `PermissionRegistrationError`. When called standalone (not via the schema-aware callback), string subjects are stored opaquely and matched by name — prefer the resolved form so cross-table relational `with` queries work correctly.
|
|
83
88
|
|
|
84
89
|
#### Cross-table grants via `with`
|
|
85
90
|
|
|
@@ -93,7 +98,7 @@ const { recipes, friendGrants } = schema;
|
|
|
93
98
|
|
|
94
99
|
type AppUser = { id: string; roles: string[] };
|
|
95
100
|
|
|
96
|
-
export const permissions = definePermissions<AppUser, typeof schema>()({
|
|
101
|
+
export const permissions = definePermissions<AppUser, typeof schema>({ schema })({
|
|
97
102
|
roles: ["user"] as const,
|
|
98
103
|
grants: (g) => ({
|
|
99
104
|
user: [
|
|
@@ -168,6 +173,20 @@ class ForbiddenError extends Error {
|
|
|
168
173
|
}
|
|
169
174
|
```
|
|
170
175
|
|
|
176
|
+
Thrown at request time when a permission check fails (row-level denial).
|
|
177
|
+
|
|
178
|
+
### `PermissionRegistrationError`
|
|
179
|
+
```typescript
|
|
180
|
+
import { PermissionRegistrationError } from "@cfast/permissions";
|
|
181
|
+
|
|
182
|
+
class PermissionRegistrationError extends Error {
|
|
183
|
+
readonly subject: string; // the unresolved string
|
|
184
|
+
readonly availableTables: readonly string[]; // sorted JS keys + SQL names
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Thrown **at startup** from `definePermissions<User, typeof schema>({ schema })(...)` when a string-form grant subject does not match any table in the schema. Fast, loud failure so a typo or a removed table surfaces immediately instead of silently matching nothing at query time.
|
|
189
|
+
|
|
171
190
|
### `can(grants, action, table): boolean`
|
|
172
191
|
```typescript
|
|
173
192
|
import { can } from "@cfast/permissions";
|