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.
Files changed (70) hide show
  1. package/dist/adapters/index.d.ts +4 -0
  2. package/dist/adapters/index.d.ts.map +1 -0
  3. package/dist/adapters/index.js +10 -0
  4. package/dist/adapters/index.js.map +1 -0
  5. package/dist/adapters/mysql.d.ts +3 -0
  6. package/dist/adapters/mysql.d.ts.map +1 -0
  7. package/dist/adapters/mysql.js +21 -0
  8. package/dist/adapters/mysql.js.map +1 -0
  9. package/dist/adapters/pg.d.ts +3 -0
  10. package/dist/adapters/pg.d.ts.map +1 -0
  11. package/dist/adapters/pg.js +20 -0
  12. package/dist/adapters/pg.js.map +1 -0
  13. package/dist/adapters/sqlite.d.ts +3 -0
  14. package/dist/adapters/sqlite.d.ts.map +1 -0
  15. package/dist/adapters/sqlite.js +64 -0
  16. package/dist/adapters/sqlite.js.map +1 -0
  17. package/dist/adapters/test.d.ts +3 -0
  18. package/dist/adapters/test.d.ts.map +1 -0
  19. package/dist/adapters/test.js +47 -0
  20. package/dist/adapters/test.js.map +1 -0
  21. package/dist/binders/sqlBinder.d.ts +14 -0
  22. package/dist/binders/sqlBinder.d.ts.map +1 -0
  23. package/dist/binders/sqlBinder.js +189 -0
  24. package/dist/binders/sqlBinder.js.map +1 -0
  25. package/dist/core/errors.d.ts +13 -0
  26. package/dist/core/errors.d.ts.map +1 -0
  27. package/dist/core/errors.js +32 -0
  28. package/dist/core/errors.js.map +1 -0
  29. package/dist/core/fileResolution.d.ts +11 -0
  30. package/dist/core/fileResolution.d.ts.map +1 -0
  31. package/dist/core/fileResolution.js +82 -0
  32. package/dist/core/fileResolution.js.map +1 -0
  33. package/dist/core/metadata.d.ts +26 -0
  34. package/dist/core/metadata.d.ts.map +1 -0
  35. package/dist/core/metadata.js +31 -0
  36. package/dist/core/metadata.js.map +1 -0
  37. package/dist/core/validator.d.ts +8 -0
  38. package/dist/core/validator.d.ts.map +1 -0
  39. package/dist/core/validator.js +66 -0
  40. package/dist/core/validator.js.map +1 -0
  41. package/dist/decorators/defaults.d.ts +2 -0
  42. package/dist/decorators/defaults.d.ts.map +1 -0
  43. package/dist/decorators/defaults.js +10 -0
  44. package/dist/decorators/defaults.js.map +1 -0
  45. package/dist/decorators/query.d.ts +8 -0
  46. package/dist/decorators/query.d.ts.map +1 -0
  47. package/dist/decorators/query.js +101 -0
  48. package/dist/decorators/query.js.map +1 -0
  49. package/dist/decorators/queryBinder.d.ts +4 -0
  50. package/dist/decorators/queryBinder.d.ts.map +1 -0
  51. package/dist/decorators/queryBinder.js +10 -0
  52. package/dist/decorators/queryBinder.js.map +1 -0
  53. package/dist/decorators/resultShape.d.ts +5 -0
  54. package/dist/decorators/resultShape.d.ts.map +1 -0
  55. package/dist/decorators/resultShape.js +20 -0
  56. package/dist/decorators/resultShape.js.map +1 -0
  57. package/dist/decorators/validateResult.d.ts +2 -0
  58. package/dist/decorators/validateResult.d.ts.map +1 -0
  59. package/dist/decorators/validateResult.js +11 -0
  60. package/dist/decorators/validateResult.js.map +1 -0
  61. package/dist/index.d.ts +11 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +27 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/registry.d.ts +16 -0
  66. package/dist/registry.d.ts.map +1 -0
  67. package/dist/registry.js +29 -0
  68. package/dist/registry.js.map +1 -0
  69. package/package.json +86 -0
  70. 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