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.
- package/README.md +28 -8
- package/dist/cli/index.js +2001 -2442
- package/dist/{context-Vki959ri.d.ts → context-BBLPNjmk.d.ts} +1 -1
- package/dist/cross-schema/index.js +1 -426
- package/dist/export/index.d.ts +395 -0
- package/dist/export/index.js +9 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +149 -2437
- package/dist/integrations/express.d.ts +3 -3
- package/dist/integrations/express.js +1 -110
- package/dist/integrations/fastify.d.ts +3 -3
- package/dist/integrations/fastify.js +1 -236
- package/dist/integrations/hono.js +0 -3
- package/dist/integrations/nestjs/index.d.ts +1 -1
- package/dist/integrations/nestjs/index.js +3 -10759
- package/dist/lint/index.d.ts +475 -0
- package/dist/lint/index.js +5 -0
- package/dist/metrics/index.d.ts +530 -0
- package/dist/metrics/index.js +3 -0
- package/dist/migrator/index.d.ts +1087 -270
- package/dist/migrator/index.js +149 -970
- package/dist/migrator-B7oPKe73.d.ts +1067 -0
- package/dist/scaffold/index.d.ts +330 -0
- package/dist/scaffold/index.js +277 -0
- package/dist/{types-BhK96FPC.d.ts → types-CGqsPe2Q.d.ts} +49 -1
- package/package.json +18 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cross-schema/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/express.js.map +0 -1
- package/dist/integrations/fastify.js.map +0 -1
- package/dist/integrations/hono.js.map +0 -1
- package/dist/integrations/nestjs/index.js.map +0 -1
- package/dist/migrator/index.js.map +0 -1
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Linting Module Types
|
|
3
|
+
*
|
|
4
|
+
* Provides type definitions for schema validation and linting.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Severity level for lint rules
|
|
8
|
+
*/
|
|
9
|
+
type LintSeverity = 'off' | 'warn' | 'error';
|
|
10
|
+
/**
|
|
11
|
+
* Naming style options
|
|
12
|
+
*/
|
|
13
|
+
type NamingStyle = 'snake_case' | 'camelCase' | 'PascalCase' | 'kebab-case';
|
|
14
|
+
/**
|
|
15
|
+
* Rule configuration - can be severity only or [severity, options]
|
|
16
|
+
*/
|
|
17
|
+
type RuleConfig<TOptions = Record<string, unknown>> = LintSeverity | [LintSeverity, TOptions];
|
|
18
|
+
/**
|
|
19
|
+
* Naming rule options
|
|
20
|
+
*/
|
|
21
|
+
interface NamingRuleOptions {
|
|
22
|
+
style: NamingStyle;
|
|
23
|
+
/** Allow exceptions matching these patterns */
|
|
24
|
+
exceptions?: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Lint configuration for defineConfig
|
|
28
|
+
*/
|
|
29
|
+
interface LintConfig {
|
|
30
|
+
rules: LintRules;
|
|
31
|
+
/** Glob patterns to include */
|
|
32
|
+
include?: string[];
|
|
33
|
+
/** Glob patterns to exclude */
|
|
34
|
+
exclude?: string[];
|
|
35
|
+
/** Custom rules directory */
|
|
36
|
+
customRulesDir?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* All available lint rules
|
|
40
|
+
*/
|
|
41
|
+
interface LintRules {
|
|
42
|
+
'table-naming'?: RuleConfig<NamingRuleOptions>;
|
|
43
|
+
'column-naming'?: RuleConfig<NamingRuleOptions>;
|
|
44
|
+
'require-primary-key'?: RuleConfig;
|
|
45
|
+
'prefer-uuid-pk'?: RuleConfig;
|
|
46
|
+
'require-timestamps'?: RuleConfig<{
|
|
47
|
+
columns?: string[];
|
|
48
|
+
}>;
|
|
49
|
+
'index-foreign-keys'?: RuleConfig;
|
|
50
|
+
'no-cascade-delete'?: RuleConfig;
|
|
51
|
+
'require-soft-delete'?: RuleConfig<{
|
|
52
|
+
column?: string;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Default lint rules configuration
|
|
57
|
+
*/
|
|
58
|
+
declare const DEFAULT_LINT_RULES: LintRules;
|
|
59
|
+
/**
|
|
60
|
+
* Represents a column in a schema
|
|
61
|
+
*/
|
|
62
|
+
interface SchemaColumn {
|
|
63
|
+
name: string;
|
|
64
|
+
dataType: string;
|
|
65
|
+
isPrimaryKey: boolean;
|
|
66
|
+
isNullable: boolean;
|
|
67
|
+
hasDefault: boolean;
|
|
68
|
+
defaultValue?: string | null | undefined;
|
|
69
|
+
references?: {
|
|
70
|
+
table: string;
|
|
71
|
+
column: string;
|
|
72
|
+
onDelete?: string | undefined;
|
|
73
|
+
onUpdate?: string | undefined;
|
|
74
|
+
} | undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Represents an index in a schema
|
|
78
|
+
*/
|
|
79
|
+
interface SchemaIndex {
|
|
80
|
+
name: string;
|
|
81
|
+
columns: string[];
|
|
82
|
+
isUnique: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Represents a table in a schema
|
|
86
|
+
*/
|
|
87
|
+
interface SchemaTable {
|
|
88
|
+
name: string;
|
|
89
|
+
schemaType: 'tenant' | 'shared';
|
|
90
|
+
columns: SchemaColumn[];
|
|
91
|
+
indexes: SchemaIndex[];
|
|
92
|
+
filePath: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Parsed schema info for linting
|
|
96
|
+
*/
|
|
97
|
+
interface ParsedSchema {
|
|
98
|
+
tables: SchemaTable[];
|
|
99
|
+
filePath: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A single lint issue
|
|
103
|
+
*/
|
|
104
|
+
interface LintIssue {
|
|
105
|
+
/** Rule that triggered this issue */
|
|
106
|
+
rule: string;
|
|
107
|
+
/** Severity level */
|
|
108
|
+
severity: 'warn' | 'error';
|
|
109
|
+
/** Human-readable message */
|
|
110
|
+
message: string;
|
|
111
|
+
/** File path where issue was found */
|
|
112
|
+
filePath: string;
|
|
113
|
+
/** Table name (if applicable) */
|
|
114
|
+
table?: string;
|
|
115
|
+
/** Column name (if applicable) */
|
|
116
|
+
column?: string;
|
|
117
|
+
/** Line number (if available) */
|
|
118
|
+
line?: number;
|
|
119
|
+
/** Suggestion to fix the issue */
|
|
120
|
+
suggestion?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Result of linting a single schema file
|
|
124
|
+
*/
|
|
125
|
+
interface LintFileResult {
|
|
126
|
+
filePath: string;
|
|
127
|
+
issues: LintIssue[];
|
|
128
|
+
tables: number;
|
|
129
|
+
columns: number;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Aggregate result of linting all schemas
|
|
133
|
+
*/
|
|
134
|
+
interface LintResult {
|
|
135
|
+
/** All file results */
|
|
136
|
+
files: LintFileResult[];
|
|
137
|
+
/** Total issues by severity */
|
|
138
|
+
summary: {
|
|
139
|
+
totalFiles: number;
|
|
140
|
+
totalTables: number;
|
|
141
|
+
totalColumns: number;
|
|
142
|
+
errors: number;
|
|
143
|
+
warnings: number;
|
|
144
|
+
};
|
|
145
|
+
/** Duration in milliseconds */
|
|
146
|
+
durationMs: number;
|
|
147
|
+
/** Whether linting passed (no errors) */
|
|
148
|
+
passed: boolean;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Output format for reporters
|
|
152
|
+
*/
|
|
153
|
+
type ReporterFormat = 'console' | 'json' | 'github';
|
|
154
|
+
/**
|
|
155
|
+
* Reporter options
|
|
156
|
+
*/
|
|
157
|
+
interface ReporterOptions {
|
|
158
|
+
format: ReporterFormat;
|
|
159
|
+
/** Show only issues (no summary) */
|
|
160
|
+
quiet?: boolean;
|
|
161
|
+
/** Show detailed output */
|
|
162
|
+
verbose?: boolean;
|
|
163
|
+
/** Use colors in output */
|
|
164
|
+
colors?: boolean;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Rule context passed to rule implementations
|
|
168
|
+
*/
|
|
169
|
+
interface RuleContext {
|
|
170
|
+
/** Report an issue */
|
|
171
|
+
report: (issue: Omit<LintIssue, 'rule' | 'severity'>) => void;
|
|
172
|
+
/** Current severity for this rule */
|
|
173
|
+
severity: 'warn' | 'error';
|
|
174
|
+
/** Rule options (if any) */
|
|
175
|
+
options: Record<string, unknown>;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Rule implementation interface
|
|
179
|
+
*/
|
|
180
|
+
interface LintRule {
|
|
181
|
+
/** Rule name */
|
|
182
|
+
name: string;
|
|
183
|
+
/** Rule description */
|
|
184
|
+
description: string;
|
|
185
|
+
/** Default severity */
|
|
186
|
+
defaultSeverity: LintSeverity;
|
|
187
|
+
/** Validate a table */
|
|
188
|
+
validateTable?: (table: SchemaTable, context: RuleContext) => void;
|
|
189
|
+
/** Validate a column */
|
|
190
|
+
validateColumn?: (column: SchemaColumn, table: SchemaTable, context: RuleContext) => void;
|
|
191
|
+
/** Validate the entire schema */
|
|
192
|
+
validateSchema?: (tables: SchemaTable[], context: RuleContext) => void;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Schema file detection result
|
|
196
|
+
*/
|
|
197
|
+
interface SchemaFileInfo {
|
|
198
|
+
filePath: string;
|
|
199
|
+
type: 'tenant' | 'shared';
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Linter options
|
|
203
|
+
*/
|
|
204
|
+
interface LinterOptions {
|
|
205
|
+
/** Schema directories to lint */
|
|
206
|
+
schemaDirs?: {
|
|
207
|
+
tenant?: string;
|
|
208
|
+
shared?: string;
|
|
209
|
+
};
|
|
210
|
+
/** Glob patterns for schema files */
|
|
211
|
+
include?: string[];
|
|
212
|
+
/** Glob patterns to exclude */
|
|
213
|
+
exclude?: string[];
|
|
214
|
+
/** Rules configuration */
|
|
215
|
+
rules?: LintRules;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Schema Parser
|
|
220
|
+
*
|
|
221
|
+
* Parses Drizzle ORM schema files to extract table, column, and index information
|
|
222
|
+
* for linting purposes.
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Parse a Drizzle schema module to extract table information
|
|
227
|
+
*/
|
|
228
|
+
declare function parseSchemaModule(module: Record<string, unknown>, filePath: string, schemaType: 'tenant' | 'shared'): SchemaTable[];
|
|
229
|
+
/**
|
|
230
|
+
* Parse a raw table definition (for testing or direct input)
|
|
231
|
+
*/
|
|
232
|
+
declare function parseRawTable(name: string, columns: Array<{
|
|
233
|
+
name: string;
|
|
234
|
+
dataType: string;
|
|
235
|
+
isPrimaryKey?: boolean;
|
|
236
|
+
isNullable?: boolean;
|
|
237
|
+
hasDefault?: boolean;
|
|
238
|
+
defaultValue?: string | null;
|
|
239
|
+
references?: SchemaColumn['references'];
|
|
240
|
+
}>, options?: {
|
|
241
|
+
schemaType?: 'tenant' | 'shared';
|
|
242
|
+
filePath?: string;
|
|
243
|
+
indexes?: Array<{
|
|
244
|
+
name: string;
|
|
245
|
+
columns: string[];
|
|
246
|
+
isUnique?: boolean;
|
|
247
|
+
}>;
|
|
248
|
+
}): SchemaTable;
|
|
249
|
+
/**
|
|
250
|
+
* Find schema files in a directory
|
|
251
|
+
*/
|
|
252
|
+
declare function findSchemaFiles(dir: string, type: 'tenant' | 'shared'): Promise<SchemaFileInfo[]>;
|
|
253
|
+
/**
|
|
254
|
+
* Load and parse a schema file
|
|
255
|
+
*/
|
|
256
|
+
declare function loadSchemaFile(filePath: string, type: 'tenant' | 'shared'): Promise<SchemaTable[]>;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Schema Linter
|
|
260
|
+
*
|
|
261
|
+
* Main linter class that orchestrates schema validation using configured rules.
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Schema Linter
|
|
266
|
+
*
|
|
267
|
+
* Validates Drizzle ORM schemas against configurable rules.
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* const linter = new SchemaLinter({
|
|
272
|
+
* rules: {
|
|
273
|
+
* 'table-naming': ['error', { style: 'snake_case' }],
|
|
274
|
+
* 'require-primary-key': 'error',
|
|
275
|
+
* 'prefer-uuid-pk': 'warn',
|
|
276
|
+
* },
|
|
277
|
+
* });
|
|
278
|
+
*
|
|
279
|
+
* const result = await linter.lintDirectory('./src/db/schema');
|
|
280
|
+
* console.log(result.passed ? 'All good!' : 'Issues found');
|
|
281
|
+
* ```
|
|
282
|
+
*/
|
|
283
|
+
declare class SchemaLinter {
|
|
284
|
+
private rules;
|
|
285
|
+
private config;
|
|
286
|
+
constructor(options?: Partial<LinterOptions>);
|
|
287
|
+
/**
|
|
288
|
+
* Get the effective severity for a rule
|
|
289
|
+
*/
|
|
290
|
+
getEffectiveSeverity(ruleName: string): LintSeverity;
|
|
291
|
+
/**
|
|
292
|
+
* Get the options for a rule
|
|
293
|
+
*/
|
|
294
|
+
getRuleOptions(ruleName: string): Record<string, unknown>;
|
|
295
|
+
/**
|
|
296
|
+
* Check if a rule is enabled
|
|
297
|
+
*/
|
|
298
|
+
isRuleEnabled(ruleName: string): boolean;
|
|
299
|
+
/**
|
|
300
|
+
* Lint a single table
|
|
301
|
+
*/
|
|
302
|
+
lintTable(table: SchemaTable): LintIssue[];
|
|
303
|
+
/**
|
|
304
|
+
* Lint multiple tables
|
|
305
|
+
*/
|
|
306
|
+
lintTables(tables: SchemaTable[]): LintResult;
|
|
307
|
+
/**
|
|
308
|
+
* Lint a schema module (object with table exports)
|
|
309
|
+
*/
|
|
310
|
+
lintModule(module: Record<string, unknown>, filePath: string, schemaType?: 'tenant' | 'shared'): LintResult;
|
|
311
|
+
/**
|
|
312
|
+
* Lint schema files in a directory
|
|
313
|
+
*/
|
|
314
|
+
lintDirectory(dir: string, type?: 'tenant' | 'shared'): Promise<LintResult>;
|
|
315
|
+
/**
|
|
316
|
+
* Lint multiple directories (tenant and shared)
|
|
317
|
+
*/
|
|
318
|
+
lintDirectories(options: {
|
|
319
|
+
tenant?: string;
|
|
320
|
+
shared?: string;
|
|
321
|
+
}): Promise<LintResult>;
|
|
322
|
+
/**
|
|
323
|
+
* Get all registered rules
|
|
324
|
+
*/
|
|
325
|
+
getRules(): LintRule[];
|
|
326
|
+
/**
|
|
327
|
+
* Get a specific rule by name
|
|
328
|
+
*/
|
|
329
|
+
getRule(name: string): LintRule | undefined;
|
|
330
|
+
/**
|
|
331
|
+
* Register a custom rule
|
|
332
|
+
*/
|
|
333
|
+
registerRule(rule: LintRule): void;
|
|
334
|
+
/**
|
|
335
|
+
* Update rule configuration
|
|
336
|
+
*/
|
|
337
|
+
setRuleConfig(ruleName: string, config: RuleConfig): void;
|
|
338
|
+
/**
|
|
339
|
+
* Get current configuration
|
|
340
|
+
*/
|
|
341
|
+
getConfig(): LintRules;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Create a linter instance with configuration
|
|
345
|
+
*/
|
|
346
|
+
declare function createLinter(options?: Partial<LinterOptions>): SchemaLinter;
|
|
347
|
+
/**
|
|
348
|
+
* Quick lint function for simple use cases
|
|
349
|
+
*/
|
|
350
|
+
declare function lintSchemas(options: {
|
|
351
|
+
tenant?: string;
|
|
352
|
+
shared?: string;
|
|
353
|
+
rules?: LintRules;
|
|
354
|
+
}): Promise<LintResult>;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Lint Reporter
|
|
358
|
+
*
|
|
359
|
+
* Formats and outputs lint results in various formats.
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Format lint results according to the specified format
|
|
364
|
+
*/
|
|
365
|
+
declare function formatLintResult(result: LintResult, options: ReporterOptions): string;
|
|
366
|
+
/**
|
|
367
|
+
* Create a reporter function for the specified format
|
|
368
|
+
*/
|
|
369
|
+
declare function createReporter(format: ReporterFormat, options?: Partial<ReporterOptions>): (result: LintResult) => string;
|
|
370
|
+
/**
|
|
371
|
+
* Print lint result to stdout
|
|
372
|
+
*/
|
|
373
|
+
declare function printLintResult(result: LintResult, options: ReporterOptions): void;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Naming Convention Rules
|
|
377
|
+
*
|
|
378
|
+
* Rules for validating table and column naming conventions.
|
|
379
|
+
*/
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Table naming rule
|
|
383
|
+
*
|
|
384
|
+
* Validates that table names follow the configured naming convention.
|
|
385
|
+
*/
|
|
386
|
+
declare const tableNamingRule: LintRule;
|
|
387
|
+
/**
|
|
388
|
+
* Column naming rule
|
|
389
|
+
*
|
|
390
|
+
* Validates that column names follow the configured naming convention.
|
|
391
|
+
*/
|
|
392
|
+
declare const columnNamingRule: LintRule;
|
|
393
|
+
/**
|
|
394
|
+
* All naming rules
|
|
395
|
+
*/
|
|
396
|
+
declare const namingRules: LintRule[];
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Convention Rules
|
|
400
|
+
*
|
|
401
|
+
* Rules for validating schema best practices and conventions.
|
|
402
|
+
*/
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Require primary key rule
|
|
406
|
+
*
|
|
407
|
+
* Every table should have a primary key defined.
|
|
408
|
+
*/
|
|
409
|
+
declare const requirePrimaryKeyRule: LintRule;
|
|
410
|
+
/**
|
|
411
|
+
* Prefer UUID primary key rule
|
|
412
|
+
*
|
|
413
|
+
* Recommends using UUID over serial/integer for primary keys.
|
|
414
|
+
*/
|
|
415
|
+
declare const preferUuidPkRule: LintRule;
|
|
416
|
+
/**
|
|
417
|
+
* Require timestamps rule
|
|
418
|
+
*
|
|
419
|
+
* Tables should have created_at and updated_at columns.
|
|
420
|
+
*/
|
|
421
|
+
declare const requireTimestampsRule: LintRule;
|
|
422
|
+
/**
|
|
423
|
+
* Index foreign keys rule
|
|
424
|
+
*
|
|
425
|
+
* Foreign key columns should have indexes for query performance.
|
|
426
|
+
*/
|
|
427
|
+
declare const indexForeignKeysRule: LintRule;
|
|
428
|
+
/**
|
|
429
|
+
* All convention rules
|
|
430
|
+
*/
|
|
431
|
+
declare const conventionRules: LintRule[];
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Security Rules
|
|
435
|
+
*
|
|
436
|
+
* Rules for validating schema security best practices.
|
|
437
|
+
*/
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* No cascade delete rule
|
|
441
|
+
*
|
|
442
|
+
* Warns about CASCADE DELETE which can accidentally remove data.
|
|
443
|
+
*/
|
|
444
|
+
declare const noCascadeDeleteRule: LintRule;
|
|
445
|
+
/**
|
|
446
|
+
* Require soft delete rule
|
|
447
|
+
*
|
|
448
|
+
* Tables should have a soft delete column instead of hard deletes.
|
|
449
|
+
*/
|
|
450
|
+
declare const requireSoftDeleteRule: LintRule;
|
|
451
|
+
/**
|
|
452
|
+
* All security rules
|
|
453
|
+
*/
|
|
454
|
+
declare const securityRules: LintRule[];
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Lint Rules Export
|
|
458
|
+
*
|
|
459
|
+
* Aggregates all available lint rules.
|
|
460
|
+
*/
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* All built-in rules
|
|
464
|
+
*/
|
|
465
|
+
declare const allRules: LintRule[];
|
|
466
|
+
/**
|
|
467
|
+
* Get rule by name
|
|
468
|
+
*/
|
|
469
|
+
declare function getRuleByName(name: string): LintRule | undefined;
|
|
470
|
+
/**
|
|
471
|
+
* Get all rule names
|
|
472
|
+
*/
|
|
473
|
+
declare function getAllRuleNames(): string[];
|
|
474
|
+
|
|
475
|
+
export { DEFAULT_LINT_RULES, type LintConfig, type LintFileResult, type LintIssue, type LintResult, type LintRule, type LintRules, type LintSeverity, type LinterOptions, type NamingRuleOptions, type NamingStyle, type ParsedSchema, type ReporterFormat, type ReporterOptions, type RuleConfig, type RuleContext, type SchemaColumn, type SchemaFileInfo, type SchemaIndex, SchemaLinter, type SchemaTable, allRules, columnNamingRule, conventionRules, createLinter, createReporter, findSchemaFiles, formatLintResult, getAllRuleNames, getRuleByName, indexForeignKeysRule, lintSchemas, loadSchemaFile, namingRules, noCascadeDeleteRule, parseRawTable, parseSchemaModule, preferUuidPkRule, printLintResult, requirePrimaryKeyRule, requireSoftDeleteRule, requireTimestampsRule, securityRules, tableNamingRule };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
var L={"table-naming":["warn",{style:"snake_case"}],"column-naming":["warn",{style:"snake_case"}],"require-primary-key":"error","prefer-uuid-pk":"warn","require-timestamps":"off","index-foreign-keys":"warn","no-cascade-delete":"off","require-soft-delete":"off"};function A(e){return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(e)}function j(e){return /^[a-z][a-zA-Z0-9]*$/.test(e)&&!e.includes("_")}function z(e){return /^[A-Z][a-zA-Z0-9]*$/.test(e)&&!e.includes("_")}function K(e){return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(e)}function I(e,t){switch(t){case "snake_case":return A(e);case "camelCase":return j(e);case "PascalCase":return z(e);case "kebab-case":return K(e);default:return true}}function O(e,t){let n=e.replace(/([a-z])([A-Z])/g,"$1_$2").replace(/-/g,"_").toLowerCase().split("_").filter(Boolean);switch(t){case "snake_case":return n.join("_");case "camelCase":return n.map((s,i)=>i===0?s:s.charAt(0).toUpperCase()+s.slice(1)).join("");case "PascalCase":return n.map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("");case "kebab-case":return n.join("-");default:return e}}function q(e,t){return t?.length?t.some(n=>new RegExp("^"+n.replace(/\*/g,".*")+"$").test(e)):false}var w={name:"table-naming",description:"Enforce consistent table naming convention",defaultSeverity:"warn",validateTable(e,t){let n=t.options,s=n.style??"snake_case",i=n.exceptions??[];if(!q(e.name,i)&&!I(e.name,s)){let r=O(e.name,s);t.report({message:`Table "${e.name}" does not follow ${s} convention`,filePath:e.filePath,table:e.name,suggestion:`Consider renaming to "${r}"`});}}},v={name:"column-naming",description:"Enforce consistent column naming convention",defaultSeverity:"warn",validateColumn(e,t,n){let s=n.options,i=s.style??"snake_case",r=s.exceptions??[];if(!q(e.name,r)&&!I(e.name,i)){let a=O(e.name,i);n.report({message:`Column "${e.name}" in table "${t.name}" does not follow ${i} convention`,filePath:t.filePath,table:t.name,column:e.name,suggestion:`Consider renaming to "${a}"`});}}},R=[w,v];var T={name:"require-primary-key",description:"Require every table to have a primary key",defaultSeverity:"error",validateTable(e,t){e.columns.some(s=>s.isPrimaryKey)||t.report({message:`Table "${e.name}" does not have a primary key`,filePath:e.filePath,table:e.name,suggestion:"Add a primary key column (e.g., id: uuid().primaryKey())"});}},P={name:"prefer-uuid-pk",description:"Prefer UUID over serial/integer for primary keys",defaultSeverity:"warn",validateColumn(e,t,n){if(!e.isPrimaryKey)return;let s=["serial","bigserial","smallserial","integer","bigint","smallint"],i=e.dataType.toLowerCase();s.some(r=>i.includes(r))&&n.report({message:`Table "${t.name}" uses ${e.dataType} for primary key instead of UUID`,filePath:t.filePath,table:t.name,column:e.name,suggestion:"Consider using uuid() for better distribution and security"});}},k={name:"require-timestamps",description:"Require timestamp columns (created_at, updated_at) on tables",defaultSeverity:"warn",validateTable(e,t){let s=t.options.columns??["created_at","updated_at"],i=e.columns.map(r=>r.name.toLowerCase());for(let r of s){let a=r.toLowerCase(),o=r.replace(/_([a-z])/g,(l,u)=>u.toUpperCase()).toLowerCase();!i.includes(a)&&!i.includes(o)&&t.report({message:`Table "${e.name}" is missing "${r}" column`,filePath:e.filePath,table:e.name,suggestion:`Add ${a}: timestamp('${a}').defaultNow().notNull()`});}}},$={name:"index-foreign-keys",description:"Require indexes on foreign key columns",defaultSeverity:"warn",validateTable(e,t){let n=e.columns.filter(r=>r.references),s=new Set;for(let r of e.indexes){let a=r.columns[0];a&&s.add(a.toLowerCase());}let i=e.columns.filter(r=>r.isPrimaryKey);for(let r of i)s.add(r.name.toLowerCase());for(let r of n)s.has(r.name.toLowerCase())||t.report({message:`Foreign key column "${r.name}" in table "${e.name}" is not indexed`,filePath:e.filePath,table:e.name,column:r.name,suggestion:`Add index: index('${e.name}_${r.name}_idx').on(${e.name}.${r.name})`});}},b=[T,P,k,$];var N={name:"no-cascade-delete",description:"Warn about CASCADE DELETE on foreign keys",defaultSeverity:"warn",validateColumn(e,t,n){if(!e.references)return;e.references.onDelete?.toLowerCase()==="cascade"&&n.report({message:`Foreign key "${e.name}" in table "${t.name}" uses CASCADE DELETE`,filePath:t.filePath,table:t.name,column:e.name,suggestion:"Consider using SET NULL, RESTRICT, or implementing soft delete to prevent accidental data loss"});}},D={name:"require-soft-delete",description:"Require soft delete column on tables",defaultSeverity:"off",validateTable(e,t){let s=t.options.column??"deleted_at",i=s.toLowerCase(),r=s.replace(/_([a-z])/g,(o,l)=>l.toUpperCase()).toLowerCase(),a=e.columns.map(o=>o.name.toLowerCase());!a.includes(i)&&!a.includes(r)&&t.report({message:`Table "${e.name}" does not have a soft delete column ("${s}")`,filePath:e.filePath,table:e.name,suggestion:`Add ${i}: timestamp('${i}') for soft delete support`});}},x=[N,D];var g=[...R,...b,...x];function M(e){return g.find(t=>t.name===e)}function Z(){return g.map(e=>e.name)}var V=Symbol.for("drizzle:Table"),B=Symbol.for("drizzle:Column");function W(e){if(!e||typeof e!="object")return false;let t=e;if(t._&&typeof t._=="object"){let n=t._;return typeof n.name=="string"&&(n.schema===void 0||typeof n.schema=="string")}return !!Object.getOwnPropertySymbols(t).some(n=>n===V)}function J(e){if(!e||typeof e!="object")return false;let t=e;return typeof t.name=="string"||typeof t.columnType=="string"||typeof t.dataType=="string"||Object.getOwnPropertySymbols(t).some(n=>n===B)}function Q(e,t){let n=t.name??e,s="unknown";if(typeof t.dataType=="string")s=t.dataType;else if(typeof t.columnType=="string")s=t.columnType;else if(typeof t.getSQLType=="function")try{s=t.getSQLType();}catch{}let i=t.primary===true||t.isPrimaryKey===true||typeof t.primaryKey=="function"&&t._isPrimaryKey===true,r=t.notNull!==true&&t.isNotNull!==true,a=t.hasDefault===true||t.default!==void 0,o=t.default!==void 0?String(t.default):null,l;if(t.references&&typeof t.references=="object"){let u=t.references;l={table:u.table??"unknown",column:u.column??"unknown",onDelete:u.onDelete,onUpdate:u.onUpdate};}return {name:n,dataType:s,isPrimaryKey:i,isNullable:r,hasDefault:a,defaultValue:o,references:l}}function Y(e){let t=[],n=e._indexes??e.indexes;if(n&&typeof n=="object")for(let[s,i]of Object.entries(n)){if(!i||typeof i!="object")continue;let r=i,a=[];if(Array.isArray(r.columns))for(let o of r.columns)typeof o=="string"?a.push(o):o&&typeof o=="object"&&"name"in o&&a.push(String(o.name));t.push({name:s,columns:a,isUnique:r.isUnique===true||r.unique===true});}return t}function S(e,t,n){let s=[];for(let[i,r]of Object.entries(e)){if(!W(r))continue;let a=r,l=a._?.name??i,u=[];for(let[m,d]of Object.entries(a)){if(m==="_"||m.startsWith("_")||!J(d))continue;let f=Q(m,d);u.push(f);}let p=Y(a);s.push({name:l,schemaType:n,columns:u,indexes:p,filePath:t});}return s}function F(e,t,n){return {name:e,schemaType:n?.schemaType??"tenant",filePath:n?.filePath??"unknown",columns:t.map(s=>({name:s.name,dataType:s.dataType,isPrimaryKey:s.isPrimaryKey??false,isNullable:s.isNullable??true,hasDefault:s.hasDefault??false,defaultValue:s.defaultValue??null,references:s.references})),indexes:n?.indexes?.map(s=>({name:s.name,columns:s.columns,isUnique:s.isUnique??false}))??[]}}async function y(e,t){let{glob:n}=await import('glob'),{resolve:s}=await import('path'),i=s(e,"**/*.ts");return (await n(i,{ignore:["**/*.test.ts","**/*.spec.ts","**/node_modules/**"]})).map(a=>({filePath:a,type:t}))}async function h(e,t){try{let n=await import(e);return S(n,e,t)}catch(n){return console.warn(`Warning: Could not parse schema file ${e}:`,n.message),[]}}function G(e){return e?typeof e=="string"?e:e[0]:"off"}function H(e){return e?typeof e=="string"?{}:e[1]??{}:{}}var C=class{rules=new Map;config;constructor(t){this.config={...L,...t?.rules};for(let n of g)this.rules.set(n.name,n);}getEffectiveSeverity(t){let n=this.config[t];return G(n)}getRuleOptions(t){let n=this.config[t];return H(n)}isRuleEnabled(t){return this.getEffectiveSeverity(t)!=="off"}lintTable(t){let n=[];for(let[s,i]of this.rules){let r=this.getEffectiveSeverity(s);if(r==="off")continue;let a=this.getRuleOptions(s),o={severity:r,options:a,report:l=>{n.push({rule:s,severity:r,...l});}};if(i.validateTable&&i.validateTable(t,o),i.validateColumn)for(let l of t.columns)i.validateColumn(l,t,o);}return n}lintTables(t){let n=Date.now(),s=new Map;for(let o of t){s.has(o.filePath)||s.set(o.filePath,{filePath:o.filePath,issues:[],tables:0,columns:0});let l=s.get(o.filePath);l.tables++,l.columns+=o.columns.length;let u=this.lintTable(o);l.issues.push(...u);}for(let[o,l]of this.rules){if(!l.validateSchema)continue;let u=this.getEffectiveSeverity(o);if(u==="off")continue;let p=this.getRuleOptions(o),m={severity:u,options:p,report:d=>{let f=d.filePath||"schema";s.has(f)||s.set(f,{filePath:f,issues:[],tables:0,columns:0}),s.get(f).issues.push({rule:o,severity:u,...d});}};l.validateSchema(t,m);}let i=Array.from(s.values()),r=i.flatMap(o=>o.issues),a={totalFiles:i.length,totalTables:i.reduce((o,l)=>o+l.tables,0),totalColumns:i.reduce((o,l)=>o+l.columns,0),errors:r.filter(o=>o.severity==="error").length,warnings:r.filter(o=>o.severity==="warn").length};return {files:i,summary:a,durationMs:Date.now()-n,passed:a.errors===0}}lintModule(t,n,s="tenant"){let i=S(t,n,s);return this.lintTables(i)}async lintDirectory(t,n="tenant"){let s=Date.now(),i=await y(t,n),r=[];for(let o of i){let l=await h(o.filePath,o.type);r.push(...l);}let a=this.lintTables(r);return a.durationMs=Date.now()-s,a}async lintDirectories(t){let n=Date.now(),s=[];if(t.tenant){let r=await y(t.tenant,"tenant");for(let a of r){let o=await h(a.filePath,a.type);s.push(...o);}}if(t.shared){let r=await y(t.shared,"shared");for(let a of r){let o=await h(a.filePath,a.type);s.push(...o);}}let i=this.lintTables(s);return i.durationMs=Date.now()-n,i}getRules(){return Array.from(this.rules.values())}getRule(t){return this.rules.get(t)}registerRule(t){this.rules.set(t.name,t);}setRuleConfig(t,n){this.config[t]=n;}getConfig(){return {...this.config}}};function E(e){return new C(e)}async function X(e){let t=E(e.rules?{rules:e.rules}:void 0),n={};return e.tenant&&(n.tenant=e.tenant),e.shared&&(n.shared=e.shared),t.lintDirectories(n)}var U={reset:"\x1B[0m",red:"\x1B[31m",yellow:"\x1B[33m",green:"\x1B[32m",cyan:"\x1B[36m",dim:"\x1B[2m",bold:"\x1B[1m"};function c(e,t,n){return n?`${U[t]}${e}${U.reset}`:e}function ee(e,t){let n=e.severity==="error"?c("error","red",t):c("warn","yellow",t),s=e.column?`${e.table}.${e.column}`:e.table??e.filePath,i=c(`(${e.rule})`,"dim",t),r=` ${n} ${e.message} ${i}`;return r+=`
|
|
2
|
+
${c(s,"dim",t)}`,e.suggestion&&(r+=`
|
|
3
|
+
${c("\u2192 "+e.suggestion,"cyan",t)}`),r}function te(e,t){let n=t.colors!==false,s=[];for(let i of e.files)if(!(i.issues.length===0&&!t.verbose)){if(s.push(""),s.push(c(i.filePath,"bold",n)),i.issues.length===0){s.push(c(" No issues found","green",n));continue}for(let r of i.issues)s.push(ee(r,n));}if(!t.quiet){s.push("");let{errors:i,warnings:r,totalTables:a}=e.summary;if(i===0&&r===0)s.push(c(`\u2713 ${a} tables validated, no issues found`,"green",n));else {let o=[];o.push(`${a} tables validated`),r>0&&o.push(c(`${r} warning${r>1?"s":""}`,"yellow",n)),i>0&&o.push(c(`${i} error${i>1?"s":""}`,"red",n)),s.push(o.join(", "));}s.push(c(`Completed in ${e.durationMs}ms`,"dim",n));}return s.join(`
|
|
4
|
+
`)}function ne(e){return JSON.stringify({passed:e.passed,summary:e.summary,files:e.files.map(t=>({filePath:t.filePath,tables:t.tables,columns:t.columns,issues:t.issues})),durationMs:e.durationMs},null,2)}function se(e){let t=[];for(let r of e.files)for(let a of r.issues){let o=a.severity==="error"?"error":"warning",l=`[${a.rule}]`,u=a.filePath,p=a.line??1,m=`::${o} file=${u},line=${p},title=${l}::${a.message}`;a.suggestion&&(m+=` (${a.suggestion})`),t.push(m);}let{errors:n,warnings:s,totalTables:i}=e.summary;return n>0||s>0?t.push(`::notice::Schema lint: ${i} tables, ${n} errors, ${s} warnings`):t.push(`::notice::Schema lint: ${i} tables validated, no issues`),t.join(`
|
|
5
|
+
`)}function _(e,t){switch(t.format){case "json":return ne(e);case "github":return se(e);default:return te(e,t)}}function re(e,t){let n={format:e,quiet:t?.quiet??false,verbose:t?.verbose??false,colors:t?.colors??true};return s=>_(s,n)}function ie(e,t){let n=_(e,t);console.log(n);}export{L as DEFAULT_LINT_RULES,C as SchemaLinter,g as allRules,v as columnNamingRule,b as conventionRules,E as createLinter,re as createReporter,y as findSchemaFiles,_ as formatLintResult,Z as getAllRuleNames,M as getRuleByName,$ as indexForeignKeysRule,X as lintSchemas,h as loadSchemaFile,R as namingRules,N as noCascadeDeleteRule,F as parseRawTable,S as parseSchemaModule,P as preferUuidPkRule,ie as printLintResult,T as requirePrimaryKeyRule,D as requireSoftDeleteRule,k as requireTimestampsRule,x as securityRules,w as tableNamingRule};
|