Skip to content

Commit acd33af

Browse files
authored
feat: add experimental plugin API and AWS auth plugin (#452)
### What changed - Experimental plugin system for extending SafeQL with custom behavior - First official plugin: `@ts-safeql/plugin-auth-aws` for AWS RDS IAM authentication - Docs and demo project for plugin usage ### Why Enables modular extensibility - users can add plugins for custom authentication, connection logic, and analysis hooks without hardcoding every scenario. Keeps SafeQL’s worker-based design while making it extensible.
1 parent 09b747e commit acd33af

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+4064
-163
lines changed

.agents/docs/authoring-plugin.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Authoring a SafeQL Plugin
2+
3+
## Checklist
4+
5+
- [ ] Create a new package under `packages/plugins/<name>/` (see [creating a package](creating-package.md))
6+
- [ ] Add `@ts-safeql/plugin-utils` as a dependency
7+
- [ ] Add `postgres` as a dependency (if using `createConnection`)
8+
- [ ] Default-export a `definePlugin()` result
9+
- [ ] Add tests under `src/plugin.test.ts` and `src/plugin.integration.test.ts`
10+
- [ ] Add a demo under `demos/plugin-<name>/`
11+
- [ ] Add a docs page at `docs/plugins/<name>.md` or update `docs/compatibility/<name>.md`
12+
- [ ] Add sidebar entry in `docs/.vitepress/config.ts`
13+
14+
## Plugin Hooks
15+
16+
### `createConnection`
17+
18+
Custom database connection strategy. Returns a `postgres` Sql instance.
19+
20+
### `connectionDefaults`
21+
22+
Default config values merged under user config. Use for library-specific type overrides.
23+
24+
### `onTarget({ node, context })`
25+
26+
Called for each TaggedTemplateExpression. Return:
27+
28+
- `TargetMatch` object to proceed with checking
29+
- `false` to skip entirely (e.g., `sql.fragment`)
30+
- `undefined` to defer to SafeQL default
31+
32+
### `onExpression({ node, context })`
33+
34+
Called for each interpolated expression. Return:
35+
36+
- `string` - SQL fragment (use `$N` for placeholder)
37+
- `false` - skip the entire query
38+
- `undefined` - use default `$N::type` behavior
39+
40+
## Key Constraints
41+
42+
- Use `type` aliases (not `interface`) for the config type
43+
- Plugin names are auto-prefixed with `safeql-plugin-`
44+
- Package names must be prefixed with `plugin-` (e.g., `@ts-safeql/plugin-auth-aws`)
45+
- Test file naming: `plugin.test.ts` for unit tests, `plugin.integration.test.ts` for DB tests
46+
47+
## Example Structure
48+
49+
```
50+
packages/plugins/my-lib/
51+
├── src/
52+
│ ├── index.ts # definePlugin() export
53+
│ ├── plugin.test.ts # Unit tests (PluginTestDriver)
54+
│ └── plugin.integration.test.ts # Integration tests (RuleTester)
55+
├── package.json
56+
├── tsconfig.json
57+
└── build.config.ts
58+
```
59+
60+
## Testing
61+
62+
Unit tests use `PluginTestDriver` from `@ts-safeql/plugin-utils/testing`:
63+
64+
```ts
65+
import { PluginTestDriver } from "@ts-safeql/plugin-utils/testing";
66+
import plugin from "./plugin";
67+
68+
const driver = new PluginTestDriver({ plugin: plugin.factory({}) });
69+
const result = driver.toSQL(`import { sql } from "my-lib"; sql.unsafe\`SELECT 1\``);
70+
expect(result).toEqual({ sql: "SELECT 1" });
71+
```
72+
73+
Integration tests use `@typescript-eslint/rule-tester` with a real database.

.agents/docs/coding-guidelines.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Coding Guidelines
2+
3+
No headline comments. Don't use comments as section dividers (e.g., `// ---- Helpers ----`). If a file needs sections, it needs separate files.
4+
5+
No comments that explain what code does. If a comment restates the code below it, the code isn't clear enough. Rename, extract, or restructure instead. A comment like `// Check if the user is valid` above `if (isValidUser(u))` is noise. Comments should explain why, not what.
6+
7+
Exception: `// ARRANGE`, `// ACT`, `// ASSERT` comments in tests are allowed.
8+
9+
Avoid using `any`, `as unknown as`, or any unsafe casts. Use type inference and type guards instead.

.agents/docs/creating-package.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Creating a New Package
2+
3+
1. Create the directory under `packages/` (or `packages/plugins/` for plugins)
4+
2. Add `package.json` — mirror an existing package for the `name`, `exports`, `scripts`, and `type` fields
5+
3. Add `tsconfig.json` extending the root `tsconfig.node.json` (adjust the relative path based on depth)
6+
4. Add `build.config.ts` — copy from an existing package
7+
5. Run `pnpm install`
8+
6. Workspace glob `packages/plugins/*` is already in `pnpm-workspace.yaml`

