eslint-plugin-supabase-services-layer 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 +21 -0
- package/README.md +316 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/no-direct-supabase-import.d.ts +28 -0
- package/dist/rules/no-direct-supabase-import.d.ts.map +1 -0
- package/dist/rules/no-direct-supabase-import.js +98 -0
- package/dist/rules/no-direct-supabase-import.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stephentraiforos
|
|
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,316 @@
|
|
|
1
|
+
# eslint-plugin-supabase-services-layer
|
|
2
|
+
|
|
3
|
+
ESLint plugin to enforce services layer architecture pattern in Supabase projects by restricting direct Supabase client imports to configured service directories.
|
|
4
|
+
|
|
5
|
+
## Why This Plugin?
|
|
6
|
+
|
|
7
|
+
In Supabase projects using Next.js (or other frameworks), it's a best practice to encapsulate database access and business logic in a services layer. This plugin enforces that pattern by preventing direct imports of Supabase clients outside of designated service directories.
|
|
8
|
+
|
|
9
|
+
**Benefits:**
|
|
10
|
+
- Centralized business logic
|
|
11
|
+
- Easier testing and mocking
|
|
12
|
+
- Consistent error handling
|
|
13
|
+
- Clear separation of concerns
|
|
14
|
+
- Better code organization
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install eslint-plugin-supabase-services-layer --save-dev
|
|
20
|
+
# or
|
|
21
|
+
pnpm add -D eslint-plugin-supabase-services-layer
|
|
22
|
+
# or
|
|
23
|
+
yarn add -D eslint-plugin-supabase-services-layer
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
Add the plugin to your ESLint configuration:
|
|
29
|
+
|
|
30
|
+
### Flat Config (eslint.config.js)
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import supabaseServicesLayer from 'eslint-plugin-supabase-services-layer';
|
|
34
|
+
|
|
35
|
+
export default [
|
|
36
|
+
{
|
|
37
|
+
plugins: {
|
|
38
|
+
'supabase-services-layer': supabaseServicesLayer,
|
|
39
|
+
},
|
|
40
|
+
rules: {
|
|
41
|
+
'supabase-services-layer/no-direct-supabase-import': [
|
|
42
|
+
'error',
|
|
43
|
+
{
|
|
44
|
+
allowedPaths: ['lib/services/**', 'supabase/functions/_shared/**'],
|
|
45
|
+
restrictedImports: ['@/lib/supabase/*', '**/supabase-client'],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Legacy Config (.eslintrc.js)
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
module.exports = {
|
|
57
|
+
plugins: ['supabase-services-layer'],
|
|
58
|
+
rules: {
|
|
59
|
+
'supabase-services-layer/no-direct-supabase-import': [
|
|
60
|
+
'error',
|
|
61
|
+
{
|
|
62
|
+
allowedPaths: ['lib/services/**', 'supabase/functions/_shared/**'],
|
|
63
|
+
restrictedImports: ['@/lib/supabase/*', '**/supabase-client'],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Rule Options
|
|
71
|
+
|
|
72
|
+
### `allowedPaths` (array of glob patterns)
|
|
73
|
+
|
|
74
|
+
Default: `['lib/services/**', 'src/services/**', 'supabase/functions/_shared/**']`
|
|
75
|
+
|
|
76
|
+
Glob patterns for directories where Supabase client imports are allowed.
|
|
77
|
+
|
|
78
|
+
**Examples:**
|
|
79
|
+
```javascript
|
|
80
|
+
allowedPaths: [
|
|
81
|
+
'lib/services/**', // Allow in lib/services/ and subdirectories
|
|
82
|
+
'src/db/**', // Allow in src/db/ and subdirectories
|
|
83
|
+
'supabase/functions/_shared/**', // Allow edge function services
|
|
84
|
+
]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `restrictedImports` (array of glob patterns)
|
|
88
|
+
|
|
89
|
+
Default: `['@/lib/supabase/*', '**/supabase-client']`
|
|
90
|
+
|
|
91
|
+
Import patterns to restrict. Supports glob patterns.
|
|
92
|
+
|
|
93
|
+
**Examples:**
|
|
94
|
+
```javascript
|
|
95
|
+
restrictedImports: [
|
|
96
|
+
'@/lib/supabase/*', // Restricts @/lib/supabase/server, client, middleware
|
|
97
|
+
'**/supabase-client', // Restricts relative imports to supabase-client files
|
|
98
|
+
'@supabase/supabase-js', // Restricts direct @supabase/supabase-js imports
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `errorMessage` (string)
|
|
103
|
+
|
|
104
|
+
Default: `"Supabase client imports are only allowed in services layer. Use service classes instead."`
|
|
105
|
+
|
|
106
|
+
Custom error message to display when violations are found.
|
|
107
|
+
|
|
108
|
+
## Examples
|
|
109
|
+
|
|
110
|
+
### ❌ Incorrect - Direct Import in API Route
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// app/api/assessments/route.ts
|
|
114
|
+
import { createClient } from '@/lib/supabase/server'; // ❌ ESLint Error
|
|
115
|
+
|
|
116
|
+
export async function GET() {
|
|
117
|
+
const supabase = await createClient();
|
|
118
|
+
const { data } = await supabase.from('assessments').select('*');
|
|
119
|
+
return Response.json(data);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### ✅ Correct - Using Service Factory
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// app/api/assessments/route.ts
|
|
127
|
+
import { createAssessmentService } from '@/lib/services/server'; // ✅ OK
|
|
128
|
+
|
|
129
|
+
export async function GET() {
|
|
130
|
+
const assessmentService = await createAssessmentService();
|
|
131
|
+
const assessments = await assessmentService.listAssessments();
|
|
132
|
+
return Response.json(assessments);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### ❌ Incorrect - Direct Import in Edge Function
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// supabase/functions/worker/index.ts
|
|
140
|
+
import { createClient } from '../_shared/supabase-client'; // ❌ ESLint Error
|
|
141
|
+
|
|
142
|
+
Deno.serve(async (req) => {
|
|
143
|
+
const supabase = createClient();
|
|
144
|
+
const { data } = await supabase.from('queue').select('*');
|
|
145
|
+
// ...
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### ✅ Correct - Using Edge Function Service
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// supabase/functions/worker/index.ts
|
|
153
|
+
import { QueueService } from '../_shared/queue-service'; // ✅ OK
|
|
154
|
+
|
|
155
|
+
Deno.serve(async (req) => {
|
|
156
|
+
const queueService = new QueueService(this.supabaseFactory);
|
|
157
|
+
const jobs = await queueService.getPendingJobs();
|
|
158
|
+
// ...
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### ✅ Allowed - Import in Service File
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// lib/services/server/assessment-service.ts
|
|
166
|
+
import { createClient } from '@/lib/supabase/server'; // ✅ OK - in allowed path
|
|
167
|
+
|
|
168
|
+
export async function createAssessmentService() {
|
|
169
|
+
const supabase = await createClient();
|
|
170
|
+
return {
|
|
171
|
+
async listAssessments() {
|
|
172
|
+
const { data } = await supabase.from('assessments').select('*');
|
|
173
|
+
return data;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Common Project Structures
|
|
180
|
+
|
|
181
|
+
### Next.js with App Router
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
// eslint.config.js
|
|
185
|
+
export default [
|
|
186
|
+
{
|
|
187
|
+
rules: {
|
|
188
|
+
'supabase-services-layer/no-direct-supabase-import': [
|
|
189
|
+
'error',
|
|
190
|
+
{
|
|
191
|
+
allowedPaths: [
|
|
192
|
+
'lib/services/**',
|
|
193
|
+
'supabase/functions/_shared/**',
|
|
194
|
+
],
|
|
195
|
+
restrictedImports: [
|
|
196
|
+
'@/lib/supabase/*',
|
|
197
|
+
'**/supabase-client',
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Monorepo with Multiple Packages
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
// eslint.config.js
|
|
210
|
+
export default [
|
|
211
|
+
{
|
|
212
|
+
rules: {
|
|
213
|
+
'supabase-services-layer/no-direct-supabase-import': [
|
|
214
|
+
'error',
|
|
215
|
+
{
|
|
216
|
+
allowedPaths: [
|
|
217
|
+
'packages/api/src/services/**',
|
|
218
|
+
'packages/shared/src/db/**',
|
|
219
|
+
],
|
|
220
|
+
restrictedImports: [
|
|
221
|
+
'@my-org/supabase/*',
|
|
222
|
+
'~/lib/supabase/*',
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Custom Services Directory
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
// eslint.config.js
|
|
235
|
+
export default [
|
|
236
|
+
{
|
|
237
|
+
rules: {
|
|
238
|
+
'supabase-services-layer/no-direct-supabase-import': [
|
|
239
|
+
'error',
|
|
240
|
+
{
|
|
241
|
+
allowedPaths: ['src/data-access/**', 'src/api/services/**'],
|
|
242
|
+
restrictedImports: ['~/supabase-clients/*'],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Troubleshooting
|
|
251
|
+
|
|
252
|
+
### False Positives in Allowed Paths
|
|
253
|
+
|
|
254
|
+
If you're getting errors in files that should be allowed:
|
|
255
|
+
|
|
256
|
+
1. Check your glob patterns match the file path
|
|
257
|
+
2. Use `**` to match subdirectories: `lib/services/**`
|
|
258
|
+
3. Verify the path separator matches your OS (use `/` for cross-platform)
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
// ❌ Wrong - won't match subdirectories
|
|
262
|
+
allowedPaths: ['lib/services']
|
|
263
|
+
|
|
264
|
+
// ✅ Correct - matches all subdirectories
|
|
265
|
+
allowedPaths: ['lib/services/**']
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Need to Temporarily Disable
|
|
269
|
+
|
|
270
|
+
If you need to temporarily allow a violation (not recommended):
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// eslint-disable-next-line supabase-services-layer/no-direct-supabase-import
|
|
274
|
+
import { createClient } from '@/lib/supabase/server';
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Better approach: Create a service method instead!
|
|
278
|
+
|
|
279
|
+
### Pattern Not Matching
|
|
280
|
+
|
|
281
|
+
If your custom patterns aren't working:
|
|
282
|
+
|
|
283
|
+
1. Ensure patterns use forward slashes: `src/services/**`
|
|
284
|
+
2. Use `*` for single-level matching: `*.ts`
|
|
285
|
+
3. Use `**` for multi-level matching: `lib/**`
|
|
286
|
+
4. Test patterns with [minimatch](https://github.com/isaacs/minimatch#testing)
|
|
287
|
+
|
|
288
|
+
## Migration Guide
|
|
289
|
+
|
|
290
|
+
If you're adding this plugin to an existing codebase:
|
|
291
|
+
|
|
292
|
+
1. Install the plugin
|
|
293
|
+
2. Run ESLint to find all violations
|
|
294
|
+
3. Create service classes for common operations
|
|
295
|
+
4. Refactor code to use services
|
|
296
|
+
5. Re-run ESLint to verify
|
|
297
|
+
|
|
298
|
+
**Common refactoring patterns:**
|
|
299
|
+
|
|
300
|
+
- API Routes → Use factory functions from `lib/services/server`
|
|
301
|
+
- Edge Functions → Use service classes with `ISupabaseClientFactory`
|
|
302
|
+
- Components → Use client services from `lib/services/client`
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
MIT
|
|
307
|
+
|
|
308
|
+
## Contributing
|
|
309
|
+
|
|
310
|
+
Contributions welcome! Please open an issue or PR.
|
|
311
|
+
|
|
312
|
+
## Related
|
|
313
|
+
|
|
314
|
+
- [Supabase Documentation](https://supabase.com/docs)
|
|
315
|
+
- [Next.js App Router](https://nextjs.org/docs/app)
|
|
316
|
+
- [ESLint Plugin Documentation](https://eslint.org/docs/latest/extend/plugins)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint plugin to enforce services layer architecture in Supabase projects
|
|
3
|
+
*/
|
|
4
|
+
declare const plugin: {
|
|
5
|
+
meta: {
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
};
|
|
9
|
+
rules: {
|
|
10
|
+
'no-direct-supabase-import': import("eslint").Rule.RuleModule;
|
|
11
|
+
};
|
|
12
|
+
configs: {
|
|
13
|
+
recommended: {
|
|
14
|
+
plugins: string[];
|
|
15
|
+
rules: {
|
|
16
|
+
'supabase-services-layer/no-direct-supabase-import': string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export default plugin;
|
|
22
|
+
export type { RuleOptions } from './rules/no-direct-supabase-import';
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;CAgBX,CAAC;AAEF,eAAe,MAAM,CAAC;AAGtB,YAAY,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const no_direct_supabase_import_1 = __importDefault(require("./rules/no-direct-supabase-import"));
|
|
7
|
+
/**
|
|
8
|
+
* ESLint plugin to enforce services layer architecture in Supabase projects
|
|
9
|
+
*/
|
|
10
|
+
const plugin = {
|
|
11
|
+
meta: {
|
|
12
|
+
name: 'eslint-plugin-supabase-services-layer',
|
|
13
|
+
version: '1.0.0',
|
|
14
|
+
},
|
|
15
|
+
rules: {
|
|
16
|
+
'no-direct-supabase-import': no_direct_supabase_import_1.default,
|
|
17
|
+
},
|
|
18
|
+
configs: {
|
|
19
|
+
recommended: {
|
|
20
|
+
plugins: ['supabase-services-layer'],
|
|
21
|
+
rules: {
|
|
22
|
+
'supabase-services-layer/no-direct-supabase-import': 'error',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
exports.default = plugin;
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,kGAAuE;AAEvE;;GAEG;AACH,MAAM,MAAM,GAAG;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,uCAAuC;QAC7C,OAAO,EAAE,OAAO;KACjB;IACD,KAAK,EAAE;QACL,2BAA2B,EAAE,mCAAsB;KACpD;IACD,OAAO,EAAE;QACP,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,yBAAyB,CAAC;YACpC,KAAK,EAAE;gBACL,mDAAmD,EAAE,OAAO;aAC7D;SACF;KACF;CACF,CAAC;AAEF,kBAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Rule } from 'eslint';
|
|
2
|
+
/**
|
|
3
|
+
* Rule configuration options
|
|
4
|
+
*/
|
|
5
|
+
export interface RuleOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Glob patterns for paths where Supabase client imports are allowed
|
|
8
|
+
* @default ["lib/services/**", "src/services/**", "supabase/functions/_shared/**"]
|
|
9
|
+
*/
|
|
10
|
+
allowedPaths?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* Import patterns to restrict (supports glob patterns)
|
|
13
|
+
* @default - See DEFAULT_RESTRICTED_PATTERNS in source
|
|
14
|
+
*/
|
|
15
|
+
restrictedImports?: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Custom error message
|
|
18
|
+
* @default "Supabase client imports are only allowed in services layer. Use service classes instead."
|
|
19
|
+
*/
|
|
20
|
+
errorMessage?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* ESLint rule to enforce services layer architecture
|
|
24
|
+
* Prevents direct Supabase client imports outside configured service directories
|
|
25
|
+
*/
|
|
26
|
+
declare const rule: Rule.RuleModule;
|
|
27
|
+
export default rule;
|
|
28
|
+
//# sourceMappingURL=no-direct-supabase-import.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-direct-supabase-import.d.ts","sourceRoot":"","sources":["../../src/rules/no-direct-supabase-import.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAG9B;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAgBD;;;GAGG;AACH,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAuFhB,CAAC;AAEF,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const minimatch_1 = require("minimatch");
|
|
4
|
+
const DEFAULT_ALLOWED_PATTERNS = [
|
|
5
|
+
'lib/services/**',
|
|
6
|
+
'src/services/**',
|
|
7
|
+
'supabase/functions/_shared/**',
|
|
8
|
+
];
|
|
9
|
+
const DEFAULT_RESTRICTED_PATTERNS = [
|
|
10
|
+
'@/lib/supabase/*',
|
|
11
|
+
'**/supabase-client',
|
|
12
|
+
];
|
|
13
|
+
const DEFAULT_ERROR_MESSAGE = 'Supabase client imports are only allowed in services layer. Use service classes instead.';
|
|
14
|
+
/**
|
|
15
|
+
* ESLint rule to enforce services layer architecture
|
|
16
|
+
* Prevents direct Supabase client imports outside configured service directories
|
|
17
|
+
*/
|
|
18
|
+
const rule = {
|
|
19
|
+
meta: {
|
|
20
|
+
type: 'problem',
|
|
21
|
+
docs: {
|
|
22
|
+
description: 'Restrict Supabase client imports to services layer only to enforce layered architecture',
|
|
23
|
+
recommended: true,
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
restrictedImport: '{{errorMessage}}',
|
|
27
|
+
},
|
|
28
|
+
schema: [
|
|
29
|
+
{
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
allowedPaths: {
|
|
33
|
+
type: 'array',
|
|
34
|
+
items: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
restrictedImports: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
items: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
errorMessage: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
create(context) {
|
|
53
|
+
const options = context.options[0] || {};
|
|
54
|
+
const ruleOptions = {
|
|
55
|
+
allowedPaths: options.allowedPaths || DEFAULT_ALLOWED_PATTERNS,
|
|
56
|
+
restrictedImports: options.restrictedImports || DEFAULT_RESTRICTED_PATTERNS,
|
|
57
|
+
errorMessage: options.errorMessage || DEFAULT_ERROR_MESSAGE,
|
|
58
|
+
};
|
|
59
|
+
const filename = context.filename;
|
|
60
|
+
// If no filename provided (e.g., in RuleTester), check if test case
|
|
61
|
+
// RuleTester passes test cases with filename in the test object
|
|
62
|
+
if (!filename) {
|
|
63
|
+
// For RuleTester without explicit filename, allow the check
|
|
64
|
+
// This handles edge cases in testing
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
// Check if current file is in an allowed path
|
|
68
|
+
const isAllowedPath = ruleOptions.allowedPaths.some((pattern) => {
|
|
69
|
+
const matcher = new minimatch_1.Minimatch(pattern, { dot: true });
|
|
70
|
+
return matcher.match(filename);
|
|
71
|
+
});
|
|
72
|
+
// Skip checking if file is in allowed path
|
|
73
|
+
if (isAllowedPath) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
ImportDeclaration(node) {
|
|
78
|
+
const importPath = String(node.source.value);
|
|
79
|
+
// Check if import matches any restricted pattern
|
|
80
|
+
const isRestricted = ruleOptions.restrictedImports.some((pattern) => {
|
|
81
|
+
const matcher = new minimatch_1.Minimatch(pattern);
|
|
82
|
+
return matcher.match(importPath);
|
|
83
|
+
});
|
|
84
|
+
if (isRestricted) {
|
|
85
|
+
context.report({
|
|
86
|
+
node,
|
|
87
|
+
messageId: 'restrictedImport',
|
|
88
|
+
data: {
|
|
89
|
+
errorMessage: ruleOptions.errorMessage,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
exports.default = rule;
|
|
98
|
+
//# sourceMappingURL=no-direct-supabase-import.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-direct-supabase-import.js","sourceRoot":"","sources":["../../src/rules/no-direct-supabase-import.ts"],"names":[],"mappings":";;AACA,yCAAsC;AAyBtC,MAAM,wBAAwB,GAAG;IAC/B,iBAAiB;IACjB,iBAAiB;IACjB,+BAA+B;CAChC,CAAC;AAEF,MAAM,2BAA2B,GAAG;IAClC,kBAAkB;IAClB,oBAAoB;CACrB,CAAC;AAEF,MAAM,qBAAqB,GACzB,0FAA0F,CAAC;AAE7F;;;GAGG;AACH,MAAM,IAAI,GAAoB;IAC5B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EACT,yFAAyF;YAC3F,WAAW,EAAE,IAAI;SAClB;QACD,QAAQ,EAAE;YACR,gBAAgB,EAAE,kBAAkB;SACrC;QACD,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,YAAY,EAAE;wBACZ,IAAI,EAAE,OAAO;wBACb,KAAK,EAAE;4BACL,IAAI,EAAE,QAAQ;yBACf;qBACF;oBACD,iBAAiB,EAAE;wBACjB,IAAI,EAAE,OAAO;wBACb,KAAK,EAAE;4BACL,IAAI,EAAE,QAAQ;yBACf;qBACF;oBACD,YAAY,EAAE;wBACZ,IAAI,EAAE,QAAQ;qBACf;iBACF;gBACD,oBAAoB,EAAE,KAAK;aAC5B;SACF;KACF;IAED,MAAM,CAAC,OAAO;QACZ,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACzC,MAAM,WAAW,GAAgB;YAC/B,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,wBAAwB;YAC9D,iBAAiB,EAAE,OAAO,CAAC,iBAAiB,IAAI,2BAA2B;YAC3E,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,qBAAqB;SAC5D,CAAC;QAEF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QAElC,oEAAoE;QACpE,gEAAgE;QAChE,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,4DAA4D;YAC5D,qCAAqC;YACrC,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,8CAA8C;QAC9C,MAAM,aAAa,GAAG,WAAW,CAAC,YAAa,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;YAC/D,MAAM,OAAO,GAAG,IAAI,qBAAS,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;YACtD,OAAO,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,2CAA2C;QAC3C,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;YACL,iBAAiB,CAAC,IAAI;gBACpB,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAE7C,iDAAiD;gBACjD,MAAM,YAAY,GAAG,WAAW,CAAC,iBAAkB,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE;oBACnE,MAAM,OAAO,GAAG,IAAI,qBAAS,CAAC,OAAO,CAAC,CAAC;oBACvC,OAAO,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;gBACnC,CAAC,CAAC,CAAC;gBAEH,IAAI,YAAY,EAAE,CAAC;oBACjB,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,SAAS,EAAE,kBAAkB;wBAC7B,IAAI,EAAE;4BACJ,YAAY,EAAE,WAAW,CAAC,YAAY;yBACvC;qBACF,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,kBAAe,IAAI,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-supabase-services-layer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESLint plugin to enforce services layer architecture pattern in Supabase projects by restricting direct Supabase client imports to configured service directories",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eslint",
|
|
7
|
+
"eslint-plugin",
|
|
8
|
+
"supabase",
|
|
9
|
+
"services-layer",
|
|
10
|
+
"architecture",
|
|
11
|
+
"layered-architecture",
|
|
12
|
+
"code-organization",
|
|
13
|
+
"database",
|
|
14
|
+
"react",
|
|
15
|
+
"nextjs"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/itz4blitz/mimir#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/itz4blitz/mimir/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/itz4blitz/mimir.git",
|
|
24
|
+
"directory": "packages/eslint-plugin-supabase-services-layer"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Stephentraiforos",
|
|
28
|
+
"main": "dist/index.js",
|
|
29
|
+
"types": "dist/index.d.ts",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"eslint": ">=9"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"minimatch": "^10.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/minimatch": "^5.1.2",
|
|
43
|
+
"@typescript-eslint/parser": "^8.55.0",
|
|
44
|
+
"@typescript-eslint/rule-tester": "^8.55.0",
|
|
45
|
+
"typescript": "^5",
|
|
46
|
+
"vitest": "^4.0.14"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc",
|
|
53
|
+
"dev": "tsc --watch",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"test:coverage": "vitest run --coverage",
|
|
57
|
+
"lint": "eslint . --config ../../eslint.config.cjs"
|
|
58
|
+
}
|
|
59
|
+
}
|