@synapsestudios/eslint-plugin-data-boundaries 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Synapse Studios
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # @synapsestudios/eslint-plugin-data-boundaries
2
+
3
+ ESLint plugin to enforce data boundary policies in modular monoliths using Prisma ORM.
4
+
5
+ ## Overview
6
+
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 models that belong to other domains.
8
+
9
+ This ESLint plugin provides two complementary rules to prevent such violations:
10
+
11
+ 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
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install --save-dev @synapsestudios/eslint-plugin-data-boundaries
18
+ ```
19
+
20
+ ## Rules
21
+
22
+ ### `no-cross-file-model-references`
23
+
24
+ Prevents Prisma models from referencing models defined in other schema files. This rule works with Prisma's multi-file schema feature to ensure each schema file is self-contained within its domain.
25
+
26
+ **Examples of violations:**
27
+
28
+ ```prisma
29
+ // membership.prisma
30
+ model UserOrganization {
31
+ userId String
32
+ user User @relation(...) // ❌ Error: User not defined in this file
33
+ }
34
+ ```
35
+
36
+ **Valid usage:**
37
+
38
+ ```prisma
39
+ // auth.prisma
40
+ model User {
41
+ id String @id
42
+ sessions Session[]
43
+ }
44
+
45
+ model Session {
46
+ id String @id
47
+ userId String
48
+ user User @relation(fields: [userId], references: [id]) // ✅ Valid: User is defined in same file
49
+ }
50
+ ```
51
+
52
+ ### `no-cross-domain-prisma-access`
53
+
54
+ Prevents TypeScript/JavaScript modules from accessing Prisma models that belong to other domains. This rule analyzes your application code and maps file paths to domains, then ensures modules only access models from their own domain (plus optionally shared models).
55
+
56
+ **Examples of violations:**
57
+
58
+ ```typescript
59
+ // In /modules/auth/service.ts
60
+ class AuthService {
61
+ async getOrganizations() {
62
+ return this.prisma.organization.findMany();
63
+ // ❌ Error: Module 'auth' cannot access 'Organization' model (belongs to 'organization' domain)
64
+ }
65
+ }
66
+ ```
67
+
68
+ **Valid usage:**
69
+
70
+ ```typescript
71
+ // In /modules/auth/service.ts
72
+ class AuthService {
73
+ async getUser(id: string) {
74
+ return this.prisma.user.findUnique({ where: { id } }); // ✅ Valid: User belongs to auth domain
75
+ }
76
+
77
+ async logAction(action: string) {
78
+ return this.prisma.auditLog.create({ data: { action } }); // ✅ Valid: AuditLog is a shared model
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Configuration
84
+
85
+ ### Basic Setup
86
+
87
+ Add the plugin to your `.eslintrc.js`:
88
+
89
+ ```javascript
90
+ module.exports = {
91
+ plugins: ['@synapsestudios/data-boundaries'],
92
+ overrides: [
93
+ // For Prisma schema files
94
+ {
95
+ files: ['**/*.prisma'],
96
+ parser: '@synapsestudios/data-boundaries/dist/parsers/prisma-parser',
97
+ rules: {
98
+ '@synapsestudios/data-boundaries/no-cross-file-model-references': 'error'
99
+ }
100
+ },
101
+ // For TypeScript application code
102
+ {
103
+ files: ['**/*.ts', '**/*.tsx'],
104
+ rules: {
105
+ '@synapsestudios/data-boundaries/no-cross-domain-prisma-access': ['error', {
106
+ schemaDir: 'prisma/schema',
107
+ allowSharedModels: true
108
+ }]
109
+ }
110
+ }
111
+ ]
112
+ };
113
+ ```
114
+
115
+ ### Using the Recommended Configuration
116
+
117
+ ```javascript
118
+ module.exports = {
119
+ extends: ['plugin:@synapsestudios/data-boundaries/recommended']
120
+ };
121
+ ```
122
+
123
+ ### Rule Options
124
+
125
+ #### `no-cross-domain-prisma-access`
126
+
127
+ - **`schemaDir`** (string): Directory containing Prisma schema files, relative to project root. Default: `'prisma/schema'`
128
+ - **`allowSharedModels`** (boolean): Whether to allow access to models in shared/main schema files. Default: `true`
129
+
130
+ ```javascript
131
+ {
132
+ '@synapsestudios/data-boundaries/no-cross-domain-prisma-access': ['error', {
133
+ schemaDir: 'database/schemas',
134
+ allowSharedModels: false
135
+ }]
136
+ }
137
+ ```
138
+
139
+ ## Directory Structure
140
+
141
+ This plugin assumes your project follows these conventions:
142
+
143
+ ### Module Structure
144
+ ```
145
+ src/
146
+ modules/
147
+ auth/ # auth domain
148
+ service.ts
149
+ controller.ts
150
+ organization/ # organization domain
151
+ service.ts
152
+ controller.ts
153
+ user-profile/ # user-profile domain
154
+ service.ts
155
+ ```
156
+
157
+ ### Schema Structure
158
+ ```
159
+ prisma/
160
+ schema/
161
+ auth.prisma # Contains User, Session models
162
+ organization.prisma # Contains Organization, Membership models
163
+ main.prisma # Contains shared models (AuditLog, Setting)
164
+ ```
165
+
166
+ ## Domain Mapping
167
+
168
+ The plugin automatically maps:
169
+
170
+ - **File paths to domains**: `/modules/auth/` → `auth` domain
171
+ - **Schema files to domains**: `auth.prisma` → `auth` domain
172
+ - **Special cases**: `main.prisma` and `schema.prisma` → `shared` domain
173
+
174
+ ## Use Cases
175
+
176
+ ### Modular Monoliths
177
+ Perfect for applications transitioning from monolith to microservices, ensuring clean domain boundaries while maintaining a single codebase.
178
+
179
+ ### Domain-Driven Design
180
+ Enforces DDD principles at the data layer, preventing cross-domain dependencies that can lead to tight coupling.
181
+
182
+ ### Team Boundaries
183
+ Helps large teams maintain clear ownership of domains and prevents accidental coupling between team-owned modules.
184
+
185
+ ### AI-Assisted Development
186
+ Particularly valuable when using AI coding tools, which can easily introduce unintended cross-domain dependencies.
187
+
188
+ ## Error Messages
189
+
190
+ The plugin provides clear, actionable error messages:
191
+
192
+ ```
193
+ Module 'auth' cannot access 'Organization' model (belongs to 'organization' domain).
194
+ Consider using a shared service or moving the logic to the appropriate domain.
195
+ ```
196
+
197
+ ```
198
+ Model field 'user' references 'User' which is not defined in this file.
199
+ Cross-file model references are not allowed.
200
+ ```
201
+
202
+ ## Migration Strategy
203
+
204
+ 1. **Start with schema boundaries**: Add the `no-cross-file-model-references` rule to prevent new violations in schema files
205
+ 2. **Split your schema**: Gradually move models to domain-specific schema files
206
+ 3. **Add application boundaries**: Enable `no-cross-domain-prisma-access` to prevent cross-domain access in application code
207
+ 4. **Refactor violations**: Create shared services or move logic to appropriate domains
208
+
209
+ ## Contributing
210
+
211
+ Issues and pull requests are welcome! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
212
+
213
+ ## License
214
+
215
+ MIT
216
+
217
+ ## Credits
218
+
219
+ Originally developed for internal use at Synapse Studios and opensourced for the community.
@@ -0,0 +1,4 @@
1
+ declare const noCrossFileModelReferences: any;
2
+ declare const noCrossDomainPrismaAccess: any;
3
+ declare const prismaParser: any;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ const noCrossFileModelReferences = require('./rules/no-cross-file-model-references');
3
+ const noCrossDomainPrismaAccess = require('./rules/no-cross-domain-prisma-access');
4
+ const prismaParser = require('./parsers/prisma-parser');
5
+ module.exports = {
6
+ rules: {
7
+ 'no-cross-file-model-references': noCrossFileModelReferences,
8
+ 'no-cross-domain-prisma-access': noCrossDomainPrismaAccess,
9
+ },
10
+ parsers: {
11
+ prisma: prismaParser,
12
+ },
13
+ configs: {
14
+ recommended: {
15
+ plugins: ['@synapsestudios/data-boundaries'],
16
+ overrides: [
17
+ {
18
+ files: ['**/*.prisma'],
19
+ parser: '@synapsestudios/data-boundaries/dist/parsers/prisma-parser',
20
+ rules: {
21
+ '@synapsestudios/data-boundaries/no-cross-file-model-references': 'error',
22
+ },
23
+ },
24
+ {
25
+ files: ['**/*.ts', '**/*.tsx'],
26
+ rules: {
27
+ '@synapsestudios/data-boundaries/no-cross-domain-prisma-access': 'error',
28
+ },
29
+ },
30
+ ],
31
+ },
32
+ },
33
+ };
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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,4DAA4D;oBACpE,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;qBACzE;iBACF;aACF;SACF;KACF;CACF,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { Schema } from '@mrleebo/prisma-ast';
2
+ import { AST } from 'eslint';
3
+ interface PrismaEslintAST extends AST.Program {
4
+ prismaAst: Schema;
5
+ }
6
+ interface ParseResult {
7
+ ast: PrismaEslintAST | AST.Program;
8
+ services: {
9
+ getPrismaAst?: () => Schema;
10
+ };
11
+ scopeManager: null;
12
+ visitorKeys: Record<string, never>;
13
+ }
14
+ /**
15
+ * Custom ESLint parser for Prisma schema files
16
+ * Converts Prisma AST to ESLint-compatible AST
17
+ */
18
+ export declare function parseForESLint(code: string, _options?: Record<string, unknown>): ParseResult;
19
+ export {};
20
+ //# sourceMappingURL=prisma-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prisma-parser.d.ts","sourceRoot":"","sources":["../../src/parsers/prisma-parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,MAAM,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAE7B,UAAU,eAAgB,SAAQ,GAAG,CAAC,OAAO;IAC3C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,WAAW;IACnB,GAAG,EAAE,eAAe,GAAG,GAAG,CAAC,OAAO,CAAC;IACnC,QAAQ,EAAE;QACR,YAAY,CAAC,EAAE,MAAM,MAAM,CAAC;KAC7B,CAAC;IACF,YAAY,EAAE,IAAI,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CACpC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,WAAW,CAiDhG"}
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseForESLint = parseForESLint;
4
+ const prisma_ast_1 = require("@mrleebo/prisma-ast");
5
+ /**
6
+ * Custom ESLint parser for Prisma schema files
7
+ * Converts Prisma AST to ESLint-compatible AST
8
+ */
9
+ function parseForESLint(code, _options = {}) {
10
+ try {
11
+ // Parse Prisma schema
12
+ const prismaAst = (0, prisma_ast_1.getSchema)(code);
13
+ // Convert to ESLint AST format
14
+ const eslintAst = {
15
+ type: 'Program',
16
+ body: [],
17
+ sourceType: 'module',
18
+ range: [0, code.length],
19
+ loc: {
20
+ start: { line: 1, column: 0 },
21
+ end: { line: code.split('\n').length, column: 0 },
22
+ },
23
+ tokens: [],
24
+ comments: [],
25
+ // Store the Prisma AST for rule access
26
+ prismaAst: prismaAst,
27
+ };
28
+ return {
29
+ ast: eslintAst,
30
+ services: {
31
+ getPrismaAst: () => prismaAst,
32
+ },
33
+ scopeManager: null,
34
+ visitorKeys: {},
35
+ };
36
+ }
37
+ catch {
38
+ // Return a minimal AST on parse errors
39
+ return {
40
+ ast: {
41
+ type: 'Program',
42
+ body: [],
43
+ sourceType: 'module',
44
+ range: [0, code.length],
45
+ loc: {
46
+ start: { line: 1, column: 0 },
47
+ end: { line: 1, column: 0 },
48
+ },
49
+ tokens: [],
50
+ comments: [],
51
+ },
52
+ services: {},
53
+ scopeManager: null,
54
+ visitorKeys: {},
55
+ };
56
+ }
57
+ }
58
+ //# sourceMappingURL=prisma-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prisma-parser.js","sourceRoot":"","sources":["../../src/parsers/prisma-parser.ts"],"names":[],"mappings":";;AAoBA,wCAiDC;AArED,oDAAwD;AAgBxD;;;GAGG;AACH,SAAgB,cAAc,CAAC,IAAY,EAAE,WAAoC,EAAE;IACjF,IAAI,CAAC;QACH,sBAAsB;QACtB,MAAM,SAAS,GAAG,IAAA,sBAAS,EAAC,IAAI,CAAC,CAAC;QAElC,+BAA+B;QAC/B,MAAM,SAAS,GAAoB;YACjC,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,EAAE;YACR,UAAU,EAAE,QAAQ;YACpB,KAAK,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC;YACvB,GAAG,EAAE;gBACH,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;gBAC7B,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE;aAClD;YACD,MAAM,EAAE,EAAE;YACV,QAAQ,EAAE,EAAE;YACZ,uCAAuC;YACvC,SAAS,EAAE,SAAS;SACrB,CAAC;QAEF,OAAO;YACL,GAAG,EAAE,SAAS;YACd,QAAQ,EAAE;gBACR,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS;aAC9B;YACD,YAAY,EAAE,IAAI;YAClB,WAAW,EAAE,EAAE;SAChB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,uCAAuC;QACvC,OAAO;YACL,GAAG,EAAE;gBACH,IAAI,EAAE,SAAS;gBACf,IAAI,EAAE,EAAE;gBACR,UAAU,EAAE,QAAQ;gBACpB,KAAK,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC;gBACvB,GAAG,EAAE;oBACH,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;oBAC7B,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;iBAC5B;gBACD,MAAM,EAAE,EAAE;gBACV,QAAQ,EAAE,EAAE;aACb;YACD,QAAQ,EAAE,EAAE;YACZ,YAAY,EAAE,IAAI;YAClB,WAAW,EAAE,EAAE;SAChB,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ interface RuleOptions {
3
+ schemaDir: string;
4
+ allowSharedModels: boolean;
5
+ }
6
+ declare const rule: ESLintUtils.RuleModule<"crossDomainAccess" | "modelNotFound" | "configError", [RuleOptions], unknown, ESLintUtils.RuleListener>;
7
+ export = rule;
8
+ //# sourceMappingURL=no-cross-domain-prisma-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-cross-domain-prisma-access.d.ts","sourceRoot":"","sources":["../../src/rules/no-cross-domain-prisma-access.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAY,MAAM,0BAA0B,CAAC;AAWjE,UAAU,WAAW;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAyDD,QAAA,MAAM,IAAI,iIAmIR,CAAC;AAEH,SAAS,IAAI,CAAC"}
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ const utils_1 = require("@typescript-eslint/utils");
36
+ const path = __importStar(require("path"));
37
+ const fs = __importStar(require("fs"));
38
+ const schema_parser_1 = require("../utils/schema-parser");
39
+ /**
40
+ * Check if a MemberExpression represents Prisma client access
41
+ */
42
+ function isPrismaClientAccess(node) {
43
+ // Direct access: prisma.user
44
+ if (node.object && node.object.type === 'Identifier' && node.object.name === 'prisma') {
45
+ return true;
46
+ }
47
+ // Property access: this.prisma.user, service.prisma.user
48
+ if (node.object && node.object.type === 'MemberExpression') {
49
+ return (node.object.property &&
50
+ node.object.property.type === 'Identifier' &&
51
+ node.object.property.name === 'prisma');
52
+ }
53
+ return false;
54
+ }
55
+ /**
56
+ * Extract model name from MemberExpression
57
+ */
58
+ function getModelNameFromMemberExpression(node) {
59
+ if (node.property && node.property.type === 'Identifier') {
60
+ return node.property.name;
61
+ }
62
+ return null;
63
+ }
64
+ /**
65
+ * Find project root by looking for package.json
66
+ */
67
+ function findProjectRoot(filePath) {
68
+ let dir = path.dirname(filePath);
69
+ while (dir !== path.dirname(dir)) {
70
+ if (fs.existsSync(path.join(dir, 'package.json'))) {
71
+ return dir;
72
+ }
73
+ dir = path.dirname(dir);
74
+ }
75
+ throw new Error('Could not find project root');
76
+ }
77
+ /**
78
+ * ESLint rule to prevent NestJS modules from accessing Prisma models
79
+ * that belong to other domains
80
+ */
81
+ const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/synapsestudios/eslint-plugin-data-boundaries#${name}`);
82
+ const rule = createRule({
83
+ name: 'no-cross-domain-prisma-access',
84
+ meta: {
85
+ type: 'problem',
86
+ docs: {
87
+ description: 'Disallow modules from accessing Prisma models defined in other domain schema files',
88
+ },
89
+ fixable: undefined,
90
+ schema: [
91
+ {
92
+ type: 'object',
93
+ properties: {
94
+ schemaDir: {
95
+ type: 'string',
96
+ description: 'Directory containing Prisma schema files (relative to project root)',
97
+ },
98
+ allowSharedModels: {
99
+ type: 'boolean',
100
+ description: 'Allow access to models in shared/main schema files',
101
+ },
102
+ },
103
+ additionalProperties: false,
104
+ },
105
+ ],
106
+ messages: {
107
+ crossDomainAccess: "Module '{{currentModule}}' cannot access '{{modelName}}' model (belongs to '{{modelDomain}}' domain). Consider using a shared service or moving the logic to the appropriate domain.",
108
+ modelNotFound: "Model '{{modelName}}' not found in any schema file. Ensure the model exists and schema files are properly configured.",
109
+ configError: "Could not determine schema directory. Please configure the 'schemaDir' option in your ESLint config.",
110
+ },
111
+ },
112
+ defaultOptions: [
113
+ {
114
+ schemaDir: 'prisma/schema',
115
+ allowSharedModels: true,
116
+ },
117
+ ],
118
+ create(context, [options]) {
119
+ const filename = context.getFilename();
120
+ // Only process TypeScript files in modules
121
+ if (!filename.includes('/modules/') || !filename.match(/\.(ts|tsx)$/)) {
122
+ return {};
123
+ }
124
+ // Extract current module from file path
125
+ const currentModule = (0, schema_parser_1.extractModuleFromPath)(filename);
126
+ if (!currentModule) {
127
+ return {};
128
+ }
129
+ // Build model-to-domain mapping
130
+ let modelToDomain = {};
131
+ try {
132
+ let schemaDir;
133
+ if (path.isAbsolute(options.schemaDir)) {
134
+ // Use absolute path directly (for tests)
135
+ schemaDir = options.schemaDir;
136
+ }
137
+ else {
138
+ // Resolve relative to project root (for real usage)
139
+ const projectRoot = findProjectRoot(filename);
140
+ schemaDir = path.join(projectRoot, options.schemaDir);
141
+ }
142
+ modelToDomain = (0, schema_parser_1.buildModelToDomainMapping)(schemaDir);
143
+ }
144
+ catch {
145
+ // Report configuration error only once per file
146
+ let reportedConfigError = false;
147
+ return {
148
+ Program(node) {
149
+ if (!reportedConfigError) {
150
+ context.report({
151
+ node,
152
+ messageId: 'configError',
153
+ });
154
+ reportedConfigError = true;
155
+ }
156
+ },
157
+ };
158
+ }
159
+ return {
160
+ // Detect property access: prisma.user, this.prisma.organization, etc.
161
+ MemberExpression(node) {
162
+ // Check if this looks like Prisma client access
163
+ if (!isPrismaClientAccess(node)) {
164
+ return;
165
+ }
166
+ // Extract the model name from the property access
167
+ const modelName = getModelNameFromMemberExpression(node);
168
+ if (!modelName || !(0, schema_parser_1.isPrismaModelName)(modelName)) {
169
+ return;
170
+ }
171
+ // Convert camelCase to PascalCase for schema lookup
172
+ const pascalModelName = (0, schema_parser_1.camelToPascalCase)(modelName);
173
+ // Check if model exists in schema files
174
+ const modelDomain = modelToDomain[pascalModelName];
175
+ if (!modelDomain) {
176
+ context.report({
177
+ node,
178
+ messageId: 'modelNotFound',
179
+ data: { modelName: pascalModelName },
180
+ });
181
+ return;
182
+ }
183
+ // Allow access to shared models if configured
184
+ if (options.allowSharedModels && modelDomain === 'shared') {
185
+ return;
186
+ }
187
+ // Check if current module matches model domain
188
+ if (currentModule !== modelDomain) {
189
+ context.report({
190
+ node,
191
+ messageId: 'crossDomainAccess',
192
+ data: {
193
+ currentModule,
194
+ modelName: pascalModelName,
195
+ modelDomain,
196
+ },
197
+ });
198
+ }
199
+ },
200
+ };
201
+ },
202
+ });
203
+ module.exports = rule;
204
+ //# sourceMappingURL=no-cross-domain-prisma-access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-cross-domain-prisma-access.js","sourceRoot":"","sources":["../../src/rules/no-cross-domain-prisma-access.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,oDAAiE;AACjE,2CAA6B;AAC7B,uCAAyB;AACzB,0DAMgC;AAOhC;;GAEG;AACH,SAAS,oBAAoB,CAAC,IAA+B;IAC3D,6BAA6B;IAC7B,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,yDAAyD;IACzD,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;QAC3D,OAAO,CACL,IAAI,CAAC,MAAM,CAAC,QAAQ;YACpB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,YAAY;YAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,QAAQ,CACvC,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,SAAS,gCAAgC,CAAC,IAA+B;IACvE,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QACzD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,SAAS,eAAe,CAAC,QAAgB;IACvC,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEjC,OAAO,GAAG,KAAK,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACjC,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC;YAClD,OAAO,GAAG,CAAC;QACb,CAAC;QACD,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;AACjD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,GAAG,mBAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CAAC,mEAAmE,IAAI,EAAE,CACpF,CAAC;AAEF,MAAM,IAAI,GAAG,UAAU,CAAuE;IAC5F,IAAI,EAAE,+BAA+B;IACrC,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EACT,oFAAoF;SACvF;QACD,OAAO,EAAE,SAAS;QAClB,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,SAAS,EAAE;wBACT,IAAI,EAAE,QAAQ;wBACd,WAAW,EAAE,qEAAqE;qBACnF;oBACD,iBAAiB,EAAE;wBACjB,IAAI,EAAE,SAAS;wBACf,WAAW,EAAE,oDAAoD;qBAClE;iBACF;gBACD,oBAAoB,EAAE,KAAK;aAC5B;SACF;QACD,QAAQ,EAAE;YACR,iBAAiB,EACf,sLAAsL;YACxL,aAAa,EACX,uHAAuH;YACzH,WAAW,EACT,sGAAsG;SACzG;KACF;IACD,cAAc,EAAE;QACd;YACE,SAAS,EAAE,eAAe;YAC1B,iBAAiB,EAAE,IAAI;SACxB;KACF;IACD,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC;QACvB,MAAM,QAAQ,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QAEvC,2CAA2C;QAC3C,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,CAAC;YACtE,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,wCAAwC;QACxC,MAAM,aAAa,GAAG,IAAA,qCAAqB,EAAC,QAAQ,CAAC,CAAC;QACtD,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,gCAAgC;QAChC,IAAI,aAAa,GAAyB,EAAE,CAAC;QAC7C,IAAI,CAAC;YACH,IAAI,SAAiB,CAAC;YACtB,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;gBACvC,yCAAyC;gBACzC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;YAChC,CAAC;iBAAM,CAAC;gBACN,oDAAoD;gBACpD,MAAM,WAAW,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;gBAC9C,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;YACxD,CAAC;YACD,aAAa,GAAG,IAAA,yCAAyB,EAAC,SAAS,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,gDAAgD;YAChD,IAAI,mBAAmB,GAAG,KAAK,CAAC;YAChC,OAAO;gBACL,OAAO,CAAC,IAAsB;oBAC5B,IAAI,CAAC,mBAAmB,EAAE,CAAC;wBACzB,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI;4BACJ,SAAS,EAAE,aAAa;yBACzB,CAAC,CAAC;wBACH,mBAAmB,GAAG,IAAI,CAAC;oBAC7B,CAAC;gBACH,CAAC;aACF,CAAC;QACJ,CAAC;QAED,OAAO;YACL,sEAAsE;YACtE,gBAAgB,CAAC,IAA+B;gBAC9C,gDAAgD;gBAChD,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC;oBAChC,OAAO;gBACT,CAAC;gBAED,kDAAkD;gBAClD,MAAM,SAAS,GAAG,gCAAgC,CAAC,IAAI,CAAC,CAAC;gBACzD,IAAI,CAAC,SAAS,IAAI,CAAC,IAAA,iCAAiB,EAAC,SAAS,CAAC,EAAE,CAAC;oBAChD,OAAO;gBACT,CAAC;gBAED,oDAAoD;gBACpD,MAAM,eAAe,GAAG,IAAA,iCAAiB,EAAC,SAAS,CAAC,CAAC;gBAErD,wCAAwC;gBACxC,MAAM,WAAW,GAAG,aAAa,CAAC,eAAe,CAAC,CAAC;gBACnD,IAAI,CAAC,WAAW,EAAE,CAAC;oBACjB,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,SAAS,EAAE,eAAe;wBAC1B,IAAI,EAAE,EAAE,SAAS,EAAE,eAAe,EAAE;qBACrC,CAAC,CAAC;oBACH,OAAO;gBACT,CAAC;gBAED,8CAA8C;gBAC9C,IAAI,OAAO,CAAC,iBAAiB,IAAI,WAAW,KAAK,QAAQ,EAAE,CAAC;oBAC1D,OAAO;gBACT,CAAC;gBAED,+CAA+C;gBAC/C,IAAI,aAAa,KAAK,WAAW,EAAE,CAAC;oBAClC,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,SAAS,EAAE,mBAAmB;wBAC9B,IAAI,EAAE;4BACJ,aAAa;4BACb,SAAS,EAAE,eAAe;4BAC1B,WAAW;yBACZ;qBACF,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC;AAEH,iBAAS,IAAI,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export = rule;
4
+ //# sourceMappingURL=no-cross-file-model-references.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-cross-file-model-references.d.ts","sourceRoot":"","sources":["../../src/rules/no-cross-file-model-references.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAqB9B,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAuGhB,CAAC;AAEF,SAAS,IAAI,CAAC"}
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ /**
3
+ * Helper function to find the line number of a field in the schema content
4
+ * This is a simplified implementation that finds the first occurrence
5
+ */
6
+ function getLineNumber(content, fieldName) {
7
+ const lines = content.split('\n');
8
+ for (let i = 0; i < lines.length; i++) {
9
+ if (lines[i].trim().startsWith(fieldName)) {
10
+ return i + 1; // ESLint uses 1-based line numbers
11
+ }
12
+ }
13
+ return 1; // Default to line 1 if not found
14
+ }
15
+ const rule = {
16
+ meta: {
17
+ type: 'problem',
18
+ docs: {
19
+ description: 'Disallow Prisma models from referencing models defined in other files',
20
+ category: 'Best Practices',
21
+ recommended: true,
22
+ },
23
+ fixable: undefined,
24
+ schema: [],
25
+ messages: {
26
+ crossFileReference: "Model field '{{field}}' references '{{model}}' which is not defined in this file. Cross-file model references are not allowed.",
27
+ },
28
+ },
29
+ create(context) {
30
+ // Only process .prisma files
31
+ const filename = context.getFilename();
32
+ if (!filename.endsWith('.prisma')) {
33
+ return {};
34
+ }
35
+ return {
36
+ Program(node) {
37
+ try {
38
+ // Get Prisma AST from parser services
39
+ const services = context.getSourceCode().parserServices;
40
+ if (!services || !services.getPrismaAst) {
41
+ // Parser doesn't support Prisma AST
42
+ return;
43
+ }
44
+ const ast = services.getPrismaAst();
45
+ // Extract all models defined in this file
46
+ const definedModels = new Set();
47
+ ast.list.forEach((item) => {
48
+ if (item.type === 'model' && item.name) {
49
+ definedModels.add(item.name);
50
+ }
51
+ });
52
+ // Check each model for cross-file references
53
+ ast.list.forEach((item) => {
54
+ if (item.type === 'model' && item.properties) {
55
+ item.properties.forEach((property) => {
56
+ if (property && property.fieldType && property.name) {
57
+ // Check if this field references another model
58
+ const referencedModelName = property.fieldType;
59
+ // Skip primitive types (String, Int, DateTime, etc.)
60
+ const primitiveTypes = [
61
+ 'String',
62
+ 'Int',
63
+ 'Float',
64
+ 'Boolean',
65
+ 'DateTime',
66
+ 'Json',
67
+ 'Bytes',
68
+ ];
69
+ if (primitiveTypes.includes(referencedModelName)) {
70
+ return;
71
+ }
72
+ // Skip array types (remove [] suffix)
73
+ const cleanModelName = referencedModelName.replace(/\[\]$/, '');
74
+ // Check if the referenced model is defined in this file
75
+ if (!definedModels.has(cleanModelName)) {
76
+ // This is a cross-file model reference - report it
77
+ const sourceCode = context.getSourceCode();
78
+ const schemaContent = sourceCode.getText();
79
+ const fieldLine = getLineNumber(schemaContent, property.name);
80
+ context.report({
81
+ node,
82
+ loc: {
83
+ line: fieldLine,
84
+ column: 0,
85
+ },
86
+ messageId: 'crossFileReference',
87
+ data: {
88
+ field: property.name,
89
+ model: cleanModelName,
90
+ },
91
+ });
92
+ }
93
+ }
94
+ });
95
+ }
96
+ });
97
+ }
98
+ catch (error) {
99
+ const errorMessage = error instanceof Error ? error.message : String(error);
100
+ console.error(`Error parsing Prisma schema in ${filename}:`, errorMessage);
101
+ // Don't report ESLint errors for parsing issues
102
+ }
103
+ },
104
+ };
105
+ },
106
+ };
107
+ module.exports = rule;
108
+ //# sourceMappingURL=no-cross-file-model-references.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-cross-file-model-references.js","sourceRoot":"","sources":["../../src/rules/no-cross-file-model-references.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export interface ModelToDomainMapping {
2
+ [modelName: string]: string;
3
+ }
4
+ /**
5
+ * Parse all schema files in a directory and build model-to-domain mapping
6
+ */
7
+ export declare function buildModelToDomainMapping(schemaDir: string): ModelToDomainMapping;
8
+ /**
9
+ * Extract module name from file path
10
+ */
11
+ export declare function extractModuleFromPath(filePath: string): string | null;
12
+ /**
13
+ * Check if a string looks like a Prisma model name (either camelCase or PascalCase)
14
+ */
15
+ export declare function isPrismaModelName(name: string): boolean;
16
+ /**
17
+ * Convert camelCase to PascalCase
18
+ */
19
+ export declare function camelToPascalCase(camelCase: string): string;
20
+ //# sourceMappingURL=schema-parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-parser.d.ts","sourceRoot":"","sources":["../../src/utils/schema-parser.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,oBAAoB;IACnC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7B;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,GAAG,oBAAoB,CA6BjF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAkCvD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE3D"}
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.buildModelToDomainMapping = buildModelToDomainMapping;
37
+ exports.extractModuleFromPath = extractModuleFromPath;
38
+ exports.isPrismaModelName = isPrismaModelName;
39
+ exports.camelToPascalCase = camelToPascalCase;
40
+ const prisma_ast_1 = require("@mrleebo/prisma-ast");
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const glob_1 = require("glob");
44
+ /**
45
+ * Parse all schema files in a directory and build model-to-domain mapping
46
+ */
47
+ function buildModelToDomainMapping(schemaDir) {
48
+ const modelToDomain = {};
49
+ try {
50
+ // Find all .prisma files in the schema directory
51
+ const schemaFiles = glob_1.glob.sync(path.join(schemaDir, '**/*.prisma'));
52
+ for (const filePath of schemaFiles) {
53
+ const content = fs.readFileSync(filePath, 'utf8');
54
+ const schema = (0, prisma_ast_1.getSchema)(content);
55
+ // Extract domain name from filename (e.g., auth.prisma -> auth)
56
+ const fileName = path.basename(filePath, '.prisma');
57
+ const domainName = fileName === 'schema' || fileName === 'main' ? 'shared' : fileName;
58
+ // Extract models from this schema file
59
+ schema.list.forEach((item) => {
60
+ if (item.type === 'model' && item.name) {
61
+ modelToDomain[item.name] = domainName;
62
+ }
63
+ });
64
+ }
65
+ }
66
+ catch (error) {
67
+ // If we can't parse schemas, return empty mapping
68
+ const errorMessage = error instanceof Error ? error.message : String(error);
69
+ console.warn(`Warning: Could not parse schema files in ${schemaDir}:`, errorMessage);
70
+ }
71
+ return modelToDomain;
72
+ }
73
+ /**
74
+ * Extract module name from file path
75
+ */
76
+ function extractModuleFromPath(filePath) {
77
+ // Normalize path separators for cross-platform compatibility
78
+ const normalizedPath = filePath.replace(/\\/g, '/');
79
+ // Match patterns like /modules/auth/... or /src/modules/organization/...
80
+ const moduleMatch = normalizedPath.match(/\/modules\/([^/]+)/);
81
+ return moduleMatch ? moduleMatch[1] : null;
82
+ }
83
+ /**
84
+ * Check if a string looks like a Prisma model name (either camelCase or PascalCase)
85
+ */
86
+ function isPrismaModelName(name) {
87
+ // Should not be a common method/property name
88
+ const commonMethods = [
89
+ 'findMany',
90
+ 'findFirst',
91
+ 'findUnique',
92
+ 'create',
93
+ 'update',
94
+ 'delete',
95
+ 'upsert',
96
+ 'count',
97
+ 'aggregate',
98
+ 'find',
99
+ 'get',
100
+ 'connect',
101
+ 'disconnect',
102
+ 'executeRaw',
103
+ 'queryRaw',
104
+ 'transaction',
105
+ ];
106
+ const commonProps = ['length', 'constructor', 'prototype', 'toString', 'valueOf'];
107
+ if (commonMethods.includes(name) || commonProps.includes(name)) {
108
+ return false;
109
+ }
110
+ // Must be non-empty, start with letter, contain only letters/numbers,
111
+ // be more than just a single letter, and not be all uppercase
112
+ return (name.length > 1 &&
113
+ /^[A-Za-z][A-Za-z0-9]*$/.test(name) &&
114
+ !/^\d/.test(name) &&
115
+ name !== name.toUpperCase()); // Not all uppercase
116
+ }
117
+ /**
118
+ * Convert camelCase to PascalCase
119
+ */
120
+ function camelToPascalCase(camelCase) {
121
+ return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
122
+ }
123
+ //# sourceMappingURL=schema-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-parser.js","sourceRoot":"","sources":["../../src/utils/schema-parser.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@synapsestudios/eslint-plugin-data-boundaries",
3
+ "version": "1.0.0",
4
+ "description": "ESLint plugin to enforce data boundary policies in modular monoliths",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "build:watch": "tsc --watch",
15
+ "clean": "rm -rf dist",
16
+ "prebuild": "npm run clean",
17
+ "prepublishOnly": "npm run build && npm run lint && npm run format:check && npm test",
18
+ "prepack": "npm run build",
19
+ "test": "jest",
20
+ "test:watch": "jest --watch",
21
+ "lint": "eslint src --ext .ts",
22
+ "format": "prettier --write src/**/*.ts tests/**/*.ts",
23
+ "format:check": "prettier --check src/**/*.ts tests/**/*.ts"
24
+ },
25
+ "keywords": [
26
+ "eslint",
27
+ "eslintplugin",
28
+ "eslint-plugin",
29
+ "prisma",
30
+ "data-boundaries",
31
+ "modular-monolith",
32
+ "domain-driven-design"
33
+ ],
34
+ "author": "Synapse Studios",
35
+ "license": "MIT",
36
+ "peerDependencies": {
37
+ "eslint": ">=7.0.0"
38
+ },
39
+ "dependencies": {
40
+ "@mrleebo/prisma-ast": "^0.12.0",
41
+ "@typescript-eslint/utils": "^8.0.0",
42
+ "glob": "^10.3.10"
43
+ },
44
+ "devDependencies": {
45
+ "@types/eslint": "^9.6.1",
46
+ "@types/jest": "^29.5.14",
47
+ "@types/node": "^24.1.0",
48
+ "@typescript-eslint/eslint-plugin": "^8.38.0",
49
+ "@typescript-eslint/parser": "^8.38.0",
50
+ "eslint": "^8.57.1",
51
+ "jest": "^29.7.0",
52
+ "prettier": "^3.6.2",
53
+ "ts-jest": "^29.4.0",
54
+ "typescript": "^5.8.3"
55
+ },
56
+ "engines": {
57
+ "node": ">=14.0.0"
58
+ },
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/synapsestudios/eslint-plugin-data-boundaries.git"
62
+ },
63
+ "bugs": {
64
+ "url": "https://github.com/synapsestudios/eslint-plugin-data-boundaries/issues"
65
+ },
66
+ "homepage": "https://github.com/synapsestudios/eslint-plugin-data-boundaries#readme"
67
+ }