.changeset/plugin-system.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@ts-safeql/eslint-plugin": minor
3+
"@ts-safeql/shared": minor
4+
"@ts-safeql/plugin-utils": minor
5+
"@ts-safeql/plugin-auth-aws": minor
6+
---
7+
8+
Add experimental plugin API and AWS auth plugin
9+
10+
- Experimental plugin system for extending SafeQL with custom behavior
11+
- First official plugin: `@ts-safeql/plugin-auth-aws` for AWS RDS IAM authentication

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ node_modules
22
.turbo
33
*.tsbuildinfo
44
dist
5-
generated
5+
generated
6+
.env
7+
.env.local

AGENTS.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Agent Guidelines
2+
3+
## Common Commands
4+
5+
```sh
6+
pnpm build # production build (via turbo)
7+
pnpm dev # stub all packages for local dev
8+
pnpm test # run all tests (via turbo)
9+
pnpm --filter <pkg> test -- --run # single package tests
10+
```
11+
12+
## Gotchas
13+
14+
- Never run `pnpm --filter <pkg> build`. Instead, always use `pnpm build` (which handles dependency ordering and output formats correctly).
15+
- For local dev, use `pnpm dev` (stubs via `unbuild --stub`, bypasses rollup entirely).
16+
- A local PostgreSQL instance is expected at `postgres://postgres:postgres@localhost:5432/postgres`.
17+
18+
## References
19+
- [Creating a New Package](.agents/docs/creating-package.md)
20+
- [Authoring a Plugin](.agents/docs/authoring-plugin.md)

demos/plugin-aws-iam/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
SAFEQL_AWS_PROFILE=my-profile
2+
SAFEQL_AWS_REGION=eu-west-1
3+
SAFEQL_DATABASE_HOST=<instance>.<id>.<region>.rds.amazonaws.com
4+
SAFEQL_DATABASE_NAME=mydb
5+
SAFEQL_DATABASE_PORT=5432
6+
SAFEQL_DATABASE_USER=iam_user
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// @ts-check
2+
3+
import "dotenv/config";
4+
import safeql from "@ts-safeql/eslint-plugin/config";
5+
import awsIamAuth from "@ts-safeql/plugin-auth-aws";
6+
import tseslint from "typescript-eslint";
7+
8+
export default tseslint.config({
9+
files: ["src/**/*.ts"],
10+
languageOptions: {
11+
parser: tseslint.parser,
12+
parserOptions: {
13+
projectService: true,
14+
},
15+
},
16+
extends: [
17+
safeql.configs.connections({
18+
plugins: [
19+
awsIamAuth({
20+
databaseHost: process.env.SAFEQL_DATABASE_HOST ?? "",
21+
databasePort: Number(process.env.SAFEQL_DATABASE_PORT ?? 5432),
22+
databaseUser: process.env.SAFEQL_DATABASE_USER ?? "",
23+
databaseName: process.env.SAFEQL_DATABASE_NAME ?? "",
24+
awsRegion: process.env.SAFEQL_AWS_REGION ?? "",
25+
awsProfile: process.env.SAFEQL_AWS_PROFILE,
26+
}),
27+
],
28+
targets: [{ tag: "sql", transform: "{type}[]" }],
29+
}),
30+
],
31+
});

demos/plugin-aws-iam/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@ts-safeql-demos/plugin-aws-iam",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "tsc",
8+
"lint": "bash scripts/verify-plugin-error.sh",
9+
"lint:live": "eslint src",
10+
"lint!": "eslint src --fix"
11+
},
12+
"devDependencies": {
13+
"@eslint/js": "^9.23.0",
14+
"@types/node": "^22.13.13",
15+
"dotenv": "^16.4.7",
16+
"eslint": "^9.23.0",
17+
"typescript": "^5.8.2",
18+
"typescript-eslint": "^8.28.0"
19+
},
20+
"dependencies": {
21+
"@ts-safeql/plugin-auth-aws": "workspace:*",
22+
"@ts-safeql/eslint-plugin": "workspace:*",
23+
"postgres": "^3.4.5"
24+
}
25+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Simulate a CI environment with no AWS credentials or config.
5+
export AWS_CONFIG_FILE=/dev/null
6+
export AWS_SHARED_CREDENTIALS_FILE=/dev/null
7+
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE 2>/dev/null || true
8+
9+
output=$(pnpm exec eslint src 2>&1 || true)
10+
11+
echo "$output"
12+
13+
echo "$output" | grep -q '\[safeql-plugin-aws-iam\]' || { echo "FAIL: plugin error prefix missing"; exit 1; }
14+
! echo "$output" | grep -q 'Internal error' || { echo "FAIL: got Internal error"; exit 1; }
15+
! echo "$output" | grep -q 'could not be loaded' || { echo "FAIL: plugin failed to load"; exit 1; }
16+
17+
echo "OK: plugin loaded, failed with expected plugin error"

0 commit comments

Comments
 (0)