supa-forge 0.1.1

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.
Files changed (40) hide show
  1. package/README.md +65 -0
  2. package/bin/cli.js +116 -0
  3. package/dist/index.d.ts +210 -0
  4. package/dist/index.js +79 -0
  5. package/dist/index.mjs +1364 -0
  6. package/dist/typeorm.d.ts +26 -0
  7. package/dist/typeorm.js +1 -0
  8. package/dist/typeorm.mjs +15 -0
  9. package/examples/Only CLS/package-lock.json +3140 -0
  10. package/examples/Only CLS/package.json +25 -0
  11. package/examples/Only CLS/src/employee.test.ts +109 -0
  12. package/examples/Only CLS/src/entities/Employee.entity.ts +27 -0
  13. package/examples/Only CLS/src/index.ts +45 -0
  14. package/examples/Only CLS/tsconfig.json +17 -0
  15. package/examples/Only CLS/vite.config.ts +8 -0
  16. package/examples/Only RLS/package-lock.json +3140 -0
  17. package/examples/Only RLS/package.json +25 -0
  18. package/examples/Only RLS/src/entities/Note.entity.ts +40 -0
  19. package/examples/Only RLS/src/entities/User.entity.ts +29 -0
  20. package/examples/Only RLS/src/index.ts +51 -0
  21. package/examples/Only RLS/src/notes.test.ts +123 -0
  22. package/examples/Only RLS/tsconfig.json +17 -0
  23. package/examples/Only RLS/vite.config.ts +8 -0
  24. package/examples/RBAC + CLS + RLS/package-lock.json +3140 -0
  25. package/examples/RBAC + CLS + RLS/package.json +21 -0
  26. package/examples/RBAC + CLS + RLS/src/entities/Project.entity.ts +47 -0
  27. package/examples/RBAC + CLS + RLS/src/entities/User.entity.ts +31 -0
  28. package/examples/RBAC + CLS + RLS/src/entities/UserRole.entity.ts +110 -0
  29. package/examples/RBAC + CLS + RLS/src/index.ts +42 -0
  30. package/examples/RBAC + CLS + RLS/src/project.test.ts +117 -0
  31. package/examples/RBAC + CLS + RLS/tsconfig.json +17 -0
  32. package/examples/RBAC + CLS + RLS/vite.config.ts +8 -0
  33. package/examples/Without Security/package-lock.json +3940 -0
  34. package/examples/Without Security/package.json +25 -0
  35. package/examples/Without Security/src/entities/Pharmacy.entity.ts +20 -0
  36. package/examples/Without Security/src/index.ts +52 -0
  37. package/examples/Without Security/src/pharmacy.test.ts +104 -0
  38. package/examples/Without Security/tsconfig.json +17 -0
  39. package/examples/Without Security/vite.config.ts +8 -0
  40. package/package.json +72 -0
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "example-only-cls",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "generate": "vite-node src/index.ts --generate",
7
+ "apply": "vite-node src/index.ts --apply",
8
+ "temp:generate": "vite-node src/index.ts --temp --generate",
9
+ "temp:apply": "vite-node src/index.ts --temp --apply",
10
+ "migration:create": "vite-node src/index.ts migration:create",
11
+ "docgen": "vite-node src/index.ts --docgen",
12
+ "test": "vitest run"
13
+ },
14
+ "dependencies": {
15
+ "supaforge": "file:../.."
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.0.0",
19
+ "typeorm": "^0.3.17",
20
+ "typescript": "^5.0.0",
21
+ "vite": "^5.0.0",
22
+ "vite-node": "^1.0.0",
23
+ "vitest": "0.34.6"
24
+ }
25
+ }
@@ -0,0 +1,109 @@
1
+ import 'reflect-metadata';
2
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3
+ import 'dotenv/config';
4
+ import { DataSource, QueryRunner } from 'supaforge/typeorm';
5
+ import { runSupaforge, applyGeneratedMigrations, runAsRole } from 'supaforge';
6
+ import { Employee } from './entities/Employee.entity';
7
+ import * as path from 'path';
8
+ import * as fs from 'fs';
9
+ import { fileURLToPath } from 'url';
10
+ import process from 'process';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ describe('Employee CLS Tests (RLS Off)', () => {
16
+ let dataSource: DataSource;
17
+ let queryRunner: QueryRunner;
18
+
19
+ const DB_CONFIG = {
20
+ host: process.env.DB_HOST || 'localhost',
21
+ port: Number(process.env.DB_PORT) || 5432,
22
+ user: process.env.DB_USER || 'postgres',
23
+ password: process.env.DB_PASSWORD || 'postgres',
24
+ database: process.env.DB_NAME || 'postgres',
25
+ };
26
+
27
+ const EMP_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
28
+
29
+ beforeAll(async () => {
30
+ const migrationDir = path.join(process.cwd(), 'migrations');
31
+ if (fs.existsSync(migrationDir)) {
32
+ fs.rmSync(migrationDir, { recursive: true, force: true });
33
+ }
34
+
35
+ const options = {
36
+ dbConfig: DB_CONFIG,
37
+ entityPath: path.join(__dirname, 'entities'),
38
+ outputDir: migrationDir,
39
+ syncSchema: true,
40
+ migrationName: 'EmployeeInit',
41
+ rls: false,
42
+ cls: true
43
+ };
44
+
45
+ await runSupaforge(options);
46
+ await applyGeneratedMigrations(options);
47
+
48
+ dataSource = new DataSource({
49
+ type: 'postgres',
50
+ ...DB_CONFIG,
51
+ username: DB_CONFIG.user,
52
+ entities: [Employee],
53
+ });
54
+
55
+ await dataSource.initialize();
56
+ queryRunner = dataSource.createQueryRunner();
57
+ await queryRunner.connect();
58
+
59
+ await queryRunner.query('DELETE FROM employees');
60
+ await queryRunner.query("INSERT INTO employees (id, name, department, salary, private_phone) VALUES ($1, 'John Doe', 'Engineering', 5000, '+1-555-0199')", [EMP_ID]);
61
+ }, 45000);
62
+
63
+ afterAll(async () => {
64
+ if (queryRunner) await queryRunner.release();
65
+ if (dataSource) await dataSource.destroy();
66
+ });
67
+
68
+ it('Anon should see name and department but NOT salary', async () => {
69
+ const data = await runAsRole(queryRunner, 'anon', '00000000-0000-0000-0000-000000000000',
70
+ 'SELECT id, name, department FROM employees'
71
+ );
72
+ expect(data).toHaveLength(1);
73
+ expect(data[0].name).toBe('John Doe');
74
+ expect(data[0].salary).toBeUndefined();
75
+
76
+ const operation = runAsRole(queryRunner, 'anon', '00000000-0000-0000-0000-000000000000',
77
+ 'SELECT salary FROM employees'
78
+ );
79
+ await expect(operation).rejects.toThrow(/permission denied/);
80
+ });
81
+
82
+ it('Authenticated user should see name but NOT private_phone', async () => {
83
+ const data = await runAsRole(queryRunner, 'authenticated', '11111111-1111-1111-1111-111111111111',
84
+ 'SELECT name FROM employees'
85
+ );
86
+ expect(data[0].name).toBe('John Doe');
87
+
88
+ const operation = runAsRole(queryRunner, 'authenticated', '11111111-1111-1111-1111-111111111111',
89
+ 'SELECT private_phone FROM employees'
90
+ );
91
+ await expect(operation).rejects.toThrow(/permission denied/);
92
+ });
93
+
94
+ it('Admin should see EVERYTHING (salary and phone)', async () => {
95
+ const data = await runAsRole(queryRunner, 'admin', '22222222-2222-2222-2222-222222222222',
96
+ 'SELECT name, salary, private_phone FROM employees'
97
+ );
98
+ expect(data).toHaveLength(1);
99
+ expect(data[0].salary).toBe('5000');
100
+ expect(data[0].private_phone).toBe('+1-555-0199');
101
+ });
102
+
103
+ it('Select * should fail for anon due to column zero-trust', async () => {
104
+ const operation = runAsRole(queryRunner, 'anon', '00000000-0000-0000-0000-000000000000',
105
+ 'SELECT * FROM employees'
106
+ );
107
+ await expect(operation).rejects.toThrow(/permission denied/);
108
+ });
109
+ });
@@ -0,0 +1,27 @@
1
+ import 'reflect-metadata';
2
+ import { Entity, PrimaryGeneratedColumn, Column } from 'supaforge/typeorm';
3
+ import { security, cls } from 'supaforge';
4
+
5
+ @Entity('employees')
6
+ @security()
7
+ export class Employee {
8
+ @PrimaryGeneratedColumn('uuid')
9
+ @cls({ anon: 'r', authenticated: 'r', admin: 'r' })
10
+ id!: string;
11
+
12
+ @Column({ type: 'text' })
13
+ @cls({ anon: 'r', authenticated: 'r', admin: 'r' })
14
+ name!: string;
15
+
16
+ @Column({ type: 'text' })
17
+ @cls({ anon: 'r', authenticated: 'r', admin: 'r' })
18
+ department!: string;
19
+
20
+ @Column({ type: 'numeric', nullable: true })
21
+ @cls({ admin: 'r' })
22
+ salary!: number;
23
+
24
+ @Column({ type: 'text', nullable: true })
25
+ @cls({ admin: 'r' })
26
+ private_phone!: string;
27
+ }
@@ -0,0 +1,45 @@
1
+ import 'reflect-metadata';
2
+ import * as path from 'path';
3
+ import 'dotenv/config';
4
+ import { runSupaforge, applyGeneratedMigrations, extractCliOptions, RunnerOptions } from 'supaforge';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ async function main() {
11
+ const flags = process.argv;
12
+ const cliOptions = extractCliOptions(flags);
13
+
14
+ const options: RunnerOptions = {
15
+ ...cliOptions,
16
+ dbConfig: {
17
+ host: process.env.DB_HOST || 'localhost',
18
+ port: Number(process.env.DB_PORT) || 5432,
19
+ user: process.env.DB_USER || 'postgres',
20
+ password: process.env.DB_PASSWORD || 'postgres',
21
+ database: flags.includes('--temp') ? 'temp_cls' : (process.env.DB_NAME || 'postgres'),
22
+ },
23
+ entityPath: path.join(__dirname, 'entities'),
24
+ outputDir: path.join(process.cwd(), 'migrations'),
25
+ syncSchema: cliOptions.syncSchema || flags.includes('--temp'),
26
+ cls: true,
27
+ rls: false,
28
+ };
29
+
30
+ try {
31
+ if (flags.includes('--apply')) {
32
+ return await applyGeneratedMigrations(options);
33
+ }
34
+ if (flags.includes('--generate')) {
35
+ await runSupaforge(options);
36
+ console.log('CLS Migration generated.');
37
+ return;
38
+ }
39
+ } catch (error) {
40
+ console.error('\nError:', error);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ main();
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "experimentalDecorators": true,
9
+ "emitDecoratorMetadata": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "noImplicitAny": true,
13
+ "outDir": "./dist",
14
+ "types": ["node"]
15
+ },
16
+ "include": ["src/**/*"]
17
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config.js';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ },
8
+ });