@shyk/kadak 0.1.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 +90 -0
- package/dist/index.cjs +548 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +373 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +373 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +516 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shyam Choudhary
|
|
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,90 @@
|
|
|
1
|
+
# Kadak ☕️
|
|
2
|
+
|
|
3
|
+
**The strongest TypeScript ORM for PostgreSQL. Period.**
|
|
4
|
+
|
|
5
|
+
Kadak (pronounced *Ka-dak*) is built for developers who find Drizzle too verbose and Prisma too heavy. It's a thin, type-safe layer over PostgreSQL that prioritizes **Developer Experience (DX)** above everything else.
|
|
6
|
+
|
|
7
|
+
[kadak.shyk.in](https://kadak.shyk.in) | [Documentation](https://kadak.shyk.in/docs) | [npm](https://www.npmjs.com/package/kadak)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why Kadak?
|
|
12
|
+
|
|
13
|
+
Drizzle is great, but it has friction. Drizzle makes you repeat yourself (`'id', serial('id')`), forces you to think about nullable-by-default (silently causing bugs), and leaves you figuring out Zod validation yourself.
|
|
14
|
+
|
|
15
|
+
**Kadak solves this.**
|
|
16
|
+
|
|
17
|
+
* **Required by Default**: Columns are NOT NULL unless you explicitly say `.optional()`.
|
|
18
|
+
* **Zero Repetition**: No more writing column names twice.
|
|
19
|
+
* **Built-in Zod**: Table schemas auto-generate Zod validators for Insert, Update, and Select.
|
|
20
|
+
* **Modern PostgreSQL**: Defaults to `IDENTITY` columns (modern standard) instead of legacy `SERIAL`.
|
|
21
|
+
* **Automatic Mapping**: Write `camelCase` in TS, get `snake_case` in PG. No mapping manual effort.
|
|
22
|
+
* **Soft Deletes & AutoUpdate**: First-class support for `deleted_at` and `updated_at` patterns.
|
|
23
|
+
* **Lightweight**: Only two runtime dependencies: `postgres.js` and `zod`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Getting Started
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm add @shyk/kadak postgres zod
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 1. Define your Schema
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { table, kadak, type Infer } from '@shyk/kadak'
|
|
37
|
+
|
|
38
|
+
export const users = table('users', {
|
|
39
|
+
id: kadak.id(), // IDENTITY Primary Key
|
|
40
|
+
name: kadak.text().required().max(100),
|
|
41
|
+
email: kadak.email().unique(),
|
|
42
|
+
age: kadak.int().optional(),
|
|
43
|
+
balance: kadak.decimal(), // Precision safe (returns string)
|
|
44
|
+
createdAt: kadak.timestamp().default('now'),
|
|
45
|
+
updatedAt: kadak.timestamp().autoUpdate(),
|
|
46
|
+
deletedAt: kadak.timestamp().softDelete(),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export type User = Infer<typeof users>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Connect and Query
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { connect } from '@shyk/kadak'
|
|
56
|
+
import { users } from './schema'
|
|
57
|
+
|
|
58
|
+
const db = connect(process.env.DATABASE_URL!, { users })
|
|
59
|
+
|
|
60
|
+
// Type-safe queries with automatic snake_case mapping
|
|
61
|
+
const allUsers = await db.users.findMany({
|
|
62
|
+
where: { isActive: true },
|
|
63
|
+
orderBy: { createdAt: 'desc' },
|
|
64
|
+
limit: 10
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Validation included
|
|
68
|
+
const result = users.validator().safeParse(somePayload)
|
|
69
|
+
if (result.success) {
|
|
70
|
+
await db.users.insert(result.data)
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Features
|
|
77
|
+
|
|
78
|
+
| Feature | Kadak | Drizzle |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| **Default Context** | Required (Safe) | Nullable (Prone to bugs) |
|
|
81
|
+
| **Column Naming** | Auto `snake_case` | Manual `name('name')` |
|
|
82
|
+
| **Validation** | First-class Zod | Separate packages |
|
|
83
|
+
| **Soft Delete** | Native `.softDelete()` | Manual WHERE clauses |
|
|
84
|
+
| **Primary Keys** | `IDENTITY` (Modern) | `SERIAL` (Legacy) |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT © [Shyam](https://github.com/shyk)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let zod = require("zod");
|
|
25
|
+
let postgres = require("postgres");
|
|
26
|
+
postgres = __toESM(postgres);
|
|
27
|
+
//#region src/schema.ts
|
|
28
|
+
/** Converts camelCase to snake_case for PostgreSQL column naming */
|
|
29
|
+
function camelToSnake(str) {
|
|
30
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A column definition carrying runtime metadata AND compile-time types.
|
|
34
|
+
*
|
|
35
|
+
* Three generics flow through the entire ORM:
|
|
36
|
+
* - TType: JS type (string, number, boolean, Date)
|
|
37
|
+
* - TNullable: whether NULL is allowed (adds `| null`)
|
|
38
|
+
* - THasDefault: whether INSERT can omit this (adds `?`)
|
|
39
|
+
*
|
|
40
|
+
* Every modifier returns a NEW Column — immutable builder.
|
|
41
|
+
*/
|
|
42
|
+
var Column = class Column {
|
|
43
|
+
constructor(config) {
|
|
44
|
+
this._config = Object.freeze({ ...config });
|
|
45
|
+
}
|
|
46
|
+
/** NOT NULL — already the default. Exists for explicit readability. */
|
|
47
|
+
required() {
|
|
48
|
+
return new Column({
|
|
49
|
+
...this._config,
|
|
50
|
+
nullable: false
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Allow NULL — column type becomes TType | null in TypeScript */
|
|
54
|
+
optional() {
|
|
55
|
+
return new Column({
|
|
56
|
+
...this._config,
|
|
57
|
+
nullable: true
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Add UNIQUE constraint */
|
|
61
|
+
unique() {
|
|
62
|
+
return new Column({
|
|
63
|
+
...this._config,
|
|
64
|
+
isUnique: true
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Mark as PRIMARY KEY */
|
|
68
|
+
primaryKey() {
|
|
69
|
+
return new Column({
|
|
70
|
+
...this._config,
|
|
71
|
+
isPrimaryKey: true
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Set default value — makes field optional in INSERT.
|
|
76
|
+
*
|
|
77
|
+
* Named shortcuts are resolved per column type to avoid ambiguity:
|
|
78
|
+
* - 'now' on TIMESTAMPTZ → SQL DEFAULT now()
|
|
79
|
+
* - 'uuid' on UUID → SQL DEFAULT gen_random_uuid()
|
|
80
|
+
* On other types, these strings are treated as literal values.
|
|
81
|
+
*/
|
|
82
|
+
default(value) {
|
|
83
|
+
const shortcuts = {
|
|
84
|
+
TIMESTAMPTZ: { now: "now()" },
|
|
85
|
+
UUID: { uuid: "gen_random_uuid()" }
|
|
86
|
+
}[this._config.pgType] ?? {};
|
|
87
|
+
const sqlExpr = typeof value === "string" ? shortcuts[value] : void 0;
|
|
88
|
+
return new Column({
|
|
89
|
+
...this._config,
|
|
90
|
+
hasDefault: true,
|
|
91
|
+
defaultValue: sqlExpr ? void 0 : value,
|
|
92
|
+
defaultSql: sqlExpr ?? this._config.defaultSql
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Min constraint — string length for text, numeric value for numbers */
|
|
96
|
+
min(n) {
|
|
97
|
+
return new Column({
|
|
98
|
+
...this._config,
|
|
99
|
+
min: n
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Max constraint — string length for text, numeric value for numbers */
|
|
103
|
+
max(n) {
|
|
104
|
+
return new Column({
|
|
105
|
+
...this._config,
|
|
106
|
+
max: n
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Auto-update on every UPDATE query — designed for updatedAt columns */
|
|
110
|
+
autoUpdate() {
|
|
111
|
+
return new Column({
|
|
112
|
+
...this._config,
|
|
113
|
+
autoUpdate: true,
|
|
114
|
+
hasDefault: true
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Soft delete marker. When set on a column:
|
|
119
|
+
* - findMany() auto-adds WHERE column IS NULL
|
|
120
|
+
* - delete() sets this column to now() instead of DELETE
|
|
121
|
+
* - findMany({ withDeleted: true }) bypasses the filter
|
|
122
|
+
*/
|
|
123
|
+
softDelete() {
|
|
124
|
+
return new Column({
|
|
125
|
+
...this._config,
|
|
126
|
+
softDelete: true,
|
|
127
|
+
nullable: true,
|
|
128
|
+
hasDefault: true
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/** Base config with required-by-default semantics */
|
|
133
|
+
function baseConfig(pgType) {
|
|
134
|
+
return {
|
|
135
|
+
pgType,
|
|
136
|
+
nullable: false,
|
|
137
|
+
hasDefault: false,
|
|
138
|
+
isPrimaryKey: false,
|
|
139
|
+
isUnique: false,
|
|
140
|
+
isGenerated: false,
|
|
141
|
+
autoUpdate: false,
|
|
142
|
+
softDelete: false
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Column factory namespace. Every column definition starts here.
|
|
147
|
+
*
|
|
148
|
+
* ```ts
|
|
149
|
+
* const users = table('users', {
|
|
150
|
+
* id: kadak.id(),
|
|
151
|
+
* name: kadak.text().required(),
|
|
152
|
+
* email: kadak.email().unique(),
|
|
153
|
+
* })
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
const kadak = {
|
|
157
|
+
id: () => new Column({
|
|
158
|
+
...baseConfig("INTEGER"),
|
|
159
|
+
isPrimaryKey: true,
|
|
160
|
+
isGenerated: true,
|
|
161
|
+
hasDefault: true
|
|
162
|
+
}),
|
|
163
|
+
uuidId: () => new Column({
|
|
164
|
+
...baseConfig("UUID"),
|
|
165
|
+
isPrimaryKey: true,
|
|
166
|
+
hasDefault: true,
|
|
167
|
+
defaultSql: "gen_random_uuid()"
|
|
168
|
+
}),
|
|
169
|
+
serialId: () => new Column({
|
|
170
|
+
...baseConfig("SERIAL"),
|
|
171
|
+
isPrimaryKey: true,
|
|
172
|
+
isGenerated: true,
|
|
173
|
+
hasDefault: true
|
|
174
|
+
}),
|
|
175
|
+
text: () => new Column(baseConfig("TEXT")),
|
|
176
|
+
int: () => new Column(baseConfig("INTEGER")),
|
|
177
|
+
number: () => new Column(baseConfig("INTEGER")),
|
|
178
|
+
float: () => new Column(baseConfig("FLOAT8")),
|
|
179
|
+
decimal: () => new Column(baseConfig("NUMERIC")),
|
|
180
|
+
boolean: () => new Column(baseConfig("BOOLEAN")),
|
|
181
|
+
timestamp: () => new Column(baseConfig("TIMESTAMPTZ")),
|
|
182
|
+
email: () => new Column({
|
|
183
|
+
...baseConfig("TEXT"),
|
|
184
|
+
zodCheck: "email"
|
|
185
|
+
}),
|
|
186
|
+
uuid: () => new Column({
|
|
187
|
+
...baseConfig("UUID"),
|
|
188
|
+
zodCheck: "uuid"
|
|
189
|
+
}),
|
|
190
|
+
json: (schema) => new Column({
|
|
191
|
+
...baseConfig("JSONB"),
|
|
192
|
+
zodSchema: schema
|
|
193
|
+
})
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* A table definition. Created via the `table()` function.
|
|
197
|
+
* Holds column metadata and generates typed Zod validators.
|
|
198
|
+
*/
|
|
199
|
+
var Table = class {
|
|
200
|
+
constructor(name, columns, options) {
|
|
201
|
+
this._name = name;
|
|
202
|
+
this._columns = columns;
|
|
203
|
+
this._options = options;
|
|
204
|
+
this._columnMap = {};
|
|
205
|
+
for (const key of Object.keys(columns)) this._columnMap[key] = camelToSnake(key);
|
|
206
|
+
}
|
|
207
|
+
/** Alias for insertValidator() — the most common use case (validating user input) */
|
|
208
|
+
validator() {
|
|
209
|
+
return this.insertValidator();
|
|
210
|
+
}
|
|
211
|
+
/** Validates data for INSERT — generated/defaulted fields are optional */
|
|
212
|
+
insertValidator() {
|
|
213
|
+
return zod.z.object(this._buildShape("insert"));
|
|
214
|
+
}
|
|
215
|
+
/** Validates data for SELECT — all columns present, nullability applied */
|
|
216
|
+
selectValidator() {
|
|
217
|
+
return zod.z.object(this._buildShape("select"));
|
|
218
|
+
}
|
|
219
|
+
/** Validates data for UPDATE — everything optional (partial update) */
|
|
220
|
+
updateValidator() {
|
|
221
|
+
return zod.z.object(this._buildShape("update"));
|
|
222
|
+
}
|
|
223
|
+
/** @internal Builds Zod shape for a given operation mode */
|
|
224
|
+
_buildShape(mode) {
|
|
225
|
+
const shape = {};
|
|
226
|
+
for (const [key, col] of Object.entries(this._columns)) {
|
|
227
|
+
let zodType = this._baseZodType(col._config);
|
|
228
|
+
if (col._config.nullable) zodType = zodType.nullable();
|
|
229
|
+
if (mode === "insert" && (col._config.hasDefault || col._config.isGenerated)) zodType = zodType.optional();
|
|
230
|
+
else if (mode === "update") zodType = zodType.optional();
|
|
231
|
+
shape[key] = zodType;
|
|
232
|
+
}
|
|
233
|
+
return shape;
|
|
234
|
+
}
|
|
235
|
+
/** @internal Creates the base Zod type for a column, without nullable/optional */
|
|
236
|
+
_baseZodType(config) {
|
|
237
|
+
if (config.pgType === "JSONB" && config.zodSchema) return config.zodSchema;
|
|
238
|
+
switch (config.pgType) {
|
|
239
|
+
case "TEXT": {
|
|
240
|
+
let s = zod.z.string();
|
|
241
|
+
if (config.zodCheck === "email") s = s.email();
|
|
242
|
+
if (config.min !== void 0) s = s.min(config.min);
|
|
243
|
+
if (config.max !== void 0) s = s.max(config.max);
|
|
244
|
+
return s;
|
|
245
|
+
}
|
|
246
|
+
case "INTEGER": {
|
|
247
|
+
let n = zod.z.number().int();
|
|
248
|
+
if (config.min !== void 0) n = n.min(config.min);
|
|
249
|
+
if (config.max !== void 0) n = n.max(config.max);
|
|
250
|
+
return n;
|
|
251
|
+
}
|
|
252
|
+
case "FLOAT8": {
|
|
253
|
+
let n = zod.z.number();
|
|
254
|
+
if (config.min !== void 0) n = n.min(config.min);
|
|
255
|
+
if (config.max !== void 0) n = n.max(config.max);
|
|
256
|
+
return n;
|
|
257
|
+
}
|
|
258
|
+
case "NUMERIC": return zod.z.string().regex(/^-?\d+(\.\d+)?$/, "Must be a valid decimal number");
|
|
259
|
+
case "BOOLEAN": return zod.z.boolean();
|
|
260
|
+
case "TIMESTAMPTZ": return zod.z.coerce.date();
|
|
261
|
+
case "UUID": return zod.z.string().uuid();
|
|
262
|
+
case "SERIAL": return zod.z.number().int();
|
|
263
|
+
default: return zod.z.unknown();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* Define a table. This is the primary API for declaring your database schema.
|
|
269
|
+
*
|
|
270
|
+
* ```ts
|
|
271
|
+
* export const users = table('users', {
|
|
272
|
+
* id: kadak.id(),
|
|
273
|
+
* name: kadak.text().required(),
|
|
274
|
+
* email: kadak.email().unique(),
|
|
275
|
+
* age: kadak.int().optional(),
|
|
276
|
+
* createdAt: kadak.timestamp().default('now'),
|
|
277
|
+
* })
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
function table(name, columns, options) {
|
|
281
|
+
return new Table(name, columns, options);
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/error.ts
|
|
285
|
+
/** Maps PostgreSQL error codes to human-readable KadakORM codes + hints */
|
|
286
|
+
const PG_ERROR_MAP = {
|
|
287
|
+
"23505": {
|
|
288
|
+
code: "UNIQUE_VIOLATION",
|
|
289
|
+
hint: "A row with this value already exists. Check your unique columns."
|
|
290
|
+
},
|
|
291
|
+
"23502": {
|
|
292
|
+
code: "NOT_NULL_VIOLATION",
|
|
293
|
+
hint: "A required column is missing. Add .optional() if it should be nullable."
|
|
294
|
+
},
|
|
295
|
+
"23503": {
|
|
296
|
+
code: "FOREIGN_KEY_VIOLATION",
|
|
297
|
+
hint: "The referenced row does not exist in the related table."
|
|
298
|
+
},
|
|
299
|
+
"42P01": {
|
|
300
|
+
code: "TABLE_NOT_FOUND",
|
|
301
|
+
hint: "Did you run migrations? The table does not exist yet."
|
|
302
|
+
},
|
|
303
|
+
"42703": {
|
|
304
|
+
code: "COLUMN_NOT_FOUND",
|
|
305
|
+
hint: "Check your schema — this column is not in the table."
|
|
306
|
+
},
|
|
307
|
+
"08006": {
|
|
308
|
+
code: "CONNECTION_ERROR",
|
|
309
|
+
hint: "Cannot reach the database. Check your DATABASE_URL and network."
|
|
310
|
+
},
|
|
311
|
+
"08001": {
|
|
312
|
+
code: "CONNECTION_ERROR",
|
|
313
|
+
hint: "Connection refused. Is PostgreSQL running?"
|
|
314
|
+
},
|
|
315
|
+
"28P01": {
|
|
316
|
+
code: "AUTH_ERROR",
|
|
317
|
+
hint: "Wrong username or password in your connection string."
|
|
318
|
+
},
|
|
319
|
+
"3D000": {
|
|
320
|
+
code: "CONNECTION_ERROR",
|
|
321
|
+
hint: "This database does not exist. Check the database name in your URL."
|
|
322
|
+
},
|
|
323
|
+
"57014": {
|
|
324
|
+
code: "QUERY_TIMEOUT",
|
|
325
|
+
hint: "Query took too long. Consider adding an index or simplifying the query."
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* The one error class users ever see from KadakORM.
|
|
330
|
+
* Always includes: what went wrong, which table/column, and how to fix it.
|
|
331
|
+
*/
|
|
332
|
+
var KadakError = class extends Error {
|
|
333
|
+
constructor(opts) {
|
|
334
|
+
super(opts.message);
|
|
335
|
+
this.name = "KadakError";
|
|
336
|
+
this.code = opts.code;
|
|
337
|
+
this.hint = opts.hint;
|
|
338
|
+
this.table = opts.table;
|
|
339
|
+
this.column = opts.column;
|
|
340
|
+
this.constraint = opts.constraint;
|
|
341
|
+
this.originalError = opts.originalError;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
/** Wraps any postgres.js error into a KadakError with context */
|
|
345
|
+
function wrapPgError(err, tableName) {
|
|
346
|
+
const pgErr = err;
|
|
347
|
+
const mapped = PG_ERROR_MAP[pgErr?.code ?? ""] ?? {
|
|
348
|
+
code: "UNKNOWN",
|
|
349
|
+
hint: "An unexpected database error occurred."
|
|
350
|
+
};
|
|
351
|
+
const table = pgErr?.table_name ?? tableName;
|
|
352
|
+
const column = pgErr?.column_name;
|
|
353
|
+
const constraint = pgErr?.constraint_name;
|
|
354
|
+
let message = pgErr?.message ?? "Unknown database error";
|
|
355
|
+
if (mapped.code === "NOT_NULL_VIOLATION" && column) message = `Column '${column}' in table '${table}' cannot be null. Did you forget to pass '${column}'?`;
|
|
356
|
+
else if (mapped.code === "UNIQUE_VIOLATION" && constraint) message = `Duplicate value violates constraint '${constraint}' on table '${table}'.`;
|
|
357
|
+
else if (mapped.code === "TABLE_NOT_FOUND") message = `Table '${table ?? "unknown"}' does not exist. Have you run your migrations?`;
|
|
358
|
+
return new KadakError({
|
|
359
|
+
code: mapped.code,
|
|
360
|
+
message,
|
|
361
|
+
hint: mapped.hint,
|
|
362
|
+
table,
|
|
363
|
+
column,
|
|
364
|
+
constraint,
|
|
365
|
+
originalError: err
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/query.ts
|
|
370
|
+
/**
|
|
371
|
+
* Query client for a single table. Created by `connect()` — never instantiated directly.
|
|
372
|
+
* Provides findMany, findFirst, insert, update, delete with full type safety.
|
|
373
|
+
*/
|
|
374
|
+
var TableClient = class {
|
|
375
|
+
constructor(sql, table) {
|
|
376
|
+
this._sql = sql;
|
|
377
|
+
this._table = table;
|
|
378
|
+
this._reverseMap = {};
|
|
379
|
+
for (const [camel, snake] of Object.entries(table._columnMap)) this._reverseMap[snake] = camel;
|
|
380
|
+
}
|
|
381
|
+
async findMany(options) {
|
|
382
|
+
try {
|
|
383
|
+
const sql = this._sql;
|
|
384
|
+
const t = this._table;
|
|
385
|
+
const cols = options?.select ? Object.keys(options.select).filter((k) => options.select[k]).map((k) => t._columnMap[k] ?? k) : Object.values(t._columnMap);
|
|
386
|
+
const whereFragment = this._buildWhere(options?.where, options?.withDeleted);
|
|
387
|
+
const orderFragment = this._buildOrderBy(options?.orderBy);
|
|
388
|
+
const limitFragment = options?.limit !== void 0 ? sql`LIMIT ${options.limit}` : sql``;
|
|
389
|
+
const offsetFragment = options?.offset !== void 0 ? sql`OFFSET ${options.offset}` : sql``;
|
|
390
|
+
return (await sql`
|
|
391
|
+
SELECT ${sql(cols)} FROM ${sql(t._name)}
|
|
392
|
+
${whereFragment} ${orderFragment} ${limitFragment} ${offsetFragment}
|
|
393
|
+
`).map((row) => this._toJs(row));
|
|
394
|
+
} catch (err) {
|
|
395
|
+
throw wrapPgError(err, this._table._name);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async findFirst(options) {
|
|
399
|
+
return (await this.findMany({
|
|
400
|
+
...options,
|
|
401
|
+
limit: 1
|
|
402
|
+
}))[0] ?? null;
|
|
403
|
+
}
|
|
404
|
+
async insert(data) {
|
|
405
|
+
try {
|
|
406
|
+
const sql = this._sql;
|
|
407
|
+
const t = this._table;
|
|
408
|
+
const isBulk = Array.isArray(data);
|
|
409
|
+
const snakeItems = (isBulk ? data : [data]).map((item) => this._toDb(item));
|
|
410
|
+
const cols = Object.keys(snakeItems[0]);
|
|
411
|
+
const result = (await sql`
|
|
412
|
+
INSERT INTO ${sql(t._name)} ${sql(snakeItems, ...cols)} RETURNING *
|
|
413
|
+
`).map((row) => this._toJs(row));
|
|
414
|
+
return isBulk ? result : result[0];
|
|
415
|
+
} catch (err) {
|
|
416
|
+
throw wrapPgError(err, this._table._name);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async update(options) {
|
|
420
|
+
try {
|
|
421
|
+
const sql = this._sql;
|
|
422
|
+
const t = this._table;
|
|
423
|
+
const snakeData = {};
|
|
424
|
+
for (const [key, value] of Object.entries(options.data)) if (value !== void 0) snakeData[t._columnMap[key] ?? key] = value;
|
|
425
|
+
for (const [key, col] of Object.entries(t._columns)) if (col._config.autoUpdate) snakeData[t._columnMap[key]] = /* @__PURE__ */ new Date();
|
|
426
|
+
const whereFragment = this._buildWhere(options.where, false);
|
|
427
|
+
return (await sql`
|
|
428
|
+
UPDATE ${sql(t._name)} SET ${sql(snakeData)} ${whereFragment} RETURNING *
|
|
429
|
+
`).map((row) => this._toJs(row));
|
|
430
|
+
} catch (err) {
|
|
431
|
+
throw wrapPgError(err, this._table._name);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async delete(options) {
|
|
435
|
+
const softDeleteEntry = Object.entries(this._table._columns).find(([_, col]) => col._config.softDelete);
|
|
436
|
+
if (softDeleteEntry) {
|
|
437
|
+
const [key] = softDeleteEntry;
|
|
438
|
+
this._table._columnMap[key];
|
|
439
|
+
return this.update({
|
|
440
|
+
where: options.where,
|
|
441
|
+
data: { [key]: /* @__PURE__ */ new Date() }
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
return this.hardDelete(options);
|
|
445
|
+
}
|
|
446
|
+
/** Always performs a real DELETE — bypasses soft delete */
|
|
447
|
+
async hardDelete(options) {
|
|
448
|
+
try {
|
|
449
|
+
const sql = this._sql;
|
|
450
|
+
const whereFragment = this._buildWhere(options.where, true);
|
|
451
|
+
return (await sql`
|
|
452
|
+
DELETE FROM ${sql(this._table._name)} ${whereFragment} RETURNING *
|
|
453
|
+
`).map((row) => this._toJs(row));
|
|
454
|
+
} catch (err) {
|
|
455
|
+
throw wrapPgError(err, this._table._name);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/** Builds a WHERE fragment from a JS object, including soft delete filter */
|
|
459
|
+
_buildWhere(where, withDeleted) {
|
|
460
|
+
const sql = this._sql;
|
|
461
|
+
const conditions = [];
|
|
462
|
+
if (where) for (const [key, value] of Object.entries(where)) {
|
|
463
|
+
const col = this._table._columnMap[key] ?? key;
|
|
464
|
+
if (value === void 0) continue;
|
|
465
|
+
if (value === null) conditions.push(sql`${sql(col)} IS NULL`);
|
|
466
|
+
else conditions.push(sql`${sql(col)} = ${value}`);
|
|
467
|
+
}
|
|
468
|
+
if (!withDeleted) {
|
|
469
|
+
for (const [key, col] of Object.entries(this._table._columns)) if (col._config.softDelete) conditions.push(sql`${sql(this._table._columnMap[key])} IS NULL`);
|
|
470
|
+
}
|
|
471
|
+
if (conditions.length === 0) return sql``;
|
|
472
|
+
let combined = conditions[0];
|
|
473
|
+
for (let i = 1; i < conditions.length; i++) combined = sql`${combined} AND ${conditions[i]}`;
|
|
474
|
+
return sql`WHERE ${combined}`;
|
|
475
|
+
}
|
|
476
|
+
/** Builds an ORDER BY fragment */
|
|
477
|
+
_buildOrderBy(orderBy) {
|
|
478
|
+
const sql = this._sql;
|
|
479
|
+
if (!orderBy) return sql``;
|
|
480
|
+
const entries = Object.entries(orderBy);
|
|
481
|
+
if (entries.length === 0) return sql``;
|
|
482
|
+
const parts = entries.map(([key, dir]) => {
|
|
483
|
+
return sql`${sql(this._table._columnMap[key] ?? key)} ${dir === "desc" ? sql`DESC` : sql`ASC`}`;
|
|
484
|
+
});
|
|
485
|
+
let combined = parts[0];
|
|
486
|
+
for (let i = 1; i < parts.length; i++) combined = sql`${combined}, ${parts[i]}`;
|
|
487
|
+
return sql`ORDER BY ${combined}`;
|
|
488
|
+
}
|
|
489
|
+
/** Converts a JS object (camelCase) to DB format (snake_case) */
|
|
490
|
+
_toDb(obj) {
|
|
491
|
+
const result = {};
|
|
492
|
+
for (const [key, value] of Object.entries(obj)) if (value !== void 0) result[this._table._columnMap[key] ?? key] = value;
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
/** Converts a DB row (snake_case) back to JS format (camelCase) */
|
|
496
|
+
_toJs(row) {
|
|
497
|
+
const result = {};
|
|
498
|
+
for (const [snake, value] of Object.entries(row)) result[this._reverseMap[snake] ?? snake] = value;
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/connect.ts
|
|
504
|
+
/**
|
|
505
|
+
* Connect to PostgreSQL and create a typed database client.
|
|
506
|
+
*
|
|
507
|
+
* ```ts
|
|
508
|
+
* const db = connect('postgresql://user:pass@localhost/mydb', { users, posts })
|
|
509
|
+
* const allUsers = await db.users.findMany()
|
|
510
|
+
* await db.close()
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
function connect(config, tables) {
|
|
514
|
+
const isString = typeof config === "string";
|
|
515
|
+
const url = isString ? config : config.url;
|
|
516
|
+
const opts = isString ? {} : config;
|
|
517
|
+
const isLocal = url.includes("localhost") || url.includes("127.0.0.1");
|
|
518
|
+
const ssl = opts.ssl ?? (isLocal ? false : "require");
|
|
519
|
+
const sql = (0, postgres.default)(url, {
|
|
520
|
+
max: opts.max ?? 10,
|
|
521
|
+
idle_timeout: 20,
|
|
522
|
+
connect_timeout: 30,
|
|
523
|
+
ssl,
|
|
524
|
+
max_lifetime: 1800,
|
|
525
|
+
onnotice: () => {}
|
|
526
|
+
});
|
|
527
|
+
const db = {};
|
|
528
|
+
for (const [key, table] of Object.entries(tables)) {
|
|
529
|
+
const client = new TableClient(sql, table);
|
|
530
|
+
if (opts.onError) client._onError = opts.onError;
|
|
531
|
+
db[key] = client;
|
|
532
|
+
}
|
|
533
|
+
db.close = () => sql.end();
|
|
534
|
+
db.sql = sql;
|
|
535
|
+
return db;
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
538
|
+
exports.Column = Column;
|
|
539
|
+
exports.KadakError = KadakError;
|
|
540
|
+
exports.Table = Table;
|
|
541
|
+
exports.TableClient = TableClient;
|
|
542
|
+
exports.camelToSnake = camelToSnake;
|
|
543
|
+
exports.connect = connect;
|
|
544
|
+
exports.kadak = kadak;
|
|
545
|
+
exports.table = table;
|
|
546
|
+
exports.wrapPgError = wrapPgError;
|
|
547
|
+
|
|
548
|
+
//# sourceMappingURL=index.cjs.map
|