drizzle-multitenant 1.1.0 → 1.3.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.
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Scaffold module types
3
+ *
4
+ * @module scaffold/types
5
+ */
6
+ /**
7
+ * Type of scaffold to generate
8
+ */
9
+ type ScaffoldType = 'tenant' | 'shared';
10
+ /**
11
+ * Kind of scaffold template
12
+ */
13
+ type ScaffoldKind = 'schema' | 'seed' | 'migration';
14
+ /**
15
+ * Options for scaffolding a schema
16
+ */
17
+ interface ScaffoldSchemaOptions {
18
+ /** Name of the schema/table (e.g., "orders", "products") */
19
+ name: string;
20
+ /** Type of schema (tenant or shared) */
21
+ type: ScaffoldType;
22
+ /** Output directory for the schema file */
23
+ outputDir?: string;
24
+ /** Whether to include example columns */
25
+ includeExample?: boolean;
26
+ /** Whether to include timestamps (createdAt, updatedAt) */
27
+ includeTimestamps?: boolean;
28
+ /** Whether to include soft delete (deletedAt) */
29
+ includeSoftDelete?: boolean;
30
+ /** Whether to use UUID for primary key (default: true) */
31
+ useUuid?: boolean;
32
+ }
33
+ /**
34
+ * Options for scaffolding a seed
35
+ */
36
+ interface ScaffoldSeedOptions {
37
+ /** Name of the seed file (e.g., "initial", "demo-data") */
38
+ name: string;
39
+ /** Type of seed (tenant or shared) */
40
+ type: ScaffoldType;
41
+ /** Output directory for the seed file */
42
+ outputDir?: string;
43
+ /** Optional table name to seed */
44
+ tableName?: string;
45
+ }
46
+ /**
47
+ * Options for scaffolding a migration
48
+ */
49
+ interface ScaffoldMigrationOptions {
50
+ /** Name of the migration (e.g., "add-orders", "create-users") */
51
+ name: string;
52
+ /** Type of migration (tenant or shared) */
53
+ type: ScaffoldType;
54
+ /** Output directory for the migration file */
55
+ outputDir?: string;
56
+ /** Migration template type */
57
+ template?: MigrationTemplate;
58
+ }
59
+ /**
60
+ * Available migration templates
61
+ */
62
+ type MigrationTemplate = 'create-table' | 'add-column' | 'add-index' | 'add-foreign-key' | 'blank';
63
+ /**
64
+ * Result of a scaffold operation
65
+ */
66
+ interface ScaffoldResult {
67
+ /** Whether the operation was successful */
68
+ success: boolean;
69
+ /** Path to the generated file */
70
+ filePath: string;
71
+ /** Name of the generated file */
72
+ fileName: string;
73
+ /** Type of scaffold generated */
74
+ kind: ScaffoldKind;
75
+ /** Tenant or shared */
76
+ type: ScaffoldType;
77
+ /** Optional error message if failed */
78
+ error?: string;
79
+ }
80
+ /**
81
+ * Template context for schema generation
82
+ */
83
+ interface SchemaTemplateContext {
84
+ /** Table name in snake_case */
85
+ tableName: string;
86
+ /** Table name in PascalCase */
87
+ tableNamePascal: string;
88
+ /** Table name in camelCase */
89
+ tableNameCamel: string;
90
+ /** Schema type (tenant or shared) */
91
+ type: ScaffoldType;
92
+ /** Whether to include timestamps */
93
+ includeTimestamps: boolean;
94
+ /** Whether to include soft delete */
95
+ includeSoftDelete: boolean;
96
+ /** Whether to use UUID for primary key */
97
+ useUuid: boolean;
98
+ /** Whether to include example columns */
99
+ includeExample: boolean;
100
+ }
101
+ /**
102
+ * Template context for seed generation
103
+ */
104
+ interface SeedTemplateContext {
105
+ /** Seed function name in camelCase */
106
+ seedName: string;
107
+ /** Table name (optional) */
108
+ tableName?: string;
109
+ /** Schema type (tenant or shared) */
110
+ type: ScaffoldType;
111
+ }
112
+ /**
113
+ * Template context for migration generation
114
+ */
115
+ interface MigrationTemplateContext {
116
+ /** Migration name for comments */
117
+ migrationName: string;
118
+ /** Schema type (tenant or shared) */
119
+ type: ScaffoldType;
120
+ /** Template type */
121
+ template: MigrationTemplate;
122
+ /** Table name (derived from migration name) */
123
+ tableName?: string;
124
+ }
125
+ /**
126
+ * Generated file output
127
+ */
128
+ interface GeneratedFile {
129
+ /** File path relative to project root */
130
+ path: string;
131
+ /** File content */
132
+ content: string;
133
+ }
134
+ /**
135
+ * Scaffold configuration from tenant.config.ts
136
+ */
137
+ interface ScaffoldConfig {
138
+ /** Schema output directory */
139
+ schemaDir?: string;
140
+ /** Seed output directory */
141
+ seedDir?: string;
142
+ /** Migrations folder */
143
+ migrationsFolder?: string;
144
+ /** Shared migrations folder */
145
+ sharedMigrationsFolder?: string;
146
+ }
147
+
148
+ /**
149
+ * Scaffold generator
150
+ *
151
+ * Orchestrates the generation of schema, seed, and migration files.
152
+ *
153
+ * @module scaffold/generator
154
+ */
155
+
156
+ /**
157
+ * Convert a name to various case formats
158
+ */
159
+ declare function toCase(name: string): {
160
+ snake: string;
161
+ pascal: string;
162
+ camel: string;
163
+ };
164
+ /**
165
+ * Default output directories
166
+ */
167
+ declare const DEFAULT_DIRS: {
168
+ readonly schemaDir: "src/db/schema";
169
+ readonly seedDir: "drizzle/seeds";
170
+ readonly tenantMigrationsDir: "drizzle/tenant-migrations";
171
+ readonly sharedMigrationsDir: "drizzle/shared-migrations";
172
+ };
173
+ /**
174
+ * Scaffold a new Drizzle schema file
175
+ *
176
+ * @param options - Scaffold options
177
+ * @returns Result of the scaffold operation
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * const result = await scaffoldSchema({
182
+ * name: 'orders',
183
+ * type: 'tenant',
184
+ * includeTimestamps: true,
185
+ * });
186
+ *
187
+ * if (result.success) {
188
+ * console.log(`Created: ${result.filePath}`);
189
+ * }
190
+ * ```
191
+ */
192
+ declare function scaffoldSchema(options: ScaffoldSchemaOptions): Promise<ScaffoldResult>;
193
+ /**
194
+ * Scaffold a new seed file
195
+ *
196
+ * @param options - Scaffold options
197
+ * @returns Result of the scaffold operation
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * const result = await scaffoldSeed({
202
+ * name: 'initial',
203
+ * type: 'tenant',
204
+ * tableName: 'users',
205
+ * });
206
+ * ```
207
+ */
208
+ declare function scaffoldSeed(options: ScaffoldSeedOptions): Promise<ScaffoldResult>;
209
+ /**
210
+ * Scaffold a new migration file
211
+ *
212
+ * @param options - Scaffold options
213
+ * @returns Result of the scaffold operation
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * const result = await scaffoldMigration({
218
+ * name: 'add-orders',
219
+ * type: 'tenant',
220
+ * template: 'create-table',
221
+ * });
222
+ * ```
223
+ */
224
+ declare function scaffoldMigration(options: ScaffoldMigrationOptions): Promise<ScaffoldResult>;
225
+ /**
226
+ * Get available migration templates
227
+ */
228
+ declare function getMigrationTemplates(): Array<{
229
+ value: MigrationTemplate;
230
+ label: string;
231
+ description: string;
232
+ }>;
233
+
234
+ /**
235
+ * Schema template generator
236
+ *
237
+ * @module scaffold/templates/schema-template
238
+ */
239
+
240
+ /**
241
+ * Generate a Drizzle schema file content
242
+ *
243
+ * @param context - Template context
244
+ * @returns Generated TypeScript code for the schema
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * const content = generateSchemaTemplate({
249
+ * tableName: 'orders',
250
+ * tableNamePascal: 'Orders',
251
+ * tableNameCamel: 'orders',
252
+ * type: 'tenant',
253
+ * includeTimestamps: true,
254
+ * includeSoftDelete: false,
255
+ * useUuid: true,
256
+ * includeExample: true,
257
+ * });
258
+ * ```
259
+ */
260
+ declare function generateSchemaTemplate(context: SchemaTemplateContext): string;
261
+
262
+ /**
263
+ * Seed template generator
264
+ *
265
+ * @module scaffold/templates/seed-template
266
+ */
267
+
268
+ /**
269
+ * Generate a seed file content
270
+ *
271
+ * @param context - Template context
272
+ * @returns Generated TypeScript code for the seed
273
+ *
274
+ * @example
275
+ * ```typescript
276
+ * const content = generateSeedTemplate({
277
+ * seedName: 'initialData',
278
+ * type: 'tenant',
279
+ * tableName: 'users',
280
+ * });
281
+ * ```
282
+ */
283
+ declare function generateSeedTemplate(context: SeedTemplateContext): string;
284
+
285
+ /**
286
+ * Migration template generator
287
+ *
288
+ * @module scaffold/templates/migration-template
289
+ */
290
+
291
+ /**
292
+ * Generate a SQL migration file content
293
+ *
294
+ * @param context - Template context
295
+ * @returns Generated SQL migration content
296
+ *
297
+ * @example
298
+ * ```typescript
299
+ * const content = generateMigrationTemplate({
300
+ * migrationName: 'add-orders',
301
+ * type: 'tenant',
302
+ * template: 'create-table',
303
+ * tableName: 'orders',
304
+ * });
305
+ * ```
306
+ */
307
+ declare function generateMigrationTemplate(context: MigrationTemplateContext): string;
308
+ /**
309
+ * Infer table name from migration name
310
+ *
311
+ * @param migrationName - Name of the migration
312
+ * @returns Inferred table name or undefined
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * inferTableName('add-orders') // 'orders'
317
+ * inferTableName('create-user-profiles') // 'user_profiles'
318
+ * inferTableName('add-index-to-users') // 'users'
319
+ * ```
320
+ */
321
+ declare function inferTableName(migrationName: string): string | undefined;
322
+ /**
323
+ * Determine the best template based on migration name
324
+ *
325
+ * @param migrationName - Name of the migration
326
+ * @returns Suggested template type
327
+ */
328
+ declare function inferMigrationTemplate(migrationName: string): MigrationTemplate;
329
+
330
+ export { DEFAULT_DIRS, type GeneratedFile, type MigrationTemplate, type MigrationTemplateContext, type ScaffoldConfig, type ScaffoldKind, type ScaffoldMigrationOptions, type ScaffoldResult, type ScaffoldSchemaOptions, type ScaffoldSeedOptions, type ScaffoldType, type SchemaTemplateContext, type SeedTemplateContext, generateMigrationTemplate, generateSchemaTemplate, generateSeedTemplate, getMigrationTemplates, inferMigrationTemplate, inferTableName, scaffoldMigration, scaffoldSchema, scaffoldSeed, toCase };
@@ -0,0 +1,277 @@
1
+ import {existsSync}from'fs';import {mkdir,writeFile,readdir}from'fs/promises';import {join,resolve,dirname}from'path';function h(t){let{tableName:e,tableNamePascal:n,tableNameCamel:r,type:i}=t,a=w(t),o=F(t),d=k(t),s=z(t),m=M(t);return `/**
2
+ * ${n} schema
3
+ * Type: ${i}
4
+ *
5
+ * @module schema/${i}/${r}
6
+ */
7
+
8
+ ${a}
9
+
10
+ /**
11
+ * ${n} table definition
12
+ */
13
+ export const ${r} = pgTable('${e}', {
14
+ ${o}
15
+ });
16
+
17
+ ${d}
18
+ ${s}
19
+ ${m}
20
+ `}function w(t){let{includeTimestamps:e,includeSoftDelete:n,useUuid:r,includeExample:i}=t,a=["pgTable"];return r?a.push("uuid"):a.push("serial"),a.push("text"),i&&a.push("varchar","boolean"),(e||n)&&a.push("timestamp"),a.push("index"),`import { ${[...new Set(a)].sort().join(", ")} } from 'drizzle-orm/pg-core';
21
+ import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
22
+ import { z } from 'zod';`}function F(t){let{includeTimestamps:e,includeSoftDelete:n,useUuid:r,includeExample:i}=t,a=[];return r?a.push(" id: uuid('id').primaryKey().defaultRandom(),"):a.push(" id: serial('id').primaryKey(),"),i&&(a.push(" name: varchar('name', { length: 255 }).notNull(),"),a.push(" description: text('description'),"),a.push(" isActive: boolean('is_active').notNull().default(true),")),e&&(a.push(" createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),"),a.push(" updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),")),n&&a.push(" deletedAt: timestamp('deleted_at', { withTimezone: true }),"),a.join(`
23
+ `)}function k(t){let{tableNameCamel:e,tableName:n,includeExample:r,includeTimestamps:i}=t,a=[];return r&&(a.push(` nameIdx: index('${n}_name_idx').on(${e}.name),`),a.push(` isActiveIdx: index('${n}_is_active_idx').on(${e}.isActive),`)),i&&a.push(` createdAtIdx: index('${n}_created_at_idx').on(${e}.createdAt),`),a.length===0?"":`/**
24
+ * ${t.tableNamePascal} table indexes
25
+ */
26
+ export const ${e}Indexes = {
27
+ ${a.join(`
28
+ `)}
29
+ };
30
+ `}function z(t){return `// Uncomment and modify to add relations
31
+ // import { relations } from 'drizzle-orm';
32
+ //
33
+ // export const ${t.tableNameCamel}Relations = relations(${t.tableNameCamel}, ({ one, many }) => ({
34
+ // // Add your relations here
35
+ // // user: one(users, {
36
+ // // fields: [${t.tableNameCamel}.userId],
37
+ // // references: [users.id],
38
+ // // }),
39
+ // }));
40
+ `}function M(t){let{tableNameCamel:e,tableNamePascal:n}=t;return `/**
41
+ * Zod schemas for validation
42
+ */
43
+ export const insert${n}Schema = createInsertSchema(${e});
44
+ export const select${n}Schema = createSelectSchema(${e});
45
+
46
+ /**
47
+ * TypeScript types inferred from schema
48
+ */
49
+ export type ${n} = typeof ${e}.$inferSelect;
50
+ export type New${n} = typeof ${e}.$inferInsert;
51
+
52
+ /**
53
+ * Zod types
54
+ */
55
+ export type Insert${n} = z.infer<typeof insert${n}Schema>;
56
+ export type Select${n} = z.infer<typeof select${n}Schema>;
57
+ `}function S(t){let{seedName:e,type:n,tableName:r}=t;return n==="shared"?P(e,r):v(e,r)}function v(t,e){let n=e?`import { ${e} } from '../../src/db/schema/tenant/${e}.js';`:"// import { yourTable } from '../../src/db/schema/tenant/yourTable.js';",r=e?G(e):X();return `/**
58
+ * Tenant seed: ${t}
59
+ *
60
+ * This seed runs in the context of each tenant's schema.
61
+ * Use this to populate initial data for each tenant.
62
+ *
63
+ * @module seeds/tenant/${t}
64
+ */
65
+
66
+ import type { SeedFunction } from 'drizzle-multitenant';
67
+ ${n}
68
+
69
+ /**
70
+ * Seed function that populates tenant data
71
+ *
72
+ * @param db - Drizzle database instance scoped to the tenant
73
+ * @param tenantId - The tenant ID being seeded
74
+ *
75
+ * @example
76
+ * \`\`\`bash
77
+ * npx drizzle-multitenant seed --file=./drizzle/seeds/tenant/${t}.ts --tenant=my-tenant
78
+ * \`\`\`
79
+ */
80
+ export const seed: SeedFunction = async (db, tenantId) => {
81
+ console.log(\`Seeding tenant: \${tenantId}\`);
82
+
83
+ ${r}
84
+ };
85
+
86
+ export default seed;
87
+ `}function P(t,e){let n=e?`import { ${e} } from '../../src/db/schema/shared/${e}.js';`:"// import { yourTable } from '../../src/db/schema/shared/yourTable.js';",r=e?B(e):j();return `/**
88
+ * Shared seed: ${t}
89
+ *
90
+ * This seed runs against the shared/public schema.
91
+ * Use this to populate global data like plans, roles, permissions, etc.
92
+ *
93
+ * @module seeds/shared/${t}
94
+ */
95
+
96
+ import type { SharedSeedFunction } from 'drizzle-multitenant';
97
+ ${n}
98
+
99
+ /**
100
+ * Seed function that populates shared data
101
+ *
102
+ * @param db - Drizzle database instance for the shared schema
103
+ *
104
+ * @example
105
+ * \`\`\`bash
106
+ * npx drizzle-multitenant seed:shared --file=./drizzle/seeds/shared/${t}.ts
107
+ * \`\`\`
108
+ */
109
+ export const seed: SharedSeedFunction = async (db) => {
110
+ console.log('Seeding shared schema...');
111
+
112
+ ${r}
113
+ };
114
+
115
+ export default seed;
116
+ `}function G(t){return ` // Insert data with conflict handling (idempotent)
117
+ await db.insert(${t}).values([
118
+ {
119
+ name: 'Example Item 1',
120
+ // Add your columns here
121
+ },
122
+ {
123
+ name: 'Example Item 2',
124
+ // Add your columns here
125
+ },
126
+ ]).onConflictDoNothing();
127
+
128
+ console.log(\`Seeded ${t} for tenant: \${tenantId}\`);`}function B(t){return ` // Insert data with conflict handling (idempotent)
129
+ await db.insert(${t}).values([
130
+ {
131
+ name: 'Example Item 1',
132
+ // Add your columns here
133
+ },
134
+ {
135
+ name: 'Example Item 2',
136
+ // Add your columns here
137
+ },
138
+ ]).onConflictDoNothing();
139
+
140
+ console.log('Seeded ${t} in shared schema');`}function X(){return ` // Example: Insert initial data
141
+ // await db.insert(yourTable).values([
142
+ // { name: 'Item 1', description: 'Description 1' },
143
+ // { name: 'Item 2', description: 'Description 2' },
144
+ // ]).onConflictDoNothing();
145
+
146
+ // You can use tenantId to customize data per tenant
147
+ // if (tenantId === 'demo-tenant') {
148
+ // await db.insert(yourTable).values([
149
+ // { name: 'Demo Item', description: 'Only for demo tenant' },
150
+ // ]);
151
+ // }
152
+
153
+ console.log(\`Seed completed for tenant: \${tenantId}\`);`}function j(){return ` // Example: Insert shared data like plans, roles, permissions
154
+ // await db.insert(plans).values([
155
+ // { id: 'free', name: 'Free', price: 0 },
156
+ // { id: 'pro', name: 'Pro', price: 29 },
157
+ // { id: 'enterprise', name: 'Enterprise', price: 99 },
158
+ // ]).onConflictDoNothing();
159
+
160
+ // await db.insert(roles).values([
161
+ // { id: 'admin', name: 'Administrator', permissions: ['*'] },
162
+ // { id: 'user', name: 'User', permissions: ['read'] },
163
+ // ]).onConflictDoNothing();
164
+
165
+ console.log('Shared seed completed');`}function N(t){let{migrationName:e,type:n,template:r,tableName:i}=t,a=new Date().toISOString(),o=`-- Migration: ${e}
166
+ -- Type: ${n}
167
+ -- Created at: ${a}
168
+ -- Template: ${r}
169
+
170
+ `;switch(r){case "create-table":return o+K(i);case "add-column":return o+W(i);case "add-index":return o+q(i);case "add-foreign-key":return o+Y(i);default:return o+H()}}function K(t){let e=t||"table_name";return `-- Create table: ${e}
171
+ CREATE TABLE IF NOT EXISTS "${e}" (
172
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
173
+ "name" VARCHAR(255) NOT NULL,
174
+ "description" TEXT,
175
+ "is_active" BOOLEAN NOT NULL DEFAULT true,
176
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
177
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
178
+ );
179
+
180
+ -- Create indexes
181
+ CREATE INDEX IF NOT EXISTS "${e}_name_idx" ON "${e}" ("name");
182
+ CREATE INDEX IF NOT EXISTS "${e}_created_at_idx" ON "${e}" ("created_at");
183
+
184
+ -- Add updated_at trigger (optional but recommended)
185
+ CREATE OR REPLACE FUNCTION update_updated_at_column()
186
+ RETURNS TRIGGER AS $$
187
+ BEGIN
188
+ NEW.updated_at = NOW();
189
+ RETURN NEW;
190
+ END;
191
+ $$ LANGUAGE plpgsql;
192
+
193
+ DROP TRIGGER IF EXISTS update_${e}_updated_at ON "${e}";
194
+ CREATE TRIGGER update_${e}_updated_at
195
+ BEFORE UPDATE ON "${e}"
196
+ FOR EACH ROW
197
+ EXECUTE FUNCTION update_updated_at_column();
198
+ `}function W(t){let e=t||"table_name";return `-- Add column to: ${e}
199
+ -- ALTER TABLE "${e}" ADD COLUMN "column_name" data_type [constraints];
200
+
201
+ -- Examples:
202
+
203
+ -- Add a nullable text column
204
+ -- ALTER TABLE "${e}" ADD COLUMN "notes" TEXT;
205
+
206
+ -- Add a non-null column with default
207
+ -- ALTER TABLE "${e}" ADD COLUMN "status" VARCHAR(50) NOT NULL DEFAULT 'pending';
208
+
209
+ -- Add a column with foreign key reference
210
+ -- ALTER TABLE "${e}" ADD COLUMN "user_id" UUID REFERENCES "users" ("id");
211
+
212
+ -- Add a column with check constraint
213
+ -- ALTER TABLE "${e}" ADD COLUMN "priority" INTEGER CHECK (priority >= 1 AND priority <= 5);
214
+
215
+ -- Write your column additions below:
216
+
217
+ `}function q(t){let e=t||"table_name";return `-- Add index to: ${e}
218
+ -- CREATE INDEX [CONCURRENTLY] "index_name" ON "${e}" ("column_name");
219
+
220
+ -- Examples:
221
+
222
+ -- Simple index on single column
223
+ -- CREATE INDEX "${e}_column_idx" ON "${e}" ("column_name");
224
+
225
+ -- Composite index on multiple columns
226
+ -- CREATE INDEX "${e}_col1_col2_idx" ON "${e}" ("column1", "column2");
227
+
228
+ -- Partial index (filtered)
229
+ -- CREATE INDEX "${e}_active_idx" ON "${e}" ("created_at") WHERE "is_active" = true;
230
+
231
+ -- Unique index
232
+ -- CREATE UNIQUE INDEX "${e}_email_unique_idx" ON "${e}" ("email");
233
+
234
+ -- GIN index for full-text search or JSONB
235
+ -- CREATE INDEX "${e}_data_gin_idx" ON "${e}" USING GIN ("data");
236
+
237
+ -- CONCURRENT index (doesn't lock table, recommended for production)
238
+ -- CREATE INDEX CONCURRENTLY "${e}_column_idx" ON "${e}" ("column_name");
239
+
240
+ -- Write your indexes below:
241
+
242
+ `}function Y(t){let e=t||"table_name";return `-- Add foreign key to: ${e}
243
+ -- ALTER TABLE "${e}" ADD CONSTRAINT "fk_name" FOREIGN KEY ("column") REFERENCES "other_table" ("id");
244
+
245
+ -- Examples:
246
+
247
+ -- Basic foreign key
248
+ -- ALTER TABLE "${e}" ADD CONSTRAINT "${e}_user_id_fk"
249
+ -- FOREIGN KEY ("user_id") REFERENCES "users" ("id");
250
+
251
+ -- Foreign key with ON DELETE action
252
+ -- ALTER TABLE "${e}" ADD CONSTRAINT "${e}_user_id_fk"
253
+ -- FOREIGN KEY ("user_id") REFERENCES "users" ("id")
254
+ -- ON DELETE CASCADE;
255
+
256
+ -- Foreign key with ON UPDATE action
257
+ -- ALTER TABLE "${e}" ADD CONSTRAINT "${e}_category_id_fk"
258
+ -- FOREIGN KEY ("category_id") REFERENCES "categories" ("id")
259
+ -- ON DELETE SET NULL
260
+ -- ON UPDATE CASCADE;
261
+
262
+ -- Cross-schema foreign key (to shared/public schema)
263
+ -- ALTER TABLE "${e}" ADD CONSTRAINT "${e}_plan_id_fk"
264
+ -- FOREIGN KEY ("plan_id") REFERENCES "public"."plans" ("id");
265
+
266
+ -- Write your foreign keys below:
267
+
268
+ `}function H(){return `-- Write your SQL migration here
269
+
270
+ -- Up migration (apply changes)
271
+
272
+
273
+ -- Remember to consider:
274
+ -- 1. Idempotency: Use IF NOT EXISTS / IF EXISTS where possible
275
+ -- 2. Transactions: This will run in a transaction
276
+ -- 3. Rollback: Plan how to undo these changes if needed
277
+ `}function x(t){let e=[/^create[-_](.+)$/i,/^add[-_](.+?)[-_]?(?:table)?$/i,/^(?:add|create)[-_](?:index|fk|constraint)[-_](?:to|on)[-_](.+)$/i,/^(.+?)[-_](?:table|schema)$/i];for(let n of e){let r=t.match(n);if(r?.[1])return r[1].toLowerCase().replace(/[-\s]+/g,"_").replace(/s$/,"")}}function $(t){let e=t.toLowerCase();return e.includes("create")||e.includes("table")?"create-table":e.includes("index")||e.includes("idx")?"add-index":e.includes("fk")||e.includes("foreign")||e.includes("reference")?"add-foreign-key":e.includes("add")||e.includes("column")?"add-column":"blank"}function y(t){let e=t.replace(/([a-z])([A-Z])/g,"$1_$2").replace(/[-\s]+/g,"_").toLowerCase(),n=e.split("_").map(i=>i.charAt(0).toUpperCase()+i.slice(1)).join(""),r=n.charAt(0).toLowerCase()+n.slice(1);return {snake:e,pascal:n,camel:r}}var f={schemaDir:"src/db/schema",seedDir:"drizzle/seeds",tenantMigrationsDir:"drizzle/tenant-migrations",sharedMigrationsDir:"drizzle/shared-migrations"};async function Q(t){let{name:e,type:n,outputDir:r,includeExample:i=true,includeTimestamps:a=true,includeSoftDelete:o=false,useUuid:d=true}=t;try{let s=y(e),m={tableName:s.snake,tableNamePascal:s.pascal,tableNameCamel:s.camel,type:n,includeTimestamps:a,includeSoftDelete:o,useUuid:d,includeExample:i},l=h(m),E=r||join(f.schemaDir,n),p=`${s.camel}.ts`,c=resolve(process.cwd(),E,p);return await mkdir(dirname(c),{recursive:!0}),existsSync(c)?{success:!1,filePath:c,fileName:p,kind:"schema",type:n,error:`File already exists: ${c}`}:(await writeFile(c,l,"utf-8"),{success:!0,filePath:c,fileName:p,kind:"schema",type:n})}catch(s){return {success:false,filePath:"",fileName:"",kind:"schema",type:n,error:s instanceof Error?s.message:String(s)}}}async function V(t){let{name:e,type:n,outputDir:r,tableName:i}=t;try{let a=y(e),o={seedName:a.camel,type:n,...i!==void 0&&{tableName:i}},d=S(o),s=r||join(f.seedDir,n),m=`${a.camel}.ts`,l=resolve(process.cwd(),s,m);return await mkdir(dirname(l),{recursive:!0}),existsSync(l)?{success:!1,filePath:l,fileName:m,kind:"seed",type:n,error:`File already exists: ${l}`}:(await writeFile(l,d,"utf-8"),{success:!0,filePath:l,fileName:m,kind:"seed",type:n})}catch(a){return {success:false,filePath:"",fileName:"",kind:"seed",type:n,error:a instanceof Error?a.message:String(a)}}}async function J(t){let{name:e,type:n,outputDir:r,template:i}=t;try{let a=n==="shared"?f.sharedMigrationsDir:f.tenantMigrationsDir,o=r||a,d=resolve(process.cwd(),o);await mkdir(d,{recursive:!0});let m=(existsSync(d)?await readdir(d):[]).filter(g=>g.endsWith(".sql")),l=0;for(let g of m){let R=g.match(/^(\d+)_/);R?.[1]&&(l=Math.max(l,parseInt(R[1],10)));}let E=(l+1).toString().padStart(4,"0"),p=e.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,""),c=`${E}_${p}.sql`,u=join(d,c),O=i||$(e),I=x(e),L={migrationName:e,type:n,template:O,...I!==void 0&&{tableName:I}},U=N(L);return existsSync(u)?{success:!1,filePath:u,fileName:c,kind:"migration",type:n,error:`File already exists: ${u}`}:(await writeFile(u,U,"utf-8"),{success:!0,filePath:u,fileName:c,kind:"migration",type:n})}catch(a){return {success:false,filePath:"",fileName:"",kind:"migration",type:n,error:a instanceof Error?a.message:String(a)}}}function ee(){return [{value:"create-table",label:"Create Table",description:"Template for creating a new table with common columns"},{value:"add-column",label:"Add Column",description:"Template for adding columns to an existing table"},{value:"add-index",label:"Add Index",description:"Template for creating indexes"},{value:"add-foreign-key",label:"Add Foreign Key",description:"Template for adding foreign key constraints"},{value:"blank",label:"Blank",description:"Empty migration with basic comments"}]}export{f as DEFAULT_DIRS,N as generateMigrationTemplate,h as generateSchemaTemplate,S as generateSeedTemplate,ee as getMigrationTemplates,$ as inferMigrationTemplate,x as inferTableName,J as scaffoldMigration,Q as scaffoldSchema,V as scaffoldSeed,y as toCase};
@@ -111,6 +111,52 @@ interface DebugContext {
111
111
  /** Additional metadata */
112
112
  metadata?: Record<string, unknown>;
113
113
  }
