drizzle-multitenant 1.0.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/.claude/settings.local.json +14 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/bin/drizzle-multitenant.js +2 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +866 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-DBerWr50.d.ts +76 -0
- package/dist/cross-schema/index.d.ts +262 -0
- package/dist/cross-schema/index.js +219 -0
- package/dist/cross-schema/index.js.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +935 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/express.d.ts +93 -0
- package/dist/integrations/express.js +110 -0
- package/dist/integrations/express.js.map +1 -0
- package/dist/integrations/fastify.d.ts +92 -0
- package/dist/integrations/fastify.js +236 -0
- package/dist/integrations/fastify.js.map +1 -0
- package/dist/integrations/hono.d.ts +2 -0
- package/dist/integrations/hono.js +3 -0
- package/dist/integrations/hono.js.map +1 -0
- package/dist/integrations/nestjs/index.d.ts +386 -0
- package/dist/integrations/nestjs/index.js +9828 -0
- package/dist/integrations/nestjs/index.js.map +1 -0
- package/dist/migrator/index.d.ts +208 -0
- package/dist/migrator/index.js +390 -0
- package/dist/migrator/index.js.map +1 -0
- package/dist/types-DKVaTaIb.d.ts +130 -0
- package/package.json +102 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { sql, getTableName } from 'drizzle-orm';
|
|
2
|
+
|
|
3
|
+
// src/cross-schema/cross-schema.ts
|
|
4
|
+
var CrossSchemaQueryBuilder = class {
|
|
5
|
+
constructor(context) {
|
|
6
|
+
this.context = context;
|
|
7
|
+
}
|
|
8
|
+
fromTable = null;
|
|
9
|
+
joins = [];
|
|
10
|
+
selectFields = {};
|
|
11
|
+
whereCondition = null;
|
|
12
|
+
orderByFields = [];
|
|
13
|
+
limitValue = null;
|
|
14
|
+
offsetValue = null;
|
|
15
|
+
/**
|
|
16
|
+
* Set the main table to query from
|
|
17
|
+
*/
|
|
18
|
+
from(source, table) {
|
|
19
|
+
const schemaName = this.getSchemaName(source);
|
|
20
|
+
this.fromTable = { table, source, schemaName };
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Add an inner join
|
|
25
|
+
*/
|
|
26
|
+
innerJoin(source, table, condition) {
|
|
27
|
+
return this.addJoin(source, table, condition, "inner");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Add a left join
|
|
31
|
+
*/
|
|
32
|
+
leftJoin(source, table, condition) {
|
|
33
|
+
return this.addJoin(source, table, condition, "left");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Add a right join
|
|
37
|
+
*/
|
|
38
|
+
rightJoin(source, table, condition) {
|
|
39
|
+
return this.addJoin(source, table, condition, "right");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Add a full outer join
|
|
43
|
+
*/
|
|
44
|
+
fullJoin(source, table, condition) {
|
|
45
|
+
return this.addJoin(source, table, condition, "full");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Select specific fields
|
|
49
|
+
*/
|
|
50
|
+
select(fields) {
|
|
51
|
+
this.selectFields = fields;
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Add a where condition
|
|
56
|
+
*/
|
|
57
|
+
where(condition) {
|
|
58
|
+
this.whereCondition = condition;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Add order by
|
|
63
|
+
*/
|
|
64
|
+
orderBy(...fields) {
|
|
65
|
+
this.orderByFields = fields;
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Set limit
|
|
70
|
+
*/
|
|
71
|
+
limit(value) {
|
|
72
|
+
this.limitValue = value;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Set offset
|
|
77
|
+
*/
|
|
78
|
+
offset(value) {
|
|
79
|
+
this.offsetValue = value;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Execute the query and return typed results
|
|
84
|
+
*/
|
|
85
|
+
async execute() {
|
|
86
|
+
if (!this.fromTable) {
|
|
87
|
+
throw new Error("[drizzle-multitenant] No table specified. Use .from() first.");
|
|
88
|
+
}
|
|
89
|
+
const sqlQuery = this.buildSql();
|
|
90
|
+
const result = await this.context.tenantDb.execute(sqlQuery);
|
|
91
|
+
return result.rows;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Build the SQL query
|
|
95
|
+
*/
|
|
96
|
+
buildSql() {
|
|
97
|
+
if (!this.fromTable) {
|
|
98
|
+
throw new Error("[drizzle-multitenant] No table specified");
|
|
99
|
+
}
|
|
100
|
+
const parts = [];
|
|
101
|
+
const selectParts = Object.entries(this.selectFields).map(([alias, column]) => {
|
|
102
|
+
const columnName = column.name;
|
|
103
|
+
return sql`${sql.raw(`"${columnName}"`)} as ${sql.raw(`"${alias}"`)}`;
|
|
104
|
+
});
|
|
105
|
+
if (selectParts.length === 0) {
|
|
106
|
+
parts.push(sql`SELECT *`);
|
|
107
|
+
} else {
|
|
108
|
+
parts.push(sql`SELECT ${sql.join(selectParts, sql`, `)}`);
|
|
109
|
+
}
|
|
110
|
+
const fromTableRef = this.getFullTableName(this.fromTable.schemaName, this.fromTable.table);
|
|
111
|
+
parts.push(sql` FROM ${sql.raw(fromTableRef)}`);
|
|
112
|
+
for (const join of this.joins) {
|
|
113
|
+
const joinTableRef = this.getFullTableName(join.schemaName, join.table);
|
|
114
|
+
const joinType = this.getJoinKeyword(join.type);
|
|
115
|
+
parts.push(sql` ${sql.raw(joinType)} ${sql.raw(joinTableRef)} ON ${join.condition}`);
|
|
116
|
+
}
|
|
117
|
+
if (this.whereCondition) {
|
|
118
|
+
parts.push(sql` WHERE ${this.whereCondition}`);
|
|
119
|
+
}
|
|
120
|
+
if (this.orderByFields.length > 0) {
|
|
121
|
+
parts.push(sql` ORDER BY ${sql.join(this.orderByFields, sql`, `)}`);
|
|
122
|
+
}
|
|
123
|
+
if (this.limitValue !== null) {
|
|
124
|
+
parts.push(sql` LIMIT ${sql.raw(this.limitValue.toString())}`);
|
|
125
|
+
}
|
|
126
|
+
if (this.offsetValue !== null) {
|
|
127
|
+
parts.push(sql` OFFSET ${sql.raw(this.offsetValue.toString())}`);
|
|
128
|
+
}
|
|
129
|
+
return sql.join(parts, sql``);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Add a join to the query
|
|
133
|
+
*/
|
|
134
|
+
addJoin(source, table, condition, type) {
|
|
135
|
+
const schemaName = this.getSchemaName(source);
|
|
136
|
+
this.joins.push({ table, source, schemaName, condition, type });
|
|
137
|
+
return this;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get schema name for a source
|
|
141
|
+
*/
|
|
142
|
+
getSchemaName(source) {
|
|
143
|
+
if (source === "tenant") {
|
|
144
|
+
return this.context.tenantSchema ?? "tenant";
|
|
145
|
+
}
|
|
146
|
+
return this.context.sharedSchema ?? "public";
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get fully qualified table name
|
|
150
|
+
*/
|
|
151
|
+
getFullTableName(schemaName, table) {
|
|
152
|
+
const tableName = getTableName(table);
|
|
153
|
+
return `"${schemaName}"."${tableName}"`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get SQL keyword for join type
|
|
157
|
+
*/
|
|
158
|
+
getJoinKeyword(type) {
|
|
159
|
+
switch (type) {
|
|
160
|
+
case "inner":
|
|
161
|
+
return "INNER JOIN";
|
|
162
|
+
case "left":
|
|
163
|
+
return "LEFT JOIN";
|
|
164
|
+
case "right":
|
|
165
|
+
return "RIGHT JOIN";
|
|
166
|
+
case "full":
|
|
167
|
+
return "FULL OUTER JOIN";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
function createCrossSchemaQuery(context) {
|
|
172
|
+
return new CrossSchemaQueryBuilder(context);
|
|
173
|
+
}
|
|
174
|
+
async function withSharedLookup(config) {
|
|
175
|
+
const {
|
|
176
|
+
tenantDb,
|
|
177
|
+
tenantTable,
|
|
178
|
+
sharedTable,
|
|
179
|
+
foreignKey,
|
|
180
|
+
sharedKey = "id",
|
|
181
|
+
sharedFields,
|
|
182
|
+
where: whereCondition
|
|
183
|
+
} = config;
|
|
184
|
+
const tenantTableName = getTableName(tenantTable);
|
|
185
|
+
const sharedTableName = getTableName(sharedTable);
|
|
186
|
+
const sharedFieldList = sharedFields.map((field) => `s."${String(field)}"`).join(", ");
|
|
187
|
+
const queryParts = [
|
|
188
|
+
`SELECT t.*, ${sharedFieldList}`,
|
|
189
|
+
`FROM "${tenantTableName}" t`,
|
|
190
|
+
`LEFT JOIN "public"."${sharedTableName}" s ON t."${String(foreignKey)}" = s."${String(sharedKey)}"`
|
|
191
|
+
];
|
|
192
|
+
if (whereCondition) {
|
|
193
|
+
queryParts.push("WHERE");
|
|
194
|
+
}
|
|
195
|
+
const sqlQuery = sql.raw(queryParts.join(" "));
|
|
196
|
+
const result = await tenantDb.execute(sqlQuery);
|
|
197
|
+
return result.rows;
|
|
198
|
+
}
|
|
199
|
+
async function crossSchemaRaw(db, options) {
|
|
200
|
+
const { tenantSchema, sharedSchema, sql: rawSql } = options;
|
|
201
|
+
const processedSql = rawSql.replace(/\$tenant\./g, `"${tenantSchema}".`).replace(/\$shared\./g, `"${sharedSchema}".`);
|
|
202
|
+
const query = sql.raw(processedSql);
|
|
203
|
+
const result = await db.execute(query);
|
|
204
|
+
return result.rows;
|
|
205
|
+
}
|
|
206
|
+
function buildCrossSchemaSelect(fields, tenantSchema, _sharedSchema) {
|
|
207
|
+
const columns = Object.entries(fields).map(([alias, column]) => {
|
|
208
|
+
const columnName = column.name;
|
|
209
|
+
return `"${columnName}" as "${alias}"`;
|
|
210
|
+
});
|
|
211
|
+
const getSchema = () => {
|
|
212
|
+
return tenantSchema;
|
|
213
|
+
};
|
|
214
|
+
return { columns, getSchema };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { CrossSchemaQueryBuilder, buildCrossSchemaSelect, createCrossSchemaQuery, crossSchemaRaw, withSharedLookup };
|
|
218
|
+
//# sourceMappingURL=index.js.map
|
|
219
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cross-schema/cross-schema.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { sql, getTableName } from 'drizzle-orm';\nimport type { NodePgDatabase } from 'drizzle-orm/node-postgres';\nimport type { SQL, Table, Column } from 'drizzle-orm';\nimport type {\n CrossSchemaContext,\n SchemaSource,\n JoinType,\n JoinCondition,\n SharedLookupConfig,\n CrossSchemaRawOptions,\n} from './types.js';\n\n/**\n * Cross-schema query builder for joining tenant and shared data\n *\n * @example\n * ```typescript\n * const query = createCrossSchemaQuery({\n * tenantDb: tenants.getDb('tenant-uuid'),\n * sharedDb: tenants.getSharedDb(),\n * });\n *\n * const results = await query\n * .from('tenant', orders)\n * .leftJoin('shared', subscriptionPlans, eq(orders.planId, subscriptionPlans.id))\n * .select({\n * orderId: orders.id,\n * planName: subscriptionPlans.name,\n * })\n * .execute();\n * ```\n */\nexport class CrossSchemaQueryBuilder<\n TTenantSchema extends Record<string, unknown> = Record<string, unknown>,\n TSharedSchema extends Record<string, unknown> = Record<string, unknown>,\n> {\n private fromTable: { table: Table; source: SchemaSource; schemaName: string } | null = null;\n private joins: Array<{\n table: Table;\n source: SchemaSource;\n schemaName: string;\n condition: JoinCondition;\n type: JoinType;\n }> = [];\n private selectFields: Record<string, Column> = {};\n private whereCondition: SQL<unknown> | null = null;\n private orderByFields: SQL<unknown>[] = [];\n private limitValue: number | null = null;\n private offsetValue: number | null = null;\n\n constructor(private readonly context: CrossSchemaContext<TTenantSchema, TSharedSchema>) {}\n\n /**\n * Set the main table to query from\n */\n from<T extends Table>(source: SchemaSource, table: T): this {\n const schemaName = this.getSchemaName(source);\n this.fromTable = { table, source, schemaName };\n return this;\n }\n\n /**\n * Add an inner join\n */\n innerJoin<T extends Table>(source: SchemaSource, table: T, condition: JoinCondition): this {\n return this.addJoin(source, table, condition, 'inner');\n }\n\n /**\n * Add a left join\n */\n leftJoin<T extends Table>(source: SchemaSource, table: T, condition: JoinCondition): this {\n return this.addJoin(source, table, condition, 'left');\n }\n\n /**\n * Add a right join\n */\n rightJoin<T extends Table>(source: SchemaSource, table: T, condition: JoinCondition): this {\n return this.addJoin(source, table, condition, 'right');\n }\n\n /**\n * Add a full outer join\n */\n fullJoin<T extends Table>(source: SchemaSource, table: T, condition: JoinCondition): this {\n return this.addJoin(source, table, condition, 'full');\n }\n\n /**\n * Select specific fields\n */\n select<T extends Record<string, Column>>(fields: T): this {\n this.selectFields = fields;\n return this;\n }\n\n /**\n * Add a where condition\n */\n where(condition: SQL<unknown>): this {\n this.whereCondition = condition;\n return this;\n }\n\n /**\n * Add order by\n */\n orderBy(...fields: SQL<unknown>[]): this {\n this.orderByFields = fields;\n return this;\n }\n\n /**\n * Set limit\n */\n limit(value: number): this {\n this.limitValue = value;\n return this;\n }\n\n /**\n * Set offset\n */\n offset(value: number): this {\n this.offsetValue = value;\n return this;\n }\n\n /**\n * Execute the query and return typed results\n */\n async execute<TResult = Record<string, unknown>>(): Promise<TResult[]> {\n if (!this.fromTable) {\n throw new Error('[drizzle-multitenant] No table specified. Use .from() first.');\n }\n\n const sqlQuery = this.buildSql();\n\n // Use the tenant db to execute (it has access to both schemas via search_path)\n const result = await this.context.tenantDb.execute(sqlQuery);\n\n return result.rows as TResult[];\n }\n\n /**\n * Build the SQL query\n */\n private buildSql(): SQL<unknown> {\n if (!this.fromTable) {\n throw new Error('[drizzle-multitenant] No table specified');\n }\n\n const parts: SQL<unknown>[] = [];\n\n // SELECT clause\n const selectParts = Object.entries(this.selectFields).map(([alias, column]) => {\n const columnName = column.name;\n return sql`${sql.raw(`\"${columnName}\"`)} as ${sql.raw(`\"${alias}\"`)}`;\n });\n\n if (selectParts.length === 0) {\n parts.push(sql`SELECT *`);\n } else {\n parts.push(sql`SELECT ${sql.join(selectParts, sql`, `)}`);\n }\n\n // FROM clause\n const fromTableRef = this.getFullTableName(this.fromTable.schemaName, this.fromTable.table);\n parts.push(sql` FROM ${sql.raw(fromTableRef)}`);\n\n // JOIN clauses\n for (const join of this.joins) {\n const joinTableRef = this.getFullTableName(join.schemaName, join.table);\n const joinType = this.getJoinKeyword(join.type);\n parts.push(sql` ${sql.raw(joinType)} ${sql.raw(joinTableRef)} ON ${join.condition}`);\n }\n\n // WHERE clause\n if (this.whereCondition) {\n parts.push(sql` WHERE ${this.whereCondition}`);\n }\n\n // ORDER BY clause\n if (this.orderByFields.length > 0) {\n parts.push(sql` ORDER BY ${sql.join(this.orderByFields, sql`, `)}`);\n }\n\n // LIMIT clause\n if (this.limitValue !== null) {\n parts.push(sql` LIMIT ${sql.raw(this.limitValue.toString())}`);\n }\n\n // OFFSET clause\n if (this.offsetValue !== null) {\n parts.push(sql` OFFSET ${sql.raw(this.offsetValue.toString())}`);\n }\n\n return sql.join(parts, sql``);\n }\n\n /**\n * Add a join to the query\n */\n private addJoin<T extends Table>(\n source: SchemaSource,\n table: T,\n condition: JoinCondition,\n type: JoinType\n ): this {\n const schemaName = this.getSchemaName(source);\n this.joins.push({ table, source, schemaName, condition, type });\n return this;\n }\n\n /**\n * Get schema name for a source\n */\n private getSchemaName(source: SchemaSource): string {\n if (source === 'tenant') {\n return this.context.tenantSchema ?? 'tenant';\n }\n return this.context.sharedSchema ?? 'public';\n }\n\n /**\n * Get fully qualified table name\n */\n private getFullTableName(schemaName: string, table: Table): string {\n const tableName = getTableName(table);\n return `\"${schemaName}\".\"${tableName}\"`;\n }\n\n /**\n * Get SQL keyword for join type\n */\n private getJoinKeyword(type: JoinType): string {\n switch (type) {\n case 'inner':\n return 'INNER JOIN';\n case 'left':\n return 'LEFT JOIN';\n case 'right':\n return 'RIGHT JOIN';\n case 'full':\n return 'FULL OUTER JOIN';\n }\n }\n}\n\n/**\n * Create a cross-schema query builder\n */\nexport function createCrossSchemaQuery<\n TTenantSchema extends Record<string, unknown> = Record<string, unknown>,\n TSharedSchema extends Record<string, unknown> = Record<string, unknown>,\n>(\n context: CrossSchemaContext<TTenantSchema, TSharedSchema>\n): CrossSchemaQueryBuilder<TTenantSchema, TSharedSchema> {\n return new CrossSchemaQueryBuilder(context);\n}\n\n/**\n * Helper for common pattern: tenant table with shared lookup\n *\n * @example\n * ```typescript\n * const ordersWithPlans = await withSharedLookup({\n * tenantDb,\n * sharedDb,\n * tenantTable: orders,\n * sharedTable: subscriptionPlans,\n * foreignKey: 'planId',\n * sharedFields: ['name', 'features', 'price'],\n * });\n * ```\n */\nexport async function withSharedLookup<\n TTenantTable extends Table,\n TSharedTable extends Table,\n TSharedFields extends keyof TSharedTable['_']['columns'],\n>(config: SharedLookupConfig<TTenantTable, TSharedTable, TSharedFields>): Promise<Array<Record<string, unknown>>> {\n const {\n tenantDb,\n tenantTable,\n sharedTable,\n foreignKey,\n sharedKey = 'id' as keyof TSharedTable['_']['columns'],\n sharedFields,\n where: whereCondition,\n } = config;\n\n // Get table names using Drizzle's utility\n const tenantTableName = getTableName(tenantTable);\n const sharedTableName = getTableName(sharedTable);\n\n // Build field list for shared table\n const sharedFieldList = sharedFields\n .map((field) => `s.\"${String(field)}\"`)\n .join(', ');\n\n // Build the query\n const queryParts = [\n `SELECT t.*, ${sharedFieldList}`,\n `FROM \"${tenantTableName}\" t`,\n `LEFT JOIN \"public\".\"${sharedTableName}\" s ON t.\"${String(foreignKey)}\" = s.\"${String(sharedKey)}\"`,\n ];\n\n if (whereCondition) {\n queryParts.push('WHERE');\n // We'll need to use raw SQL for the where condition\n }\n\n const sqlQuery = sql.raw(queryParts.join(' '));\n\n const result = await tenantDb.execute(sqlQuery);\n\n return result.rows as Array<Record<string, unknown>>;\n}\n\n/**\n * Execute raw cross-schema SQL with type safety\n *\n * @example\n * ```typescript\n * const result = await crossSchemaRaw<{ userName: string; planName: string }>({\n * tenantSchema: 'tenant_abc123',\n * sharedSchema: 'public',\n * sql: `\n * SELECT u.name as \"userName\", p.name as \"planName\"\n * FROM $tenant.users u\n * JOIN $shared.plans p ON u.plan_id = p.id\n * `,\n * });\n * ```\n */\nexport async function crossSchemaRaw<TResult = Record<string, unknown>>(\n db: NodePgDatabase<Record<string, unknown>>,\n options: CrossSchemaRawOptions\n): Promise<TResult[]> {\n const { tenantSchema, sharedSchema, sql: rawSql } = options;\n\n // Replace $tenant and $shared placeholders\n const processedSql = rawSql\n .replace(/\\$tenant\\./g, `\"${tenantSchema}\".`)\n .replace(/\\$shared\\./g, `\"${sharedSchema}\".`);\n\n const query = sql.raw(processedSql);\n\n const result = await db.execute(query);\n\n return result.rows as TResult[];\n}\n\n/**\n * Create a typed cross-schema query using SQL template\n *\n * @example\n * ```typescript\n * const users = await crossSchemaSelect(tenantDb, {\n * tenantSchema: 'tenant_abc',\n * sharedSchema: 'public',\n * select: {\n * id: users.id,\n * name: users.name,\n * planName: plans.name,\n * },\n * from: { table: users, schema: 'tenant' },\n * joins: [\n * { table: plans, schema: 'shared', on: eq(users.planId, plans.id), type: 'left' },\n * ],\n * });\n * ```\n */\nexport function buildCrossSchemaSelect<T extends Record<string, Column>>(\n fields: T,\n tenantSchema: string,\n _sharedSchema: string\n): { columns: string[]; getSchema: () => string } {\n const columns = Object.entries(fields).map(([alias, column]) => {\n const columnName = column.name;\n return `\"${columnName}\" as \"${alias}\"`;\n });\n\n const getSchema = (): string => {\n // This would need more context to determine which schema a column belongs to\n // For now, return tenant schema as default\n return tenantSchema;\n };\n\n return { columns, getSchema };\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { C as Config, T as TenantManager } from './types-DKVaTaIb.js';
|
|
2
|
+
export { b as ConnectionConfig, D as DEFAULT_CONFIG, H as Hooks, I as IsolationConfig, c as IsolationStrategy, M as MetricsConfig, P as PoolEntry, d as SchemasConfig, S as SharedDb, a as TenantDb } from './types-DKVaTaIb.js';
|
|
3
|
+
export { B as BaseTenantContext, a as TenantContext, T as TenantContextData, c as createTenantContext } from './context-DBerWr50.js';
|
|
4
|
+
export { AppliedMigration, CreateTenantOptions, DropTenantOptions, MigrateOptions, MigrationErrorHandler, MigrationFile, MigrationHooks, MigrationProgressCallback, MigrationResults, Migrator, MigratorConfig, TenantMigrationResult, TenantMigrationStatus, createMigrator } from './migrator/index.js';
|
|
5
|
+
export { ColumnSelection, CrossSchemaContext, CrossSchemaQueryBuilder, CrossSchemaRawOptions, InferSelectedColumns, JoinCondition, JoinDefinition, JoinType, LookupResult, SchemaSource, SharedLookupConfig, TableReference, buildCrossSchemaSelect, createCrossSchemaQuery, crossSchemaRaw, withSharedLookup } from './cross-schema/index.js';
|
|
6
|
+
import 'pg';
|
|
7
|
+
import 'drizzle-orm/node-postgres';
|
|
8
|
+
import 'drizzle-orm';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Define configuration for drizzle-multitenant
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { defineConfig } from 'drizzle-multitenant';
|
|
16
|
+
* import * as tenantSchema from './schemas/tenant';
|
|
17
|
+
* import * as sharedSchema from './schemas/shared';
|
|
18
|
+
*
|
|
19
|
+
* export default defineConfig({
|
|
20
|
+
* connection: {
|
|
21
|
+
* url: process.env.DATABASE_URL!,
|
|
22
|
+
* poolConfig: {
|
|
23
|
+
* max: 10,
|
|
24
|
+
* idleTimeoutMillis: 30000,
|
|
25
|
+
* },
|
|
26
|
+
* },
|
|
27
|
+
* isolation: {
|
|
28
|
+
* strategy: 'schema',
|
|
29
|
+
* schemaNameTemplate: (tenantId) => `tenant_${tenantId.replace(/-/g, '_')}`,
|
|
30
|
+
* maxPools: 50,
|
|
31
|
+
* poolTtlMs: 60 * 60 * 1000,
|
|
32
|
+
* },
|
|
33
|
+
* schemas: {
|
|
34
|
+
* tenant: tenantSchema,
|
|
35
|
+
* shared: sharedSchema,
|
|
36
|
+
* },
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function defineConfig<TTenantSchema extends Record<string, unknown>, TSharedSchema extends Record<string, unknown> = Record<string, unknown>>(config: Config<TTenantSchema, TSharedSchema>): Config<TTenantSchema, TSharedSchema>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a tenant manager instance
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { createTenantManager, defineConfig } from 'drizzle-multitenant';
|
|
48
|
+
*
|
|
49
|
+
* const config = defineConfig({
|
|
50
|
+
* connection: { url: process.env.DATABASE_URL! },
|
|
51
|
+
* isolation: {
|
|
52
|
+
* strategy: 'schema',
|
|
53
|
+
* schemaNameTemplate: (id) => `tenant_${id}`,
|
|
54
|
+
* },
|
|
55
|
+
* schemas: { tenant: tenantSchema },
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* const tenants = createTenantManager(config);
|
|
59
|
+
*
|
|
60
|
+
* // Get database for a specific tenant
|
|
61
|
+
* const db = tenants.getDb('tenant-uuid');
|
|
62
|
+
* const users = await db.select().from(schema.users);
|
|
63
|
+
*
|
|
64
|
+
* // Get shared database
|
|
65
|
+
* const sharedDb = tenants.getSharedDb();
|
|
66
|
+
* const plans = await sharedDb.select().from(sharedSchema.plans);
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare function createTenantManager<TTenantSchema extends Record<string, unknown>, TSharedSchema extends Record<string, unknown> = Record<string, unknown>>(config: Config<TTenantSchema, TSharedSchema>): TenantManager<TTenantSchema, TSharedSchema>;
|
|
70
|
+
|
|
71
|
+
export { Config, TenantManager, createTenantManager, defineConfig };
|