@zanzojs/drizzle 0.1.0-beta.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 +21 -0
- package/dist/index.cjs +66 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gonzalo Jeria
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createZanzoAdapter: () => createZanzoAdapter
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
var import_drizzle_orm = require("drizzle-orm");
|
|
27
|
+
var import_core = require("@zanzojs/core");
|
|
28
|
+
function createZanzoAdapter(engine, tupleTable, options) {
|
|
29
|
+
const isDev = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
|
|
30
|
+
const shouldWarn = options?.warnOnNestedConditions ?? isDev;
|
|
31
|
+
return function withPermissions(actor, action, resourceType, resourceIdColumn) {
|
|
32
|
+
const ast = engine.buildDatabaseQuery(actor, action, resourceType);
|
|
33
|
+
if (ast && ast.conditions.length > 100) {
|
|
34
|
+
throw new Error(`[Zanzo] Security Exception: The resulting AST exceeds the maximum safe limit of 100 conditional branches. Please optimize your schema or rely on pre-computed tuples to avoid database exhaustion.`);
|
|
35
|
+
}
|
|
36
|
+
if (!ast) {
|
|
37
|
+
return import_drizzle_orm.sql`1 = 0`;
|
|
38
|
+
}
|
|
39
|
+
const parseCondition = (cond) => {
|
|
40
|
+
const objectString = import_drizzle_orm.sql`${resourceType} || '${import_drizzle_orm.sql.raw(import_core.ENTITY_REF_SEPARATOR)}' || ${resourceIdColumn}`;
|
|
41
|
+
if (cond.type === "nested" && shouldWarn) {
|
|
42
|
+
console.warn(`[Zanzo] Nested permission path detected: '${[cond.relation, ...cond.nextRelationPath].join(import_core.RELATION_PATH_SEPARATOR)}'. The SQL adapter resolves this via pre-materialized tuples. Ensure you used expandTuples() when writing this relationship to the database. See: https://zanzo.dev/docs/tuple-expansion`);
|
|
43
|
+
}
|
|
44
|
+
const relationString = cond.type === "nested" ? [cond.relation, ...cond.nextRelationPath].join(import_core.RELATION_PATH_SEPARATOR) : cond.relation;
|
|
45
|
+
return import_drizzle_orm.sql`EXISTS (
|
|
46
|
+
SELECT 1 FROM ${tupleTable}
|
|
47
|
+
WHERE ${tupleTable.object} = ${objectString}
|
|
48
|
+
AND ${tupleTable.relation} = ${relationString}
|
|
49
|
+
AND ${tupleTable.subject} = ${cond.targetSubject}
|
|
50
|
+
)`;
|
|
51
|
+
};
|
|
52
|
+
const parsedConditions = ast.conditions.map(parseCondition).filter((c) => c !== void 0);
|
|
53
|
+
if (parsedConditions.length === 0) {
|
|
54
|
+
return import_drizzle_orm.sql`1 = 0`;
|
|
55
|
+
}
|
|
56
|
+
if (ast.operator === "AND") {
|
|
57
|
+
return (0, import_drizzle_orm.and)(...parsedConditions);
|
|
58
|
+
}
|
|
59
|
+
return (0, import_drizzle_orm.or)(...parsedConditions);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
63
|
+
0 && (module.exports = {
|
|
64
|
+
createZanzoAdapter
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { or, and, SQL, sql, AnyColumn } from 'drizzle-orm';\nimport type { QueryAST, Condition, ZanzoEngine, SchemaData, ExtractSchemaResources, ExtractSchemaActions } from '@zanzojs/core';\nimport { ENTITY_REF_SEPARATOR, RELATION_PATH_SEPARATOR } from '@zanzojs/core';\n\n/**\n * Ensures the passed Drizzle Table conforms to the mandatory Zanzibar Universal Tuple Structure.\n */\nexport interface ZanzoTupleTable {\n object: AnyColumn; // String e.g. \"Invoice:123\"\n relation: AnyColumn; // String e.g. \"owner\"\n subject: AnyColumn; // String e.g. \"User:1\"\n [key: string]: any; // Allow extensions like IDs or context\n}\n\nexport interface ZanzoAdapterOptions {\n /**\n * Emits a console.warn when a nested permission path (e.g. 'org.admin') is detected,\n * reminding you to use expandTuples() when writing this relationship.\n *\n * @default true in NODE_ENV=development, false in production\n */\n warnOnNestedConditions?: boolean;\n}\n\n/**\n * Creates a \"Zero-Config\" Drizzle ORM permission adapter tailored for the Zanzibar Pattern.\n * Rather than mapping individual specific columns, this queries a Universal Tuple Table resolving access instantly.\n *\n * @param engine The initialized ZanzoEngine instance\n * @param tupleTable The central Drizzle Table where all Relation Tuples are stored\n * @param options Optional configuration for the adapter\n * @returns A bounded `withPermissions` closure\n */\nexport function createZanzoAdapter<TSchema extends SchemaData, TTable extends ZanzoTupleTable>(\n engine: ZanzoEngine<TSchema>,\n tupleTable: TTable,\n options?: ZanzoAdapterOptions\n) {\n // Smart default: auto-enable warnings in development unless explicitly configured\n const isDev = typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';\n const shouldWarn = options?.warnOnNestedConditions ?? isDev;\n\n /**\n * Generates a Drizzle SQL AST (subquery strategy) resolving access against the Universal Tuple Table.\n *\n * @param actor The Subject identifier validating access (e.g \"User:1\")\n * @param action The protected action (e.g \"read\")\n * @param resourceType The target Domain scope (e.g \"Invoice\")\n * @param resourceIdColumn The specific Drizzle column representing the object's ID in the business table (e.g `invoices.id`)\n */\n return function withPermissions<\n TResourceName extends Extract<ExtractSchemaResources<TSchema>, string>,\n TAction extends ExtractSchemaActions<TSchema, TResourceName>,\n >(\n actor: string,\n action: TAction,\n resourceType: TResourceName,\n resourceIdColumn: AnyColumn\n ): SQL<unknown> {\n \n // Evaluate the underlying pure logical AST\n const ast = engine.buildDatabaseQuery(actor, action as any, resourceType as any);\n\n // Protection Against 'The List Problem' / Query Payload Exhaustion:\n // If a badly designed ReBAC schema generates a monstrous combinatorial AST tree, \n // it could exceed max SQL text limits resulting in DB crashes. Abort safely.\n if (ast && ast.conditions.length > 100) {\n throw new Error(`[Zanzo] Security Exception: The resulting AST exceeds the maximum safe limit of 100 conditional branches. Please optimize your schema or rely on pre-computed tuples to avoid database exhaustion.`);\n }\n\n if (!ast) {\n // Access totally denied\n return sql`1 = 0`; \n }\n\n const parseCondition = (cond: Condition): SQL<unknown> | undefined => {\n \n // In the Zanzibar Pattern, ALL conditions (direct or nested) ultimately result\n // in looking up pre-computed or dynamically queried tuples.\n // E.g for a direct target: SELECT 1 FROM tuples WHERE object = TYPE:ID AND relation = X AND subject = TARGET\n \n const objectString = sql`${resourceType} || '${sql.raw(ENTITY_REF_SEPARATOR)}' || ${resourceIdColumn}`;\n\n // In Zanzibar, nested conditions (e.g. org.admin) are evaluated using the \"Tuple Expansion\" pattern.\n // This means the user has asynchronously written materialized tuples into the database.\n // Therefore, both direct and nested queries are resolved identically via O(1) EXISTS lookups.\n if (cond.type === 'nested' && shouldWarn) {\n console.warn(`[Zanzo] Nested permission path detected: '${[cond.relation, ...cond.nextRelationPath].join(RELATION_PATH_SEPARATOR)}'. The SQL adapter resolves this via pre-materialized tuples. Ensure you used expandTuples() when writing this relationship to the database. See: https://zanzo.dev/docs/tuple-expansion`);\n }\n\n const relationString = cond.type === 'nested' \n ? [cond.relation, ...cond.nextRelationPath].join(RELATION_PATH_SEPARATOR) \n : cond.relation;\n\n return sql`EXISTS (\n SELECT 1 FROM ${tupleTable} \n WHERE ${tupleTable.object} = ${objectString} \n AND ${tupleTable.relation} = ${relationString} \n AND ${tupleTable.subject} = ${cond.targetSubject}\n )`;\n };\n\n const parsedConditions = ast.conditions\n .map(parseCondition)\n .filter((c): c is SQL<unknown> => c !== undefined);\n\n if (parsedConditions.length === 0) {\n return sql`1 = 0`;\n }\n\n if (ast.operator === 'AND') {\n return and(...parsedConditions) as SQL<unknown>;\n }\n\n return or(...parsedConditions) as SQL<unknown>;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA6C;AAE7C,kBAA8D;AA+BvD,SAAS,mBACd,QACA,YACA,SACA;AAEA,QAAM,QAAQ,OAAO,YAAY,eAAe,QAAQ,KAAK,aAAa;AAC1E,QAAM,aAAa,SAAS,0BAA0B;AAUtD,SAAO,SAAS,gBAId,OACA,QACA,cACA,kBACc;AAGd,UAAM,MAAM,OAAO,mBAAmB,OAAO,QAAe,YAAmB;AAK/E,QAAI,OAAO,IAAI,WAAW,SAAS,KAAK;AACtC,YAAM,IAAI,MAAM,oMAAoM;AAAA,IACtN;AAEA,QAAI,CAAC,KAAK;AAER,aAAO;AAAA,IACT;AAEA,UAAM,iBAAiB,CAAC,SAA8C;AAMpE,YAAM,eAAe,yBAAM,YAAY,QAAQ,uBAAI,IAAI,gCAAoB,CAAC,QAAQ,gBAAgB;AAKpG,UAAI,KAAK,SAAS,YAAY,YAAY;AACxC,gBAAQ,KAAK,6CAA6C,CAAC,KAAK,UAAU,GAAG,KAAK,gBAAgB,EAAE,KAAK,mCAAuB,CAAC,0LAA0L;AAAA,MAC7T;AAEA,YAAM,iBAAiB,KAAK,SAAS,WACjC,CAAC,KAAK,UAAU,GAAG,KAAK,gBAAgB,EAAE,KAAK,mCAAuB,IACtE,KAAK;AAET,aAAO;AAAA,wBACW,UAAU;AAAA,gBAClB,WAAW,MAAM,MAAM,YAAY;AAAA,gBACnC,WAAW,QAAQ,MAAM,cAAc;AAAA,gBACvC,WAAW,OAAO,MAAM,KAAK,aAAa;AAAA;AAAA,IAEtD;AAEA,UAAM,mBAAmB,IAAI,WAC1B,IAAI,cAAc,EAClB,OAAO,CAAC,MAAyB,MAAM,MAAS;AAEnD,QAAI,iBAAiB,WAAW,GAAG;AAChC,aAAO;AAAA,IACV;AAEA,QAAI,IAAI,aAAa,OAAO;AAC1B,iBAAO,wBAAI,GAAG,gBAAgB;AAAA,IAChC;AAEA,eAAO,uBAAG,GAAG,gBAAgB;AAAA,EAC/B;AACF;","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AnyColumn, SQL } from 'drizzle-orm';
|
|
2
|
+
import { SchemaData, ZanzoEngine, ExtractSchemaResources, ExtractSchemaActions } from '@zanzojs/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ensures the passed Drizzle Table conforms to the mandatory Zanzibar Universal Tuple Structure.
|
|
6
|
+
*/
|
|
7
|
+
interface ZanzoTupleTable {
|
|
8
|
+
object: AnyColumn;
|
|
9
|
+
relation: AnyColumn;
|
|
10
|
+
subject: AnyColumn;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
interface ZanzoAdapterOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Emits a console.warn when a nested permission path (e.g. 'org.admin') is detected,
|
|
16
|
+
* reminding you to use expandTuples() when writing this relationship.
|
|
17
|
+
*
|
|
18
|
+
* @default true in NODE_ENV=development, false in production
|
|
19
|
+
*/
|
|
20
|
+
warnOnNestedConditions?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a "Zero-Config" Drizzle ORM permission adapter tailored for the Zanzibar Pattern.
|
|
24
|
+
* Rather than mapping individual specific columns, this queries a Universal Tuple Table resolving access instantly.
|
|
25
|
+
*
|
|
26
|
+
* @param engine The initialized ZanzoEngine instance
|
|
27
|
+
* @param tupleTable The central Drizzle Table where all Relation Tuples are stored
|
|
28
|
+
* @param options Optional configuration for the adapter
|
|
29
|
+
* @returns A bounded `withPermissions` closure
|
|
30
|
+
*/
|
|
31
|
+
declare function createZanzoAdapter<TSchema extends SchemaData, TTable extends ZanzoTupleTable>(engine: ZanzoEngine<TSchema>, tupleTable: TTable, options?: ZanzoAdapterOptions): <TResourceName extends Extract<ExtractSchemaResources<TSchema>, string>, TAction extends ExtractSchemaActions<TSchema, TResourceName>>(actor: string, action: TAction, resourceType: TResourceName, resourceIdColumn: AnyColumn) => SQL<unknown>;
|
|
32
|
+
|
|
33
|
+
export { type ZanzoAdapterOptions, type ZanzoTupleTable, createZanzoAdapter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AnyColumn, SQL } from 'drizzle-orm';
|
|
2
|
+
import { SchemaData, ZanzoEngine, ExtractSchemaResources, ExtractSchemaActions } from '@zanzojs/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ensures the passed Drizzle Table conforms to the mandatory Zanzibar Universal Tuple Structure.
|
|
6
|
+
*/
|
|
7
|
+
interface ZanzoTupleTable {
|
|
8
|
+
object: AnyColumn;
|
|
9
|
+
relation: AnyColumn;
|
|
10
|
+
subject: AnyColumn;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
interface ZanzoAdapterOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Emits a console.warn when a nested permission path (e.g. 'org.admin') is detected,
|
|
16
|
+
* reminding you to use expandTuples() when writing this relationship.
|
|
17
|
+
*
|
|
18
|
+
* @default true in NODE_ENV=development, false in production
|
|
19
|
+
*/
|
|
20
|
+
warnOnNestedConditions?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a "Zero-Config" Drizzle ORM permission adapter tailored for the Zanzibar Pattern.
|
|
24
|
+
* Rather than mapping individual specific columns, this queries a Universal Tuple Table resolving access instantly.
|
|
25
|
+
*
|
|
26
|
+
* @param engine The initialized ZanzoEngine instance
|
|
27
|
+
* @param tupleTable The central Drizzle Table where all Relation Tuples are stored
|
|
28
|
+
* @param options Optional configuration for the adapter
|
|
29
|
+
* @returns A bounded `withPermissions` closure
|
|
30
|
+
*/
|
|
31
|
+
declare function createZanzoAdapter<TSchema extends SchemaData, TTable extends ZanzoTupleTable>(engine: ZanzoEngine<TSchema>, tupleTable: TTable, options?: ZanzoAdapterOptions): <TResourceName extends Extract<ExtractSchemaResources<TSchema>, string>, TAction extends ExtractSchemaActions<TSchema, TResourceName>>(actor: string, action: TAction, resourceType: TResourceName, resourceIdColumn: AnyColumn) => SQL<unknown>;
|
|
32
|
+
|
|
33
|
+
export { type ZanzoAdapterOptions, type ZanzoTupleTable, createZanzoAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { or, and, sql } from "drizzle-orm";
|
|
3
|
+
import { ENTITY_REF_SEPARATOR, RELATION_PATH_SEPARATOR } from "@zanzojs/core";
|
|
4
|
+
function createZanzoAdapter(engine, tupleTable, options) {
|
|
5
|
+
const isDev = typeof process !== "undefined" && process.env?.NODE_ENV === "development";
|
|
6
|
+
const shouldWarn = options?.warnOnNestedConditions ?? isDev;
|
|
7
|
+
return function withPermissions(actor, action, resourceType, resourceIdColumn) {
|
|
8
|
+
const ast = engine.buildDatabaseQuery(actor, action, resourceType);
|
|
9
|
+
if (ast && ast.conditions.length > 100) {
|
|
10
|
+
throw new Error(`[Zanzo] Security Exception: The resulting AST exceeds the maximum safe limit of 100 conditional branches. Please optimize your schema or rely on pre-computed tuples to avoid database exhaustion.`);
|
|
11
|
+
}
|
|
12
|
+
if (!ast) {
|
|
13
|
+
return sql`1 = 0`;
|
|
14
|
+
}
|
|
15
|
+
const parseCondition = (cond) => {
|
|
16
|
+
const objectString = sql`${resourceType} || '${sql.raw(ENTITY_REF_SEPARATOR)}' || ${resourceIdColumn}`;
|
|
17
|
+
if (cond.type === "nested" && shouldWarn) {
|
|
18
|
+
console.warn(`[Zanzo] Nested permission path detected: '${[cond.relation, ...cond.nextRelationPath].join(RELATION_PATH_SEPARATOR)}'. The SQL adapter resolves this via pre-materialized tuples. Ensure you used expandTuples() when writing this relationship to the database. See: https://zanzo.dev/docs/tuple-expansion`);
|
|
19
|
+
}
|
|
20
|
+
const relationString = cond.type === "nested" ? [cond.relation, ...cond.nextRelationPath].join(RELATION_PATH_SEPARATOR) : cond.relation;
|
|
21
|
+
return sql`EXISTS (
|
|
22
|
+
SELECT 1 FROM ${tupleTable}
|
|
23
|
+
WHERE ${tupleTable.object} = ${objectString}
|
|
24
|
+
AND ${tupleTable.relation} = ${relationString}
|
|
25
|
+
AND ${tupleTable.subject} = ${cond.targetSubject}
|
|
26
|
+
)`;
|
|
27
|
+
};
|
|
28
|
+
const parsedConditions = ast.conditions.map(parseCondition).filter((c) => c !== void 0);
|
|
29
|
+
if (parsedConditions.length === 0) {
|
|
30
|
+
return sql`1 = 0`;
|
|
31
|
+
}
|
|
32
|
+
if (ast.operator === "AND") {
|
|
33
|
+
return and(...parsedConditions);
|
|
34
|
+
}
|
|
35
|
+
return or(...parsedConditions);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
createZanzoAdapter
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { or, and, SQL, sql, AnyColumn } from 'drizzle-orm';\nimport type { QueryAST, Condition, ZanzoEngine, SchemaData, ExtractSchemaResources, ExtractSchemaActions } from '@zanzojs/core';\nimport { ENTITY_REF_SEPARATOR, RELATION_PATH_SEPARATOR } from '@zanzojs/core';\n\n/**\n * Ensures the passed Drizzle Table conforms to the mandatory Zanzibar Universal Tuple Structure.\n */\nexport interface ZanzoTupleTable {\n object: AnyColumn; // String e.g. \"Invoice:123\"\n relation: AnyColumn; // String e.g. \"owner\"\n subject: AnyColumn; // String e.g. \"User:1\"\n [key: string]: any; // Allow extensions like IDs or context\n}\n\nexport interface ZanzoAdapterOptions {\n /**\n * Emits a console.warn when a nested permission path (e.g. 'org.admin') is detected,\n * reminding you to use expandTuples() when writing this relationship.\n *\n * @default true in NODE_ENV=development, false in production\n */\n warnOnNestedConditions?: boolean;\n}\n\n/**\n * Creates a \"Zero-Config\" Drizzle ORM permission adapter tailored for the Zanzibar Pattern.\n * Rather than mapping individual specific columns, this queries a Universal Tuple Table resolving access instantly.\n *\n * @param engine The initialized ZanzoEngine instance\n * @param tupleTable The central Drizzle Table where all Relation Tuples are stored\n * @param options Optional configuration for the adapter\n * @returns A bounded `withPermissions` closure\n */\nexport function createZanzoAdapter<TSchema extends SchemaData, TTable extends ZanzoTupleTable>(\n engine: ZanzoEngine<TSchema>,\n tupleTable: TTable,\n options?: ZanzoAdapterOptions\n) {\n // Smart default: auto-enable warnings in development unless explicitly configured\n const isDev = typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';\n const shouldWarn = options?.warnOnNestedConditions ?? isDev;\n\n /**\n * Generates a Drizzle SQL AST (subquery strategy) resolving access against the Universal Tuple Table.\n *\n * @param actor The Subject identifier validating access (e.g \"User:1\")\n * @param action The protected action (e.g \"read\")\n * @param resourceType The target Domain scope (e.g \"Invoice\")\n * @param resourceIdColumn The specific Drizzle column representing the object's ID in the business table (e.g `invoices.id`)\n */\n return function withPermissions<\n TResourceName extends Extract<ExtractSchemaResources<TSchema>, string>,\n TAction extends ExtractSchemaActions<TSchema, TResourceName>,\n >(\n actor: string,\n action: TAction,\n resourceType: TResourceName,\n resourceIdColumn: AnyColumn\n ): SQL<unknown> {\n \n // Evaluate the underlying pure logical AST\n const ast = engine.buildDatabaseQuery(actor, action as any, resourceType as any);\n\n // Protection Against 'The List Problem' / Query Payload Exhaustion:\n // If a badly designed ReBAC schema generates a monstrous combinatorial AST tree, \n // it could exceed max SQL text limits resulting in DB crashes. Abort safely.\n if (ast && ast.conditions.length > 100) {\n throw new Error(`[Zanzo] Security Exception: The resulting AST exceeds the maximum safe limit of 100 conditional branches. Please optimize your schema or rely on pre-computed tuples to avoid database exhaustion.`);\n }\n\n if (!ast) {\n // Access totally denied\n return sql`1 = 0`; \n }\n\n const parseCondition = (cond: Condition): SQL<unknown> | undefined => {\n \n // In the Zanzibar Pattern, ALL conditions (direct or nested) ultimately result\n // in looking up pre-computed or dynamically queried tuples.\n // E.g for a direct target: SELECT 1 FROM tuples WHERE object = TYPE:ID AND relation = X AND subject = TARGET\n \n const objectString = sql`${resourceType} || '${sql.raw(ENTITY_REF_SEPARATOR)}' || ${resourceIdColumn}`;\n\n // In Zanzibar, nested conditions (e.g. org.admin) are evaluated using the \"Tuple Expansion\" pattern.\n // This means the user has asynchronously written materialized tuples into the database.\n // Therefore, both direct and nested queries are resolved identically via O(1) EXISTS lookups.\n if (cond.type === 'nested' && shouldWarn) {\n console.warn(`[Zanzo] Nested permission path detected: '${[cond.relation, ...cond.nextRelationPath].join(RELATION_PATH_SEPARATOR)}'. The SQL adapter resolves this via pre-materialized tuples. Ensure you used expandTuples() when writing this relationship to the database. See: https://zanzo.dev/docs/tuple-expansion`);\n }\n\n const relationString = cond.type === 'nested' \n ? [cond.relation, ...cond.nextRelationPath].join(RELATION_PATH_SEPARATOR) \n : cond.relation;\n\n return sql`EXISTS (\n SELECT 1 FROM ${tupleTable} \n WHERE ${tupleTable.object} = ${objectString} \n AND ${tupleTable.relation} = ${relationString} \n AND ${tupleTable.subject} = ${cond.targetSubject}\n )`;\n };\n\n const parsedConditions = ast.conditions\n .map(parseCondition)\n .filter((c): c is SQL<unknown> => c !== undefined);\n\n if (parsedConditions.length === 0) {\n return sql`1 = 0`;\n }\n\n if (ast.operator === 'AND') {\n return and(...parsedConditions) as SQL<unknown>;\n }\n\n return or(...parsedConditions) as SQL<unknown>;\n };\n}\n"],"mappings":";AAAA,SAAS,IAAI,KAAU,WAAsB;AAE7C,SAAS,sBAAsB,+BAA+B;AA+BvD,SAAS,mBACd,QACA,YACA,SACA;AAEA,QAAM,QAAQ,OAAO,YAAY,eAAe,QAAQ,KAAK,aAAa;AAC1E,QAAM,aAAa,SAAS,0BAA0B;AAUtD,SAAO,SAAS,gBAId,OACA,QACA,cACA,kBACc;AAGd,UAAM,MAAM,OAAO,mBAAmB,OAAO,QAAe,YAAmB;AAK/E,QAAI,OAAO,IAAI,WAAW,SAAS,KAAK;AACtC,YAAM,IAAI,MAAM,oMAAoM;AAAA,IACtN;AAEA,QAAI,CAAC,KAAK;AAER,aAAO;AAAA,IACT;AAEA,UAAM,iBAAiB,CAAC,SAA8C;AAMpE,YAAM,eAAe,MAAM,YAAY,QAAQ,IAAI,IAAI,oBAAoB,CAAC,QAAQ,gBAAgB;AAKpG,UAAI,KAAK,SAAS,YAAY,YAAY;AACxC,gBAAQ,KAAK,6CAA6C,CAAC,KAAK,UAAU,GAAG,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,CAAC,0LAA0L;AAAA,MAC7T;AAEA,YAAM,iBAAiB,KAAK,SAAS,WACjC,CAAC,KAAK,UAAU,GAAG,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,IACtE,KAAK;AAET,aAAO;AAAA,wBACW,UAAU;AAAA,gBAClB,WAAW,MAAM,MAAM,YAAY;AAAA,gBACnC,WAAW,QAAQ,MAAM,cAAc;AAAA,gBACvC,WAAW,OAAO,MAAM,KAAK,aAAa;AAAA;AAAA,IAEtD;AAEA,UAAM,mBAAmB,IAAI,WAC1B,IAAI,cAAc,EAClB,OAAO,CAAC,MAAyB,MAAM,MAAS;AAEnD,QAAI,iBAAiB,WAAW,GAAG;AAChC,aAAO;AAAA,IACV;AAEA,QAAI,IAAI,aAAa,OAAO;AAC1B,aAAO,IAAI,GAAG,gBAAgB;AAAA,IAChC;AAEA,WAAO,GAAG,GAAG,gBAAgB;AAAA,EAC/B;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zanzojs/drizzle",
|
|
3
|
+
"version": "0.1.0-beta.0",
|
|
4
|
+
"description": "Drizzle ORM adapter for Zanzo ReBAC. Zero-config Zanzibar Tuple Pattern with parameterized SQL.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"@zanzojs/core",
|
|
7
|
+
"drizzle",
|
|
8
|
+
"drizzle-orm",
|
|
9
|
+
"rebac",
|
|
10
|
+
"authorization",
|
|
11
|
+
"zanzibar"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/GonzaloJeria/zanzo",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/GonzaloJeria/zanzo.git",
|
|
17
|
+
"directory": "packages/drizzle"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Gonzalo Jeria",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"require": "./dist/index.cjs",
|
|
26
|
+
"types": "./dist/index.d.ts"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.cjs",
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@zanzojs/core": "^0.1.0-beta.0",
|
|
42
|
+
"drizzle-orm": ">=0.29.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"drizzle-orm": "^0.30.0",
|
|
46
|
+
"typescript": "^5.7.2",
|
|
47
|
+
"tsup": "latest",
|
|
48
|
+
"vitest": "latest",
|
|
49
|
+
"@zanzojs/core": "0.1.0-beta.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:types": "vitest run --typecheck"
|
|
55
|
+
}
|
|
56
|
+
}
|