114
+ /**
115
+ * Lint rule configuration - can be severity only or [severity, options]
116
+ */
117
+ type LintRuleConfig<TOptions = Record<string, unknown>> = 'off' | 'warn' | 'error' | ['off' | 'warn' | 'error', TOptions];
118
+ /**
119
+ * All available lint rules for configuration
120
+ */
121
+ interface LintRulesConfig {
122
+ /** Enforce table naming convention (snake_case by default) */
123
+ 'table-naming'?: LintRuleConfig<{
124
+ style?: 'snake_case' | 'camelCase' | 'PascalCase' | 'kebab-case';
125
+ exceptions?: string[];
126
+ }>;
127
+ /** Enforce column naming convention (snake_case by default) */
128
+ 'column-naming'?: LintRuleConfig<{
129
+ style?: 'snake_case' | 'camelCase' | 'PascalCase' | 'kebab-case';
130
+ exceptions?: string[];
131
+ }>;
132
+ /** Require every table to have a primary key */
133
+ 'require-primary-key'?: LintRuleConfig;
134
+ /** Prefer UUID over serial/integer for primary keys */
135
+ 'prefer-uuid-pk'?: LintRuleConfig;
136
+ /** Require created_at/updated_at columns */
137
+ 'require-timestamps'?: LintRuleConfig<{
138
+ columns?: string[];
139
+ }>;
140
+ /** Require indexes on foreign key columns */
141
+ 'index-foreign-keys'?: LintRuleConfig;
142
+ /** Warn about CASCADE DELETE on foreign keys */
143
+ 'no-cascade-delete'?: LintRuleConfig;
144
+ /** Require soft delete column on tables */
145
+ 'require-soft-delete'?: LintRuleConfig<{
146
+ column?: string;
147
+ }>;
148
+ }
149
+ /**
150
+ * Lint configuration
151
+ */
152
+ interface LintConfig {
153
+ /** Lint rules configuration */
154
+ rules?: LintRulesConfig;
155
+ /** Glob patterns for schema files to include */
156
+ include?: string[];
157
+ /** Glob patterns for schema files to exclude */
158
+ exclude?: string[];
159
+ }
114
160
  /**
115
161
  * Main configuration interface
116
162
  */
