@synapsestudios/eslint-plugin-data-boundaries 1.2.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 +113 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/rules/no-cross-schema-slonik-access.d.ts +7 -0
- package/dist/rules/no-cross-schema-slonik-access.d.ts.map +1 -0
- package/dist/rules/no-cross-schema-slonik-access.js +193 -0
- package/dist/rules/no-cross-schema-slonik-access.js.map +1 -0
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
# @synapsestudios/eslint-plugin-data-boundaries
|
|
2
2
|
|
|
3
|
-
ESLint plugin to enforce data boundary policies in modular monoliths using Prisma ORM.
|
|
3
|
+
ESLint plugin to enforce data boundary policies in modular monoliths using Prisma ORM and slonik.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
| Rule | Description | Scope |
|
|
8
|
+
|------|-------------|-------|
|
|
9
|
+
| [`no-cross-file-model-references`](#no-cross-file-model-references) | Prevents Prisma models from referencing models defined in other schema files | Prisma schema files |
|
|
10
|
+
| [`no-cross-domain-prisma-access`](#no-cross-domain-prisma-access) | Prevents modules from accessing Prisma models outside their domain boundaries | TypeScript/JavaScript |
|
|
11
|
+
| [`no-cross-schema-slonik-access`](#no-cross-schema-slonik-access) | Prevents modules from accessing database tables outside their schema boundaries via slonik | TypeScript/JavaScript |
|
|
4
12
|
|
|
5
13
|
## Overview
|
|
6
14
|
|
|
7
|
-
When building modular monoliths, maintaining clear boundaries between domains is crucial for long-term maintainability. ORMs like Prisma make it easy to accidentally create tight coupling at the data layer by allowing modules to access
|
|
15
|
+
When building modular monoliths, maintaining clear boundaries between domains is crucial for long-term maintainability. ORMs like Prisma and query builders like slonik make it easy to accidentally create tight coupling at the data layer by allowing modules to access data that belongs to other domains.
|
|
8
16
|
|
|
9
|
-
This ESLint plugin provides
|
|
17
|
+
This ESLint plugin provides three complementary rules to prevent such violations:
|
|
10
18
|
|
|
11
19
|
1. **Schema-level enforcement**: Prevents Prisma schema files from referencing models defined in other schema files
|
|
12
|
-
2. **Application-level enforcement**: Prevents TypeScript code from accessing Prisma models outside their domain boundaries
|
|
20
|
+
2. **Application-level enforcement**: Prevents TypeScript code from accessing Prisma models outside their domain boundaries
|
|
21
|
+
3. **SQL-level enforcement**: Prevents slonik SQL queries from accessing tables outside the module's schema
|
|
13
22
|
|
|
14
23
|
## Installation
|
|
15
24
|
|
|
@@ -82,6 +91,70 @@ class AuthService {
|
|
|
82
91
|
}
|
|
83
92
|
```
|
|
84
93
|
|
|
94
|
+
### `no-cross-schema-slonik-access`
|
|
95
|
+
|
|
96
|
+
Prevents TypeScript/JavaScript modules from accessing database tables outside their schema boundaries when using slonik. This rule enforces that all table references must be explicitly qualified with the module's schema name and prevents cross-schema access.
|
|
97
|
+
|
|
98
|
+
**Examples of violations:**
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// In /modules/auth/service.ts
|
|
102
|
+
import { sql } from 'slonik';
|
|
103
|
+
|
|
104
|
+
class AuthService {
|
|
105
|
+
async getUser(id: string) {
|
|
106
|
+
// ❌ Error: Module 'auth' must use fully qualified table names. Use 'auth.users' instead of 'users'.
|
|
107
|
+
return await this.pool.query(sql`
|
|
108
|
+
SELECT * FROM users WHERE id = ${id}
|
|
109
|
+
`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getUserOrganizations(userId: string) {
|
|
113
|
+
// ❌ Error: Module 'auth' cannot access table 'memberships' in schema 'organization'.
|
|
114
|
+
return await this.pool.query(sql`
|
|
115
|
+
SELECT * FROM organization.memberships WHERE user_id = ${userId}
|
|
116
|
+
`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Valid usage:**
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// In /modules/auth/service.ts
|
|
125
|
+
import { sql } from 'slonik';
|
|
126
|
+
|
|
127
|
+
class AuthService {
|
|
128
|
+
async getUser(id: string) {
|
|
129
|
+
// ✅ Valid: Fully qualified table name within module's schema
|
|
130
|
+
return await this.pool.query(sql`
|
|
131
|
+
SELECT * FROM auth.users WHERE id = ${id}
|
|
132
|
+
`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async getUserSessions(userId: string) {
|
|
136
|
+
// ✅ Valid: Both tables are explicitly qualified with auth schema
|
|
137
|
+
return await this.pool.query(sql`
|
|
138
|
+
SELECT s.* FROM auth.sessions s
|
|
139
|
+
JOIN auth.users u ON s.user_id = u.id
|
|
140
|
+
WHERE u.id = ${userId}
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Configuration:**
|
|
147
|
+
|
|
148
|
+
The rule supports the same `modulePath` configuration as other rules:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
{
|
|
152
|
+
'@synapsestudios/data-boundaries/no-cross-schema-slonik-access': ['error', {
|
|
153
|
+
modulePath: '/modules/' // Default - change to '/src/' for NestJS projects
|
|
154
|
+
}]
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
85
158
|
## Configuration
|
|
86
159
|
|
|
87
160
|
### Basic Setup (Legacy Config)
|
|
@@ -103,7 +176,7 @@ module.exports = {
|
|
|
103
176
|
// For Prisma schema files - uses our custom parser
|
|
104
177
|
{
|
|
105
178
|
files: ['**/*.prisma'],
|
|
106
|
-
parser: '@synapsestudios/data-boundaries/
|
|
179
|
+
parser: '@synapsestudios/eslint-plugin-data-boundaries/parsers/prisma',
|
|
107
180
|
rules: {
|
|
108
181
|
'@synapsestudios/data-boundaries/no-cross-file-model-references': 'error'
|
|
109
182
|
}
|
|
@@ -115,6 +188,9 @@ module.exports = {
|
|
|
115
188
|
'@synapsestudios/data-boundaries/no-cross-domain-prisma-access': ['error', {
|
|
116
189
|
schemaDir: 'prisma/schema',
|
|
117
190
|
modulePath: '/modules/' // Default - change to '/src/' for NestJS projects
|
|
191
|
+
}],
|
|
192
|
+
'@synapsestudios/data-boundaries/no-cross-schema-slonik-access': ['error', {
|
|
193
|
+
modulePath: '/modules/' // Default - change to '/src/' for NestJS projects
|
|
118
194
|
}]
|
|
119
195
|
}
|
|
120
196
|
}
|
|
@@ -128,7 +204,7 @@ For projects using ESLint's flat config (ESM), add to your `eslint.config.mjs`:
|
|
|
128
204
|
|
|
129
205
|
```javascript
|
|
130
206
|
import eslintPluginDataBoundaries from '@synapsestudios/eslint-plugin-data-boundaries';
|
|
131
|
-
import prismaParser from '@synapsestudios/eslint-plugin-data-boundaries/
|
|
207
|
+
import prismaParser from '@synapsestudios/eslint-plugin-data-boundaries/parsers/prisma';
|
|
132
208
|
|
|
133
209
|
export default [
|
|
134
210
|
// 1. Global ignores first
|
|
@@ -167,6 +243,12 @@ export default [
|
|
|
167
243
|
modulePath: '/src/' // Use '/src/' for NestJS, '/modules/' for other structures
|
|
168
244
|
}
|
|
169
245
|
],
|
|
246
|
+
'@synapsestudios/data-boundaries/no-cross-schema-slonik-access': [
|
|
247
|
+
'error',
|
|
248
|
+
{
|
|
249
|
+
modulePath: '/src/' // Use '/src/' for NestJS, '/modules/' for other structures
|
|
250
|
+
}
|
|
251
|
+
],
|
|
170
252
|
},
|
|
171
253
|
},
|
|
172
254
|
];
|
|
@@ -176,7 +258,7 @@ export default [
|
|
|
176
258
|
|
|
177
259
|
1. **Parser isolation is critical** - Prisma config must be completely separate from TypeScript config
|
|
178
260
|
2. **Configuration order matters** - Place Prisma config before TypeScript config
|
|
179
|
-
3. **ESM imports
|
|
261
|
+
3. **ESM imports** - The parser can be imported from the cleaner export path
|
|
180
262
|
4. **Global ignores + overrides** - Use global ignore for `.prisma` then override in Prisma-specific config
|
|
181
263
|
|
|
182
264
|
**⚠️ Important Configuration Note**:
|
|
@@ -207,6 +289,18 @@ module.exports = {
|
|
|
207
289
|
}
|
|
208
290
|
```
|
|
209
291
|
|
|
292
|
+
#### `no-cross-schema-slonik-access`
|
|
293
|
+
|
|
294
|
+
- **`modulePath`** (string): Path pattern to match module directories. Default: `'/modules/'`. Use `'/src/'` for NestJS projects or other domain-based structures.
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
{
|
|
298
|
+
'@synapsestudios/data-boundaries/no-cross-schema-slonik-access': ['error', {
|
|
299
|
+
modulePath: '/src/' // For NestJS-style projects
|
|
300
|
+
}]
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
210
304
|
## Directory Structure
|
|
211
305
|
|
|
212
306
|
This plugin supports multiple project structures:
|
|
@@ -285,12 +379,22 @@ Model field 'user' references 'User' which is not defined in this file.
|
|
|
285
379
|
Cross-file model references are not allowed.
|
|
286
380
|
```
|
|
287
381
|
|
|
382
|
+
```
|
|
383
|
+
Module 'auth' must use fully qualified table names. Use 'auth.users' instead of 'users'.
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
Module 'auth' cannot access table 'memberships' in schema 'organization'.
|
|
388
|
+
SQL queries should only access tables within the module's own schema ('auth').
|
|
389
|
+
```
|
|
390
|
+
|
|
288
391
|
## Migration Strategy
|
|
289
392
|
|
|
290
393
|
1. **Start with schema boundaries**: Add the `no-cross-file-model-references` rule to prevent new violations in schema files
|
|
291
394
|
2. **Split your schema**: Gradually move models to domain-specific schema files
|
|
292
395
|
3. **Add application boundaries**: Enable `no-cross-domain-prisma-access` to prevent cross-domain access in application code
|
|
293
|
-
4. **
|
|
396
|
+
4. **Enforce SQL boundaries**: Enable `no-cross-schema-slonik-access` if using slonik to prevent cross-schema SQL queries
|
|
397
|
+
5. **Refactor violations**: Create shared services or move logic to appropriate domains
|
|
294
398
|
|
|
295
399
|
## Troubleshooting
|
|
296
400
|
|
|
@@ -312,7 +416,7 @@ module.exports = {
|
|
|
312
416
|
overrides: [
|
|
313
417
|
{
|
|
314
418
|
files: ['**/*.prisma'],
|
|
315
|
-
parser: '@synapsestudios/data-boundaries/
|
|
419
|
+
parser: '@synapsestudios/eslint-plugin-data-boundaries/parsers/prisma', // This handles .prisma files
|
|
316
420
|
rules: {
|
|
317
421
|
'@synapsestudios/data-boundaries/no-cross-file-model-references': 'error'
|
|
318
422
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,0BAA0B,KAAoD,CAAC;AACrF,QAAA,MAAM,yBAAyB,KAAmD,CAAC;AACnF,QAAA,MAAM,YAAY,KAAqC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,0BAA0B,KAAoD,CAAC;AACrF,QAAA,MAAM,yBAAyB,KAAmD,CAAC;AACnF,QAAA,MAAM,yBAAyB,KAAmD,CAAC;AACnF,QAAA,MAAM,YAAY,KAAqC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const noCrossFileModelReferences = require('./rules/no-cross-file-model-references');
|
|
3
3
|
const noCrossDomainPrismaAccess = require('./rules/no-cross-domain-prisma-access');
|
|
4
|
+
const noCrossSchemaSlonikAccess = require('./rules/no-cross-schema-slonik-access');
|
|
4
5
|
const prismaParser = require('./parsers/prisma-parser');
|
|
5
6
|
module.exports = {
|
|
6
7
|
rules: {
|
|
7
8
|
'no-cross-file-model-references': noCrossFileModelReferences,
|
|
8
9
|
'no-cross-domain-prisma-access': noCrossDomainPrismaAccess,
|
|
10
|
+
'no-cross-schema-slonik-access': noCrossSchemaSlonikAccess,
|
|
9
11
|
},
|
|
10
12
|
parsers: {
|
|
11
13
|
prisma: prismaParser,
|
|
@@ -16,7 +18,7 @@ module.exports = {
|
|
|
16
18
|
overrides: [
|
|
17
19
|
{
|
|
18
20
|
files: ['**/*.prisma'],
|
|
19
|
-
parser: '@synapsestudios/data-boundaries/
|
|
21
|
+
parser: '@synapsestudios/eslint-plugin-data-boundaries/parsers/prisma',
|
|
20
22
|
rules: {
|
|
21
23
|
'@synapsestudios/data-boundaries/no-cross-file-model-references': 'error',
|
|
22
24
|
},
|
|
@@ -25,6 +27,7 @@ module.exports = {
|
|
|
25
27
|
files: ['**/*.ts', '**/*.tsx'],
|
|
26
28
|
rules: {
|
|
27
29
|
'@synapsestudios/data-boundaries/no-cross-domain-prisma-access': 'error',
|
|
30
|
+
'@synapsestudios/data-boundaries/no-cross-schema-slonik-access': 'error',
|
|
28
31
|
},
|
|
29
32
|
},
|
|
30
33
|
],
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,MAAM,0BAA0B,GAAG,OAAO,CAAC,wCAAwC,CAAC,CAAC;AACrF,MAAM,yBAAyB,GAAG,OAAO,CAAC,uCAAuC,CAAC,CAAC;AACnF,MAAM,YAAY,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;AAExD,MAAM,CAAC,OAAO,GAAG;IACf,KAAK,EAAE;QACL,gCAAgC,EAAE,0BAA0B;QAC5D,+BAA+B,EAAE,yBAAyB;KAC3D;IACD,OAAO,EAAE;QACP,MAAM,EAAE,YAAY;KACrB;IACD,OAAO,EAAE;QACP,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,iCAAiC,CAAC;YAC5C,SAAS,EAAE;gBACT;oBACE,KAAK,EAAE,CAAC,aAAa,CAAC;oBACtB,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,MAAM,0BAA0B,GAAG,OAAO,CAAC,wCAAwC,CAAC,CAAC;AACrF,MAAM,yBAAyB,GAAG,OAAO,CAAC,uCAAuC,CAAC,CAAC;AACnF,MAAM,yBAAyB,GAAG,OAAO,CAAC,uCAAuC,CAAC,CAAC;AACnF,MAAM,YAAY,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;AAExD,MAAM,CAAC,OAAO,GAAG;IACf,KAAK,EAAE;QACL,gCAAgC,EAAE,0BAA0B;QAC5D,+BAA+B,EAAE,yBAAyB;QAC1D,+BAA+B,EAAE,yBAAyB;KAC3D;IACD,OAAO,EAAE;QACP,MAAM,EAAE,YAAY;KACrB;IACD,OAAO,EAAE;QACP,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,iCAAiC,CAAC;YAC5C,SAAS,EAAE;gBACT;oBACE,KAAK,EAAE,CAAC,aAAa,CAAC;oBACtB,MAAM,EAAE,8DAA8D;oBACtE,KAAK,EAAE;wBACL,gEAAgE,EAAE,OAAO;qBAC1E;iBACF;gBACD;oBACE,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;oBAC9B,KAAK,EAAE;wBACL,+DAA+D,EAAE,OAAO;wBACxE,+DAA+D,EAAE,OAAO;qBACzE;iBACF;aACF;SACF;KACF;CACF,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
interface RuleOptions {
|
|
3
|
+
modulePath: string;
|
|
4
|
+
}
|
|
5
|
+
declare const rule: ESLintUtils.RuleModule<"crossSchemaAccess" | "unqualifiedTable", [RuleOptions], unknown, ESLintUtils.RuleListener>;
|
|
6
|
+
export = rule;
|
|
7
|
+
//# sourceMappingURL=no-cross-schema-slonik-access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-cross-schema-slonik-access.d.ts","sourceRoot":"","sources":["../../src/rules/no-cross-schema-slonik-access.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAY,MAAM,0BAA0B,CAAC;AAGjE,UAAU,WAAW;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAmID,QAAA,MAAM,IAAI,oHA0FR,CAAC;AAEH,SAAS,IAAI,CAAC"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
3
|
+
const schema_parser_1 = require("../utils/schema-parser");
|
|
4
|
+
/**
|
|
5
|
+
* Extract table names from SQL string using simple regex patterns
|
|
6
|
+
*/
|
|
7
|
+
function extractTableNames(sql) {
|
|
8
|
+
const tableNames = [];
|
|
9
|
+
// Remove comments and normalize whitespace
|
|
10
|
+
const cleanSql = sql
|
|
11
|
+
.replace(/--.*$/gm, '') // Remove line comments
|
|
12
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
|
|
13
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
14
|
+
.trim();
|
|
15
|
+
// Patterns to match table references
|
|
16
|
+
const patterns = [
|
|
17
|
+
// FROM clause: FROM schema.table, FROM table
|
|
18
|
+
/FROM\s+(?:(\w+)\.)?(\w+)/gi,
|
|
19
|
+
// JOIN clause: JOIN schema.table, JOIN table
|
|
20
|
+
/JOIN\s+(?:(\w+)\.)?(\w+)/gi,
|
|
21
|
+
// INSERT INTO: INSERT INTO schema.table, INSERT INTO table
|
|
22
|
+
/INSERT\s+INTO\s+(?:(\w+)\.)?(\w+)/gi,
|
|
23
|
+
// UPDATE: UPDATE schema.table, UPDATE table
|
|
24
|
+
/UPDATE\s+(?:(\w+)\.)?(\w+)/gi,
|
|
25
|
+
// DELETE FROM: DELETE FROM schema.table, DELETE FROM table
|
|
26
|
+
/DELETE\s+FROM\s+(?:(\w+)\.)?(\w+)/gi,
|
|
27
|
+
];
|
|
28
|
+
patterns.forEach((pattern) => {
|
|
29
|
+
let match;
|
|
30
|
+
while ((match = pattern.exec(cleanSql)) !== null) {
|
|
31
|
+
const schema = match[1];
|
|
32
|
+
const table = match[2];
|
|
33
|
+
if (table && table.toLowerCase() !== 'select') {
|
|
34
|
+
// If schema is specified, use schema.table format
|
|
35
|
+
if (schema) {
|
|
36
|
+
tableNames.push(`${schema}.${table}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// If no schema specified, just use table name
|
|
40
|
+
tableNames.push(table);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return [...new Set(tableNames)]; // Remove duplicates
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if a table reference violates schema boundaries
|
|
49
|
+
*/
|
|
50
|
+
function isTableAccessViolation(tableName, currentModule) {
|
|
51
|
+
// Check if table name includes schema (schema.table format)
|
|
52
|
+
if (tableName.includes('.')) {
|
|
53
|
+
const [schema, table] = tableName.split('.', 2);
|
|
54
|
+
// If schema is explicitly specified and doesn't match module, it's a violation
|
|
55
|
+
if (schema && schema !== currentModule) {
|
|
56
|
+
return {
|
|
57
|
+
isViolation: true,
|
|
58
|
+
schema,
|
|
59
|
+
table,
|
|
60
|
+
reason: 'crossSchema',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// For unqualified table names, this is now a violation - require explicit schema
|
|
66
|
+
return {
|
|
67
|
+
isViolation: true,
|
|
68
|
+
table: tableName,
|
|
69
|
+
reason: 'unqualified',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { isViolation: false };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if node is a slonik sql tagged template literal
|
|
76
|
+
*/
|
|
77
|
+
function isSlonikSqlCall(node) {
|
|
78
|
+
if (node.tag.type === 'Identifier' && node.tag.name === 'sql') {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
// Handle member expressions like db.sql or this.sql
|
|
82
|
+
if (node.tag.type === 'MemberExpression' &&
|
|
83
|
+
node.tag.property.type === 'Identifier' &&
|
|
84
|
+
node.tag.property.name === 'sql') {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Extract SQL string from template literal
|
|
91
|
+
*/
|
|
92
|
+
function extractSqlFromTemplate(node) {
|
|
93
|
+
const quasi = node.quasi;
|
|
94
|
+
// Combine all template parts into a single string
|
|
95
|
+
let sql = '';
|
|
96
|
+
quasi.quasis.forEach((element, index) => {
|
|
97
|
+
sql += element.value.raw;
|
|
98
|
+
// Add placeholder for expressions (${...})
|
|
99
|
+
if (index < quasi.expressions.length) {
|
|
100
|
+
sql += ' ? '; // Use placeholder for parameter
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return sql;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* ESLint rule to prevent slonik SQL queries from accessing tables
|
|
107
|
+
* outside the current module's schema
|
|
108
|
+
*/
|
|
109
|
+
const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/synapsestudios/eslint-plugin-data-boundaries#${name}`);
|
|
110
|
+
const rule = createRule({
|
|
111
|
+
name: 'no-cross-schema-slonik-access',
|
|
112
|
+
meta: {
|
|
113
|
+
type: 'problem',
|
|
114
|
+
docs: {
|
|
115
|
+
description: 'Disallow slonik SQL queries from accessing tables in schemas outside the current module',
|
|
116
|
+
},
|
|
117
|
+
fixable: undefined,
|
|
118
|
+
schema: [
|
|
119
|
+
{
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
modulePath: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'Path pattern to match module directories (e.g., "/modules/", "/src/")',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
messages: {
|
|
131
|
+
crossSchemaAccess: "Module '{{currentModule}}' cannot access table '{{table}}' in schema '{{schema}}'. SQL queries should only access tables within the module's own schema ('{{currentModule}}').",
|
|
132
|
+
unqualifiedTable: "Module '{{currentModule}}' must use fully qualified table names. Use '{{currentModule}}.{{table}}' instead of '{{table}}'.",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
defaultOptions: [
|
|
136
|
+
{
|
|
137
|
+
modulePath: '/modules/',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
create(context, [options]) {
|
|
141
|
+
const filename = context.getFilename();
|
|
142
|
+
// Only process TypeScript files in modules
|
|
143
|
+
if (!filename.includes(options.modulePath) || !filename.match(/\.(ts|tsx)$/)) {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
// Extract current module from file path
|
|
147
|
+
const currentModule = (0, schema_parser_1.extractModuleFromPath)(filename, options.modulePath);
|
|
148
|
+
if (!currentModule) {
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
// Detect slonik sql tagged template literals
|
|
153
|
+
TaggedTemplateExpression(node) {
|
|
154
|
+
if (!isSlonikSqlCall(node)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Extract SQL from template literal
|
|
158
|
+
const sqlString = extractSqlFromTemplate(node);
|
|
159
|
+
// Extract table names from SQL
|
|
160
|
+
const tableNames = extractTableNames(sqlString);
|
|
161
|
+
// Check each table for schema boundary violations
|
|
162
|
+
tableNames.forEach((tableName) => {
|
|
163
|
+
const violation = isTableAccessViolation(tableName, currentModule);
|
|
164
|
+
if (violation.isViolation) {
|
|
165
|
+
if (violation.reason === 'crossSchema' && violation.schema && violation.table) {
|
|
166
|
+
context.report({
|
|
167
|
+
node,
|
|
168
|
+
messageId: 'crossSchemaAccess',
|
|
169
|
+
data: {
|
|
170
|
+
currentModule,
|
|
171
|
+
schema: violation.schema,
|
|
172
|
+
table: violation.table,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else if (violation.reason === 'unqualified' && violation.table) {
|
|
177
|
+
context.report({
|
|
178
|
+
node,
|
|
179
|
+
messageId: 'unqualifiedTable',
|
|
180
|
+
data: {
|
|
181
|
+
currentModule,
|
|
182
|
+
table: violation.table,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
module.exports = rule;
|
|
193
|
+
//# sourceMappingURL=no-cross-schema-slonik-access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-cross-schema-slonik-access.js","sourceRoot":"","sources":["../../src/rules/no-cross-schema-slonik-access.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synapsestudios/eslint-plugin-data-boundaries",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "ESLint plugin to enforce data boundary policies in modular monoliths",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./parsers/prisma": {
|
|
13
|
+
"types": "./dist/parsers/prisma-parser.d.ts",
|
|
14
|
+
"default": "./dist/parsers/prisma-parser.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"files": [
|
|
8
18
|
"dist",
|
|
9
19
|
"README.md",
|
|
@@ -50,6 +60,7 @@
|
|
|
50
60
|
"eslint": "^8.57.1",
|
|
51
61
|
"jest": "^29.7.0",
|
|
52
62
|
"prettier": "^3.6.2",
|
|
63
|
+
"slonik": "^47.3.2",
|
|
53
64
|
"ts-jest": "^29.4.0",
|
|
54
65
|
"typescript": "^5.8.3"
|
|
55
66
|
},
|