sqlstack 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/dist/adapters/index.d.ts +4 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mysql.d.ts +3 -0
- package/dist/adapters/mysql.d.ts.map +1 -0
- package/dist/adapters/mysql.js +21 -0
- package/dist/adapters/mysql.js.map +1 -0
- package/dist/adapters/pg.d.ts +3 -0
- package/dist/adapters/pg.d.ts.map +1 -0
- package/dist/adapters/pg.js +20 -0
- package/dist/adapters/pg.js.map +1 -0
- package/dist/adapters/sqlite.d.ts +3 -0
- package/dist/adapters/sqlite.d.ts.map +1 -0
- package/dist/adapters/sqlite.js +64 -0
- package/dist/adapters/sqlite.js.map +1 -0
- package/dist/adapters/test.d.ts +3 -0
- package/dist/adapters/test.d.ts.map +1 -0
- package/dist/adapters/test.js +47 -0
- package/dist/adapters/test.js.map +1 -0
- package/dist/binders/sqlBinder.d.ts +14 -0
- package/dist/binders/sqlBinder.d.ts.map +1 -0
- package/dist/binders/sqlBinder.js +189 -0
- package/dist/binders/sqlBinder.js.map +1 -0
- package/dist/core/errors.d.ts +13 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +32 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/fileResolution.d.ts +11 -0
- package/dist/core/fileResolution.d.ts.map +1 -0
- package/dist/core/fileResolution.js +82 -0
- package/dist/core/fileResolution.js.map +1 -0
- package/dist/core/metadata.d.ts +26 -0
- package/dist/core/metadata.d.ts.map +1 -0
- package/dist/core/metadata.js +31 -0
- package/dist/core/metadata.js.map +1 -0
- package/dist/core/validator.d.ts +8 -0
- package/dist/core/validator.d.ts.map +1 -0
- package/dist/core/validator.js +66 -0
- package/dist/core/validator.js.map +1 -0
- package/dist/decorators/defaults.d.ts +2 -0
- package/dist/decorators/defaults.d.ts.map +1 -0
- package/dist/decorators/defaults.js +10 -0
- package/dist/decorators/defaults.js.map +1 -0
- package/dist/decorators/query.d.ts +8 -0
- package/dist/decorators/query.d.ts.map +1 -0
- package/dist/decorators/query.js +101 -0
- package/dist/decorators/query.js.map +1 -0
- package/dist/decorators/queryBinder.d.ts +4 -0
- package/dist/decorators/queryBinder.d.ts.map +1 -0
- package/dist/decorators/queryBinder.js +10 -0
- package/dist/decorators/queryBinder.js.map +1 -0
- package/dist/decorators/resultShape.d.ts +5 -0
- package/dist/decorators/resultShape.d.ts.map +1 -0
- package/dist/decorators/resultShape.js +20 -0
- package/dist/decorators/resultShape.js.map +1 -0
- package/dist/decorators/validateResult.d.ts +2 -0
- package/dist/decorators/validateResult.d.ts.map +1 -0
- package/dist/decorators/validateResult.js +11 -0
- package/dist/decorators/validateResult.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +16 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +29 -0
- package/dist/registry.js.map +1 -0
- package/package.json +86 -0
- package/readme.md +376 -0
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sqlstack",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"require": "./dist/index.js",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./registry": {
|
|
20
|
+
"types": "./dist/registry.d.ts",
|
|
21
|
+
"require": "./dist/registry.js",
|
|
22
|
+
"import": "./dist/registry.js"
|
|
23
|
+
},
|
|
24
|
+
"./adapters": {
|
|
25
|
+
"types": "./dist/adapters/index.d.ts",
|
|
26
|
+
"require": "./dist/adapters/index.js",
|
|
27
|
+
"import": "./dist/adapters/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./errors": {
|
|
30
|
+
"types": "./dist/core/errors.d.ts",
|
|
31
|
+
"require": "./dist/core/errors.js",
|
|
32
|
+
"import": "./dist/core/errors.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.json",
|
|
37
|
+
"test": "jest",
|
|
38
|
+
"test:watch": "jest --watch",
|
|
39
|
+
"clean": "rimraf dist",
|
|
40
|
+
"test:integration": "jest -c jest.integration.config.ts",
|
|
41
|
+
"db:pg:up": "docker compose -f docker/docker-compose.pg.yml up -d",
|
|
42
|
+
"db:pg:down": "docker compose -f docker/docker-compose.pg.yml down -v",
|
|
43
|
+
"db:mysql:up": "docker compose -f docker/docker-compose.mysql.yml up -d",
|
|
44
|
+
"db:mysql:down": "docker compose -f docker/docker-compose.mysql.yml down -v",
|
|
45
|
+
"db:ps": "docker ps --filter name=sqlstack-",
|
|
46
|
+
"db:logs:pg": "docker logs -f sqlstack-postgres",
|
|
47
|
+
"db:logs:mysql": "docker logs -f sqlstack-mysql"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/jest": "^29.5.13",
|
|
51
|
+
"jest": "^29.7.0",
|
|
52
|
+
"rimraf": "^5.0.5",
|
|
53
|
+
"ts-jest": "^29.2.5",
|
|
54
|
+
"ts-node": "^10.9.2",
|
|
55
|
+
"typescript": "^5.6.2"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"ajv": "^8.17.1",
|
|
59
|
+
"ajv-formats": "^2.1.1",
|
|
60
|
+
"reflect-metadata": "^0.1.13",
|
|
61
|
+
"sqlite": "^5.1.1"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"better-sqlite3": "^9.6.0",
|
|
65
|
+
"mysql2": "^3.14.4",
|
|
66
|
+
"pg": "^8.16.3",
|
|
67
|
+
"sqlite3": "^5.1.7"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"better-sqlite3": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"sqlite3": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"mysql2": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"pg": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=20 <23"
|
|
85
|
+
}
|
|
86
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# sqlstack
|
|
2
|
+
|
|
3
|
+
SQL-first data access for Node.js and TypeScript.
|
|
4
|
+
|
|
5
|
+
Write real SQL, keep it next to your code, and use small, composable decorators to add behavior like defaults, single-row results, and result validation — without ORM complexity.
|
|
6
|
+
|
|
7
|
+
- Real SQL in `.sql` files
|
|
8
|
+
- TypeScript-first: declare args/return types yourself, or plug in validation/typegen
|
|
9
|
+
- Composable decorators: `@Single`, `@Only`, `@Exist`, `@Defaults`, `@ValidateResult` (order-independent)
|
|
10
|
+
- Minimal runtime: a thin executor layer on top of your DB driver
|
|
11
|
+
- Works with Postgres, MySQL/MariaDB, SQLite (more adapters coming)
|
|
12
|
+
- Structured errors via `SqlStackError` base class
|
|
13
|
+
- Configurable connections via a simple registry (per-class and per-method)
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install sqlstack
|
|
19
|
+
# or
|
|
20
|
+
yarn add sqlstack
|
|
21
|
+
# or
|
|
22
|
+
pnpm add sqlstack
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Install a database driver as needed:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install pg # Postgres
|
|
29
|
+
npm install mysql2 # MySQL / MariaDB
|
|
30
|
+
npm install better-sqlite3 # SQLite
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configure Databases
|
|
34
|
+
|
|
35
|
+
Register your database connections once at application startup and set a default.
|
|
36
|
+
|
|
37
|
+
boot.ts
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// boot.ts — register your connections
|
|
41
|
+
import { SqlStackDB } from "sqlstack/registry";
|
|
42
|
+
import { createPgDb, createMysqlDb, createSqliteDb } from "sqlstack/adapters";
|
|
43
|
+
|
|
44
|
+
SqlStackDB
|
|
45
|
+
.register("primary", createPgDb(process.env.PG_URL!))
|
|
46
|
+
.register("analytics", createMysqlDb(process.env.MYSQL_URL!))
|
|
47
|
+
.register("test", createSqliteDb(":memory:"))
|
|
48
|
+
.setDefault("primary");
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then, choose a database per repository (class-level default):
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { QueryBinder, Query, SqlStackError } from "sqlstack";
|
|
55
|
+
|
|
56
|
+
@QueryBinder({ db: "analytics" }) // class default
|
|
57
|
+
export class ReportsRepo {
|
|
58
|
+
// Uses "analytics" unless overridden below
|
|
59
|
+
@Query()
|
|
60
|
+
async topPages(_a: { since: string }): Promise<Row[]> {
|
|
61
|
+
throw new SqlStackError("replaced");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Load `boot.ts` once during app startup (for example, in your server entry).
|
|
67
|
+
|
|
68
|
+
Per-method override (use a different DB for a specific query):
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { QueryBinder, Query, SqlStackError } from "sqlstack";
|
|
72
|
+
|
|
73
|
+
@QueryBinder({ db: "primary" }) // default for this class
|
|
74
|
+
export class MixedRepo {
|
|
75
|
+
@Query() // uses "primary"
|
|
76
|
+
async dailySummary(_a: { date: string }): Promise<Row[]> {
|
|
77
|
+
throw new SqlStackError("replaced");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Query({ db: "analytics" }) // override to use "analytics"
|
|
81
|
+
async topPages(_a: { since: string }): Promise<Row[]> {
|
|
82
|
+
throw new SqlStackError("replaced");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quick Start
|
|
88
|
+
|
|
89
|
+
1) Create a repository class
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { QueryBinder, Query, SqlStackError } from "sqlstack";
|
|
93
|
+
|
|
94
|
+
@QueryBinder() // looks for .sql files in the same folder by default
|
|
95
|
+
export class UsersRepo {
|
|
96
|
+
@Query()
|
|
97
|
+
async findByEmail(_a: { email: string }): Promise<User[]> {
|
|
98
|
+
throw new SqlStackError("replaced by @Query");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
2) Add the SQL file next to your class
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
users/
|
|
107
|
+
index.ts
|
|
108
|
+
findByEmail.sql
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
findByEmail.sql
|
|
112
|
+
|
|
113
|
+
```sql
|
|
114
|
+
SELECT id, email, name, status, created_at
|
|
115
|
+
FROM users
|
|
116
|
+
WHERE email = :email;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
3) Call your repository
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const repo = new UsersRepo();
|
|
123
|
+
const rows = await repo.findByEmail({ email: "alice@example.com" });
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## SQL Parameters
|
|
127
|
+
|
|
128
|
+
Write parameters directly in your `.sql` files and pass values from your method call.
|
|
129
|
+
|
|
130
|
+
### Named parameters
|
|
131
|
+
|
|
132
|
+
Use `:name` placeholders that map to properties of a single object argument.
|
|
133
|
+
|
|
134
|
+
```sql
|
|
135
|
+
-- findByEmail.sql
|
|
136
|
+
SELECT id, email, name, status
|
|
137
|
+
FROM users
|
|
138
|
+
WHERE email = :email AND status = :status;
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
await repo.findByEmail({ email: "alice@example.com", status: "active" });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Positional parameters
|
|
146
|
+
|
|
147
|
+
Use `:arg1`, `:arg2`, … and pass values by position. Indexing starts at 1.
|
|
148
|
+
|
|
149
|
+
```sql
|
|
150
|
+
-- findById.sql
|
|
151
|
+
SELECT *
|
|
152
|
+
FROM users
|
|
153
|
+
WHERE id = :arg1 AND org_id = :arg2;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
await repo.findById("user-123", "org-456");
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Use either named or positional parameters within a single query (do not mix both).
|
|
161
|
+
|
|
162
|
+
### Array parameters
|
|
163
|
+
|
|
164
|
+
Pass arrays to expand into `IN (...)` lists.
|
|
165
|
+
|
|
166
|
+
```sql
|
|
167
|
+
-- listByIds.sql
|
|
168
|
+
SELECT *
|
|
169
|
+
FROM users
|
|
170
|
+
WHERE id IN (:ids);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
await repo.listByIds({ ids: ["user-1", "user-2", "user-3"] });
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Note: For empty arrays, prefer an early return in your code (e.g., `if (!ids.length) return [];`) to avoid generating invalid SQL.
|
|
178
|
+
|
|
179
|
+
### Null and undefined
|
|
180
|
+
|
|
181
|
+
- `null` maps to SQL `NULL`.
|
|
182
|
+
- `undefined` is not allowed and will throw; provide a value or omit the parameter entirely.
|
|
183
|
+
|
|
184
|
+
## SQL File Resolution
|
|
185
|
+
|
|
186
|
+
`@Query()` loads SQL from files based on a simple, predictable rule:
|
|
187
|
+
|
|
188
|
+
- No argument: resolves from the declaring file’s folder.
|
|
189
|
+
- File path: resolves from `dirname(filePath)`.
|
|
190
|
+
- Folder path: resolves directly from that folder.
|
|
191
|
+
|
|
192
|
+
For each method name, sqlstack looks for `<method>.<dialect>.sql` first, then `<method>.sql`. Dialect suffixes: `.pg.sql`, `.mysql.sql`, `.sqlite.sql`. Resolution and file contents are cached. See docs/file_resolution.md for details.
|
|
193
|
+
|
|
194
|
+
## Decorators
|
|
195
|
+
|
|
196
|
+
### `@Defaults`
|
|
197
|
+
|
|
198
|
+
Provide default parameter values.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { QueryBinder, Defaults, Query, SqlStackError } from "sqlstack";
|
|
202
|
+
|
|
203
|
+
@QueryBinder()
|
|
204
|
+
class UsersRepo {
|
|
205
|
+
@Defaults({ status: "active", limit: 10 })
|
|
206
|
+
@Query()
|
|
207
|
+
async listUsers(_a: { status?: string; limit?: number }): Promise<User[]> {
|
|
208
|
+
throw new SqlStackError();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
- Calling `repo.listUsers({})` fills `{ status: "active", limit: 10 }`.
|
|
214
|
+
|
|
215
|
+
### Result shape decorators
|
|
216
|
+
|
|
217
|
+
Choose a decorator to shape row results without parameters.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { QueryBinder, Single, Only, Exist, None, Query, SqlStackError } from "sqlstack";
|
|
221
|
+
|
|
222
|
+
@QueryBinder()
|
|
223
|
+
class UsersRepo {
|
|
224
|
+
// @Single → return the first row or null
|
|
225
|
+
@Single
|
|
226
|
+
@Query()
|
|
227
|
+
async firstOrNull(_a: { email: string }): Promise<User | null> {
|
|
228
|
+
throw new SqlStackError();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// @Only → require exactly one row; throw if 0 or >1
|
|
232
|
+
@Only
|
|
233
|
+
@Query()
|
|
234
|
+
async exactlyOne(_a: { id: string }): Promise<User> {
|
|
235
|
+
throw new SqlStackError();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// @Exist → require at least one row; return first row; throw if none
|
|
239
|
+
@Exist
|
|
240
|
+
@Query()
|
|
241
|
+
async requireOne(_a: { email: string }): Promise<User> {
|
|
242
|
+
throw new SqlStackError();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// @None → require zero rows; return null; throw if any rows
|
|
246
|
+
@None
|
|
247
|
+
@Query()
|
|
248
|
+
async ensureNone(_a: { status: string }): Promise<null> {
|
|
249
|
+
throw new SqlStackError();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### `@ValidateResult`
|
|
255
|
+
|
|
256
|
+
Validate query results with JSON Schema.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import schema from "./user.row.schema.json" assert { type: "json" };
|
|
260
|
+
import { QueryBinder, ValidateResult, Single, Query, SqlStackError } from "sqlstack";
|
|
261
|
+
|
|
262
|
+
@QueryBinder()
|
|
263
|
+
class UsersRepo {
|
|
264
|
+
@ValidateResult(schema)
|
|
265
|
+
@Single
|
|
266
|
+
@Query()
|
|
267
|
+
async findByEmail(_a: { email: string }): Promise<User | null> {
|
|
268
|
+
throw new SqlStackError();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
## Inline Builders
|
|
276
|
+
|
|
277
|
+
Intentionally not included. sqlstack prioritizes explicit `.sql` files for clarity and control.
|
|
278
|
+
|
|
279
|
+
## JSON Schema
|
|
280
|
+
|
|
281
|
+
Add a JSON Schema file and attach it to a method with `@ValidateResult`.
|
|
282
|
+
|
|
283
|
+
user.row.schema.json
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"type": "object",
|
|
288
|
+
"properties": {
|
|
289
|
+
"id": { "type": "string" },
|
|
290
|
+
"email": { "type": "string", "format": "email" },
|
|
291
|
+
"status": { "enum": ["active", "inactive"] }
|
|
292
|
+
},
|
|
293
|
+
"required": ["id", "email", "status"],
|
|
294
|
+
"additionalProperties": false
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Usage
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import schema from "./user.row.schema.json" assert { type: "json" };
|
|
302
|
+
import { QueryBinder, ValidateResult, Query } from "sqlstack";
|
|
303
|
+
|
|
304
|
+
@QueryBinder()
|
|
305
|
+
class UsersRepo {
|
|
306
|
+
@ValidateResult(schema)
|
|
307
|
+
@Query()
|
|
308
|
+
async listUsers(): Promise<User[]> {
|
|
309
|
+
// executes and validates each row against the schema
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Errors
|
|
315
|
+
|
|
316
|
+
sqlstack provides a base error and specific error types you can import and catch.
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { SqlStackError } from "sqlstack";
|
|
320
|
+
import { ExactlyOneRowError, NoRowsError, ValidationError } from "sqlstack/errors";
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
// call repository methods
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (err instanceof ExactlyOneRowError) {
|
|
326
|
+
// @Only: expected exactly one row
|
|
327
|
+
}
|
|
328
|
+
if (err instanceof NoRowsError) {
|
|
329
|
+
// @Exist: expected at least one row
|
|
330
|
+
}
|
|
331
|
+
if (err instanceof ValidationError) {
|
|
332
|
+
// @ValidateResult: schema mismatch
|
|
333
|
+
}
|
|
334
|
+
if (err instanceof SqlStackError) {
|
|
335
|
+
// handle sqlstack-specific failures
|
|
336
|
+
}
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
## Philosophy
|
|
343
|
+
|
|
344
|
+
- SQL-first: SQL is the source of truth. No hidden queries.
|
|
345
|
+
- Decorators as metadata: `@Defaults`, `@Single`, `@Only`, `@Exist`, `@ValidateResult` add labels; execution order is fixed internally, so decorator order doesn’t matter.
|
|
346
|
+
- Escape hatches: Use inline builders for simple CRUD; keep complex SQL in `.sql` files. You stay in control.
|
|
347
|
+
|
|
348
|
+
### Execution Pipeline
|
|
349
|
+
|
|
350
|
+
At runtime, decorators execute in a fixed, predictable order regardless of source order:
|
|
351
|
+
|
|
352
|
+
1) Defaults → 2) Query → 3) Single/Only/Exist → 4) ValidateResult. See docs/decorators.md for the model and examples.
|
|
353
|
+
|
|
354
|
+
### Validation Controls
|
|
355
|
+
|
|
356
|
+
- Default: validation is enabled.
|
|
357
|
+
- Env toggles (precedence):
|
|
358
|
+
- `SQLSTACK_ENABLE_VALIDATION` truthy → force ON (even in production)
|
|
359
|
+
- else if `SQLSTACK_DISABLE_VALIDATION` truthy → force OFF
|
|
360
|
+
- else → ON
|
|
361
|
+
|
|
362
|
+
## Database Support
|
|
363
|
+
|
|
364
|
+
- Postgres (`pg`)
|
|
365
|
+
- MySQL / MariaDB (`mysql2`)
|
|
366
|
+
- SQLite (`better-sqlite3`)
|
|
367
|
+
|
|
368
|
+
More adapters are planned.
|
|
369
|
+
|
|
370
|
+
### Dialect Inference
|
|
371
|
+
|
|
372
|
+
The binder placeholder style is inferred from the selected database (method → class → registry default). Only override via `@Query({ dialect })` if you need to force a specific dialect.
|
|
373
|
+
|
|
374
|
+
## License
|
|
375
|
+
|
|
376
|
+
ISC
|