@@ -127,6 +173,8 @@ interface Config<TTenantSchema extends Record<string, unknown> = Record<string,
127
173
  metrics?: MetricsConfig;
128
174
  /** Debug configuration */
129
175
  debug?: DebugConfig;
176
+ /** Schema linting configuration */
177
+ lint?: LintConfig;
130
178
  }
131
179
  /**
132
180
  * Internal pool entry for LRU cache
@@ -346,4 +394,4 @@ declare const DEFAULT_CONFIG: {
346
394
  };
347
395
  };
348
396
 
349
- export { type Config as C, type DebugConfig as D, type Hooks as H, type IsolationConfig as I, type MetricsConfig as M, type PoolEntry as P, type RetryConfig as R, type SharedDb as S, type TenantManager as T, type WarmupOptions as W, type TenantDb as a, type ConnectionConfig as b, type IsolationStrategy as c, type SchemasConfig as d, type DebugContext as e, type WarmupResult as f, type TenantWarmupResult as g, type HealthCheckOptions as h, type HealthCheckResult as i, type PoolHealth as j, type PoolHealthStatus as k, type MetricsResult as l, type TenantPoolMetrics as m, type ConnectionMetrics as n, DEFAULT_CONFIG as o };
397
+ export { type Config as C, type DebugConfig as D, type Hooks as H, type IsolationConfig as I, type LintRuleConfig as L, type MetricsConfig as M, type PoolEntry as P, type RetryConfig as R, type SharedDb as S, type TenantManager as T, type WarmupOptions as W, type TenantDb as a, type ConnectionConfig as b, type IsolationStrategy as c, type SchemasConfig as d, type DebugContext as e, type WarmupResult as f, type TenantWarmupResult as g, type HealthCheckOptions as h, type HealthCheckResult as i, type PoolHealth as j, type PoolHealthStatus as k, type MetricsResult as l, type TenantPoolMetrics as m, type ConnectionMetrics as n, type LintRulesConfig as o, type LintConfig as p, DEFAULT_CONFIG as q };