@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/dist/index.mjs
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import postgres from "postgres";
|
|
3
|
+
//#region src/schema.ts
|
|
4
|
+
/** Converts camelCase to snake_case for PostgreSQL column naming */
|
|
5
|
+
function camelToSnake(str) {
|
|
6
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A column definition carrying runtime metadata AND compile-time types.
|
|
10
|
+
*
|
|
11
|
+
* Three generics flow through the entire ORM:
|
|
12
|
+
* - TType: JS type (string, number, boolean, Date)
|
|
13
|
+
* - TNullable: whether NULL is allowed (adds `| null`)
|
|
14
|
+
* - THasDefault: whether INSERT can omit this (adds `?`)
|
|
15
|
+
*
|
|
16
|
+
* Every modifier returns a NEW Column — immutable builder.
|
|
17
|
+
*/
|
|
18
|
+
var Column = class Column {
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this._config = Object.freeze({ ...config });
|
|
21
|
+
}
|
|
22
|
+
/** NOT NULL — already the default. Exists for explicit readability. */
|
|
23
|
+
required() {
|
|
24
|
+
return new Column({
|
|
25
|
+
...this._config,
|
|
26
|
+
nullable: false
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Allow NULL — column type becomes TType | null in TypeScript */
|
|
30
|
+
optional() {
|
|
31
|
+
return new Column({
|
|
32
|
+
...this._config,
|
|
33
|
+
nullable: true
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Add UNIQUE constraint */
|
|
37
|
+
unique() {
|
|
38
|
+
return new Column({
|
|
39
|
+
...this._config,
|
|
40
|
+
isUnique: true
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Mark as PRIMARY KEY */
|
|
44
|
+
primaryKey() {
|
|
45
|
+
return new Column({
|
|
46
|
+
...this._config,
|
|
47
|
+
isPrimaryKey: true
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Set default value — makes field optional in INSERT.
|
|
52
|
+
*
|
|
53
|
+
* Named shortcuts are resolved per column type to avoid ambiguity:
|
|
54
|
+
* - 'now' on TIMESTAMPTZ → SQL DEFAULT now()
|
|
55
|
+
* - 'uuid' on UUID → SQL DEFAULT gen_random_uuid()
|
|
56
|
+
* On other types, these strings are treated as literal values.
|
|
57
|
+
*/
|
|
58
|
+
default(value) {
|
|
59
|
+
const shortcuts = {
|
|
60
|
+
TIMESTAMPTZ: { now: "now()" },
|
|
61
|
+
UUID: { uuid: "gen_random_uuid()" }
|
|
62
|
+
}[this._config.pgType] ?? {};
|
|
63
|
+
const sqlExpr = typeof value === "string" ? shortcuts[value] : void 0;
|
|
64
|
+
return new Column({
|
|
65
|
+
...this._config,
|
|
66
|
+
hasDefault: true,
|
|
67
|
+
defaultValue: sqlExpr ? void 0 : value,
|
|
68
|
+
defaultSql: sqlExpr ?? this._config.defaultSql
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Min constraint — string length for text, numeric value for numbers */
|
|
72
|
+
min(n) {
|
|
73
|
+
return new Column({
|
|
74
|
+
...this._config,
|
|
75
|
+
min: n
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** Max constraint — string length for text, numeric value for numbers */
|
|
79
|
+
max(n) {
|
|
80
|
+
return new Column({
|
|
81
|
+
...this._config,
|
|
82
|
+
max: n
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** Auto-update on every UPDATE query — designed for updatedAt columns */
|
|
86
|
+
autoUpdate() {
|
|
87
|
+
return new Column({
|
|
88
|
+
...this._config,
|
|
89
|
+
autoUpdate: true,
|
|
90
|
+
hasDefault: true
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Soft delete marker. When set on a column:
|
|
95
|
+
* - findMany() auto-adds WHERE column IS NULL
|
|
96
|
+
* - delete() sets this column to now() instead of DELETE
|
|
97
|
+
* - findMany({ withDeleted: true }) bypasses the filter
|
|
98
|
+
*/
|
|
99
|
+
softDelete() {
|
|
100
|
+
return new Column({
|
|
101
|
+
...this._config,
|
|
102
|
+
softDelete: true,
|
|
103
|
+
nullable: true,
|
|
104
|
+
hasDefault: true
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
/** Base config with required-by-default semantics */
|
|
109
|
+
function baseConfig(pgType) {
|
|
110
|
+
return {
|
|
111
|
+
pgType,
|
|
112
|
+
nullable: false,
|
|
113
|
+
hasDefault: false,
|
|
114
|
+
isPrimaryKey: false,
|
|
115
|
+
isUnique: false,
|
|
116
|
+
isGenerated: false,
|
|
117
|
+
autoUpdate: false,
|
|
118
|
+
softDelete: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Column factory namespace. Every column definition starts here.
|
|
123
|
+
*
|
|
124
|
+
* ```ts
|
|
125
|
+
* const users = table('users', {
|
|
126
|
+
* id: kadak.id(),
|
|
127
|
+
* name: kadak.text().required(),
|
|
128
|
+
* email: kadak.email().unique(),
|
|
129
|
+
* })
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
const kadak = {
|
|
133
|
+
id: () => new Column({
|
|
134
|
+
...baseConfig("INTEGER"),
|
|
135
|
+
isPrimaryKey: true,
|
|
136
|
+
isGenerated: true,
|
|
137
|
+
hasDefault: true
|
|
138
|
+
}),
|
|
139
|
+
uuidId: () => new Column({
|
|
140
|
+
...baseConfig("UUID"),
|
|
141
|
+
isPrimaryKey: true,
|
|
142
|
+
hasDefault: true,
|
|
143
|
+
defaultSql: "gen_random_uuid()"
|
|
144
|
+
}),
|
|
145
|
+
serialId: () => new Column({
|
|
146
|
+
...baseConfig("SERIAL"),
|
|
147
|
+
isPrimaryKey: true,
|
|
148
|
+
isGenerated: true,
|
|
149
|
+
hasDefault: true
|
|
150
|
+
}),
|
|
151
|
+
text: () => new Column(baseConfig("TEXT")),
|
|
152
|
+
int: () => new Column(baseConfig("INTEGER")),
|
|
153
|
+
number: () => new Column(baseConfig("INTEGER")),
|
|
154
|
+
float: () => new Column(baseConfig("FLOAT8")),
|
|
155
|
+
decimal: () => new Column(baseConfig("NUMERIC")),
|
|
156
|
+
boolean: () => new Column(baseConfig("BOOLEAN")),
|
|
157
|
+
timestamp: () => new Column(baseConfig("TIMESTAMPTZ")),
|
|
158
|
+
email: () => new Column({
|
|
159
|
+
...baseConfig("TEXT"),
|
|
160
|
+
zodCheck: "email"
|
|
161
|
+
}),
|
|
162
|
+
uuid: () => new Column({
|
|
163
|
+
...baseConfig("UUID"),
|
|
164
|
+
zodCheck: "uuid"
|
|
165
|
+
}),
|
|
166
|
+
json: (schema) => new Column({
|
|
167
|
+
...baseConfig("JSONB"),
|
|
168
|
+
zodSchema: schema
|
|
169
|
+
})
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* A table definition. Created via the `table()` function.
|
|
173
|
+
* Holds column metadata and generates typed Zod validators.
|
|
174
|
+
*/
|
|
175
|
+
var Table = class {
|
|
176
|
+
constructor(name, columns, options) {
|
|
177
|
+
this._name = name;
|
|
178
|
+
this._columns = columns;
|
|
179
|
+
this._options = options;
|
|
180
|
+
this._columnMap = {};
|
|
181
|
+
for (const key of Object.keys(columns)) this._columnMap[key] = camelToSnake(key);
|
|
182
|
+
}
|
|
183
|
+
/** Alias for insertValidator() — the most common use case (validating user input) */
|
|
184
|
+
validator() {
|
|
185
|
+
return this.insertValidator();
|
|
186
|
+
}
|
|
187
|
+
/** Validates data for INSERT — generated/defaulted fields are optional */
|
|
188
|
+
insertValidator() {
|
|
189
|
+
return z.object(this._buildShape("insert"));
|
|
190
|
+
}
|
|
191
|
+
/** Validates data for SELECT — all columns present, nullability applied */
|
|
192
|
+
selectValidator() {
|
|
193
|
+
return z.object(this._buildShape("select"));
|
|
194
|
+
}
|
|
195
|
+
/** Validates data for UPDATE — everything optional (partial update) */
|
|
196
|
+
updateValidator() {
|
|
197
|
+
return z.object(this._buildShape("update"));
|
|
198
|
+
}
|
|
199
|
+
/** @internal Builds Zod shape for a given operation mode */
|
|
200
|
+
_buildShape(mode) {
|
|
201
|
+
const shape = {};
|
|
202
|
+
for (const [key, col] of Object.entries(this._columns)) {
|
|
203
|
+
let zodType = this._baseZodType(col._config);
|
|
204
|
+
if (col._config.nullable) zodType = zodType.nullable();
|
|
205
|
+
if (mode === "insert" && (col._config.hasDefault || col._config.isGenerated)) zodType = zodType.optional();
|
|
206
|
+
else if (mode === "update") zodType = zodType.optional();
|
|
207
|
+
shape[key] = zodType;
|
|
208
|
+
}
|
|
209
|
+
return shape;
|
|
210
|
+
}
|
|
211
|
+
/** @internal Creates the base Zod type for a column, without nullable/optional */
|
|
212
|
+
_baseZodType(config) {
|
|
213
|
+
if (config.pgType === "JSONB" && config.zodSchema) return config.zodSchema;
|
|
214
|
+
switch (config.pgType) {
|
|
215
|
+
case "TEXT": {
|
|
216
|
+
let s = z.string();
|
|
217
|
+
if (config.zodCheck === "email") s = s.email();
|
|
218
|
+
if (config.min !== void 0) s = s.min(config.min);
|
|
219
|
+
if (config.max !== void 0) s = s.max(config.max);
|
|
220
|
+
return s;
|
|
221
|
+
}
|
|
222
|
+
case "INTEGER": {
|
|
223
|
+
let n = z.number().int();
|
|
224
|
+
if (config.min !== void 0) n = n.min(config.min);
|
|
225
|
+
if (config.max !== void 0) n = n.max(config.max);
|
|
226
|
+
return n;
|
|
227
|
+
}
|
|
228
|
+
case "FLOAT8": {
|
|
229
|
+
let n = z.number();
|
|
230
|
+
if (config.min !== void 0) n = n.min(config.min);
|
|
231
|
+
if (config.max !== void 0) n = n.max(config.max);
|
|
232
|
+
return n;
|
|
233
|
+
}
|
|
234
|
+
case "NUMERIC": return z.string().regex(/^-?\d+(\.\d+)?$/, "Must be a valid decimal number");
|
|
235
|
+
case "BOOLEAN": return z.boolean();
|
|
236
|
+
case "TIMESTAMPTZ": return z.coerce.date();
|
|
237
|
+
case "UUID": return z.string().uuid();
|
|
238
|
+
case "SERIAL": return z.number().int();
|
|
239
|
+
default: return z.unknown();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Define a table. This is the primary API for declaring your database schema.
|
|
245
|
+
*
|
|
246
|
+
* ```ts
|
|
247
|
+
* export const users = table('users', {
|
|
248
|
+
* id: kadak.id(),
|
|
249
|
+
* name: kadak.text().required(),
|
|
250
|
+
* email: kadak.email().unique(),
|
|
251
|
+
* age: kadak.int().optional(),
|
|
252
|
+
* createdAt: kadak.timestamp().default('now'),
|
|
253
|
+
* })
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
function table(name, columns, options) {
|
|
257
|
+
return new Table(name, columns, options);
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/error.ts
|
|
261
|
+
/** Maps PostgreSQL error codes to human-readable KadakORM codes + hints */
|
|
262
|
+
const PG_ERROR_MAP = {
|
|
263
|
+
"23505": {
|
|
264
|
+
code: "UNIQUE_VIOLATION",
|
|
265
|
+
hint: "A row with this value already exists. Check your unique columns."
|
|
266
|
+
},
|
|
267
|
+
"23502": {
|
|
268
|
+
code: "NOT_NULL_VIOLATION",
|
|
269
|
+
hint: "A required column is missing. Add .optional() if it should be nullable."
|
|
270
|
+
},
|
|
271
|
+
"23503": {
|
|
272
|
+
code: "FOREIGN_KEY_VIOLATION",
|
|
273
|
+
hint: "The referenced row does not exist in the related table."
|
|
274
|
+
},
|
|
275
|
+
"42P01": {
|
|
276
|
+
code: "TABLE_NOT_FOUND",
|
|
277
|
+
hint: "Did you run migrations? The table does not exist yet."
|
|
278
|
+
},
|
|
279
|
+
"42703": {
|
|
280
|
+
code: "COLUMN_NOT_FOUND",
|
|
281
|
+
hint: "Check your schema — this column is not in the table."
|
|
282
|
+
},
|
|
283
|
+
"08006": {
|
|
284
|
+
code: "CONNECTION_ERROR",
|
|
285
|
+
hint: "Cannot reach the database. Check your DATABASE_URL and network."
|
|
286
|
+
},
|
|
287
|
+
"08001": {
|
|
288
|
+
code: "CONNECTION_ERROR",
|
|
289
|
+
hint: "Connection refused. Is PostgreSQL running?"
|
|
290
|
+
},
|
|
291
|
+
"28P01": {
|
|
292
|
+
code: "AUTH_ERROR",
|
|
293
|
+
hint: "Wrong username or password in your connection string."
|
|
294
|
+
},
|
|
295
|
+
"3D000": {
|
|
296
|
+
code: "CONNECTION_ERROR",
|
|
297
|
+
hint: "This database does not exist. Check the database name in your URL."
|
|
298
|
+
},
|
|
299
|
+
"57014": {
|
|
300
|
+
code: "QUERY_TIMEOUT",
|
|
301
|
+
hint: "Query took too long. Consider adding an index or simplifying the query."
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* The one error class users ever see from KadakORM.
|
|
306
|
+
* Always includes: what went wrong, which table/column, and how to fix it.
|
|
307
|
+
*/
|
|
308
|
+
var KadakError = class extends Error {
|
|
309
|
+
constructor(opts) {
|
|
310
|
+
super(opts.message);
|
|
311
|
+
this.name = "KadakError";
|
|
312
|
+
this.code = opts.code;
|
|
313
|
+
this.hint = opts.hint;
|
|
314
|
+
this.table = opts.table;
|
|
315
|
+
this.column = opts.column;
|
|
316
|
+
this.constraint = opts.constraint;
|
|
317
|
+
this.originalError = opts.originalError;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
/** Wraps any postgres.js error into a KadakError with context */
|
|
321
|
+
function wrapPgError(err, tableName) {
|
|
322
|
+
const pgErr = err;
|
|
323
|
+
const mapped = PG_ERROR_MAP[pgErr?.code ?? ""] ?? {
|
|
324
|
+
code: "UNKNOWN",
|
|
325
|
+
hint: "An unexpected database error occurred."
|
|
326
|
+
};
|
|
327
|
+
const table = pgErr?.table_name ?? tableName;
|
|
328
|
+
const column = pgErr?.column_name;
|
|
329
|
+
const constraint = pgErr?.constraint_name;
|
|
330
|
+
let message = pgErr?.message ?? "Unknown database error";
|
|
331
|
+
if (mapped.code === "NOT_NULL_VIOLATION" && column) message = `Column '${column}' in table '${table}' cannot be null. Did you forget to pass '${column}'?`;
|
|
332
|
+
else if (mapped.code === "UNIQUE_VIOLATION" && constraint) message = `Duplicate value violates constraint '${constraint}' on table '${table}'.`;
|
|
333
|
+
else if (mapped.code === "TABLE_NOT_FOUND") message = `Table '${table ?? "unknown"}' does not exist. Have you run your migrations?`;
|
|
334
|
+
return new KadakError({
|
|
335
|
+
code: mapped.code,
|
|
336
|
+
message,
|
|
337
|
+
hint: mapped.hint,
|
|
338
|
+
table,
|
|
339
|
+
column,
|
|
340
|
+
constraint,
|
|
341
|
+
originalError: err
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/query.ts
|
|
346
|
+
/**
|
|
347
|
+
* Query client for a single table. Created by `connect()` — never instantiated directly.
|
|
348
|
+
* Provides findMany, findFirst, insert, update, delete with full type safety.
|
|
349
|
+
*/
|
|
350
|
+
var TableClient = class {
|
|
351
|
+
constructor(sql, table) {
|
|
352
|
+
this._sql = sql;
|
|
353
|
+
this._table = table;
|
|
354
|
+
this._reverseMap = {};
|
|
355
|
+
for (const [camel, snake] of Object.entries(table._columnMap)) this._reverseMap[snake] = camel;
|
|
356
|
+
}
|
|
357
|
+
async findMany(options) {
|
|
358
|
+
try {
|
|
359
|
+
const sql = this._sql;
|
|
360
|
+
const t = this._table;
|
|
361
|
+
const cols = options?.select ? Object.keys(options.select).filter((k) => options.select[k]).map((k) => t._columnMap[k] ?? k) : Object.values(t._columnMap);
|
|
362
|
+
const whereFragment = this._buildWhere(options?.where, options?.withDeleted);
|
|
363
|
+
const orderFragment = this._buildOrderBy(options?.orderBy);
|
|
364
|
+
const limitFragment = options?.limit !== void 0 ? sql`LIMIT ${options.limit}` : sql``;
|
|
365
|
+
const offsetFragment = options?.offset !== void 0 ? sql`OFFSET ${options.offset}` : sql``;
|
|
366
|
+
return (await sql`
|
|
367
|
+
SELECT ${sql(cols)} FROM ${sql(t._name)}
|
|
368
|
+
${whereFragment} ${orderFragment} ${limitFragment} ${offsetFragment}
|
|
369
|
+
`).map((row) => this._toJs(row));
|
|
370
|
+
} catch (err) {
|
|
371
|
+
throw wrapPgError(err, this._table._name);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async findFirst(options) {
|
|
375
|
+
return (await this.findMany({
|
|
376
|
+
...options,
|
|
377
|
+
limit: 1
|
|
378
|
+
}))[0] ?? null;
|
|
379
|
+
}
|
|
380
|
+
async insert(data) {
|
|
381
|
+
try {
|
|
382
|
+
const sql = this._sql;
|
|
383
|
+
const t = this._table;
|
|
384
|
+
const isBulk = Array.isArray(data);
|
|
385
|
+
const snakeItems = (isBulk ? data : [data]).map((item) => this._toDb(item));
|
|
386
|
+
const cols = Object.keys(snakeItems[0]);
|
|
387
|
+
const result = (await sql`
|
|
388
|
+
INSERT INTO ${sql(t._name)} ${sql(snakeItems, ...cols)} RETURNING *
|
|
389
|
+
`).map((row) => this._toJs(row));
|
|
390
|
+
return isBulk ? result : result[0];
|
|
391
|
+
} catch (err) {
|
|
392
|
+
throw wrapPgError(err, this._table._name);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async update(options) {
|
|
396
|
+
try {
|
|
397
|
+
const sql = this._sql;
|
|
398
|
+
const t = this._table;
|
|
399
|
+
const snakeData = {};
|
|
400
|
+
for (const [key, value] of Object.entries(options.data)) if (value !== void 0) snakeData[t._columnMap[key] ?? key] = value;
|
|
401
|
+
for (const [key, col] of Object.entries(t._columns)) if (col._config.autoUpdate) snakeData[t._columnMap[key]] = /* @__PURE__ */ new Date();
|
|
402
|
+
const whereFragment = this._buildWhere(options.where, false);
|
|
403
|
+
return (await sql`
|
|
404
|
+
UPDATE ${sql(t._name)} SET ${sql(snakeData)} ${whereFragment} RETURNING *
|
|
405
|
+
`).map((row) => this._toJs(row));
|
|
406
|
+
} catch (err) {
|
|
407
|
+
throw wrapPgError(err, this._table._name);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async delete(options) {
|
|
411
|
+
const softDeleteEntry = Object.entries(this._table._columns).find(([_, col]) => col._config.softDelete);
|
|
412
|
+
if (softDeleteEntry) {
|
|
413
|
+
const [key] = softDeleteEntry;
|
|
414
|
+
this._table._columnMap[key];
|
|
415
|
+
return this.update({
|
|
416
|
+
where: options.where,
|
|
417
|
+
data: { [key]: /* @__PURE__ */ new Date() }
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return this.hardDelete(options);
|
|
421
|
+
}
|
|
422
|
+
/** Always performs a real DELETE — bypasses soft delete */
|
|
423
|
+
async hardDelete(options) {
|
|
424
|
+
try {
|
|
425
|
+
const sql = this._sql;
|
|
426
|
+
const whereFragment = this._buildWhere(options.where, true);
|
|
427
|
+
return (await sql`
|
|
428
|
+
DELETE FROM ${sql(this._table._name)} ${whereFragment} RETURNING *
|
|
429
|
+
`).map((row) => this._toJs(row));
|
|
430
|
+
} catch (err) {
|
|
431
|
+
throw wrapPgError(err, this._table._name);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Builds a WHERE fragment from a JS object, including soft delete filter */
|
|
435
|
+
_buildWhere(where, withDeleted) {
|
|
436
|
+
const sql = this._sql;
|
|
437
|
+
const conditions = [];
|
|
438
|
+
if (where) for (const [key, value] of Object.entries(where)) {
|
|
439
|
+
const col = this._table._columnMap[key] ?? key;
|
|
440
|
+
if (value === void 0) continue;
|
|
441
|
+
if (value === null) conditions.push(sql`${sql(col)} IS NULL`);
|
|
442
|
+
else conditions.push(sql`${sql(col)} = ${value}`);
|
|
443
|
+
}
|
|
444
|
+
if (!withDeleted) {
|
|
445
|
+
for (const [key, col] of Object.entries(this._table._columns)) if (col._config.softDelete) conditions.push(sql`${sql(this._table._columnMap[key])} IS NULL`);
|
|
446
|
+
}
|
|
447
|
+
if (conditions.length === 0) return sql``;
|
|
448
|
+
let combined = conditions[0];
|
|
449
|
+
for (let i = 1; i < conditions.length; i++) combined = sql`${combined} AND ${conditions[i]}`;
|
|
450
|
+
return sql`WHERE ${combined}`;
|
|
451
|
+
}
|
|
452
|
+
/** Builds an ORDER BY fragment */
|
|
453
|
+
_buildOrderBy(orderBy) {
|
|
454
|
+
const sql = this._sql;
|
|
455
|
+
if (!orderBy) return sql``;
|
|
456
|
+
const entries = Object.entries(orderBy);
|
|
457
|
+
if (entries.length === 0) return sql``;
|
|
458
|
+
const parts = entries.map(([key, dir]) => {
|
|
459
|
+
return sql`${sql(this._table._columnMap[key] ?? key)} ${dir === "desc" ? sql`DESC` : sql`ASC`}`;
|
|
460
|
+
});
|
|
461
|
+
let combined = parts[0];
|
|
462
|
+
for (let i = 1; i < parts.length; i++) combined = sql`${combined}, ${parts[i]}`;
|
|
463
|
+
return sql`ORDER BY ${combined}`;
|
|
464
|
+
}
|
|
465
|
+
/** Converts a JS object (camelCase) to DB format (snake_case) */
|
|
466
|
+
_toDb(obj) {
|
|
467
|
+
const result = {};
|
|
468
|
+
for (const [key, value] of Object.entries(obj)) if (value !== void 0) result[this._table._columnMap[key] ?? key] = value;
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
/** Converts a DB row (snake_case) back to JS format (camelCase) */
|
|
472
|
+
_toJs(row) {
|
|
473
|
+
const result = {};
|
|
474
|
+
for (const [snake, value] of Object.entries(row)) result[this._reverseMap[snake] ?? snake] = value;
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/connect.ts
|
|
480
|
+
/**
|
|
481
|
+
* Connect to PostgreSQL and create a typed database client.
|
|
482
|
+
*
|
|
483
|
+
* ```ts
|
|
484
|
+
* const db = connect('postgresql://user:pass@localhost/mydb', { users, posts })
|
|
485
|
+
* const allUsers = await db.users.findMany()
|
|
486
|
+
* await db.close()
|
|
487
|
+
* ```
|
|
488
|
+
*/
|
|
489
|
+
function connect(config, tables) {
|
|
490
|
+
const isString = typeof config === "string";
|
|
491
|
+
const url = isString ? config : config.url;
|
|
492
|
+
const opts = isString ? {} : config;
|
|
493
|
+
const isLocal = url.includes("localhost") || url.includes("127.0.0.1");
|
|
494
|
+
const ssl = opts.ssl ?? (isLocal ? false : "require");
|
|
495
|
+
const sql = postgres(url, {
|
|
496
|
+
max: opts.max ?? 10,
|
|
497
|
+
idle_timeout: 20,
|
|
498
|
+
connect_timeout: 30,
|
|
499
|
+
ssl,
|
|
500
|
+
max_lifetime: 1800,
|
|
501
|
+
onnotice: () => {}
|
|
502
|
+
});
|
|
503
|
+
const db = {};
|
|
504
|
+
for (const [key, table] of Object.entries(tables)) {
|
|
505
|
+
const client = new TableClient(sql, table);
|
|
506
|
+
if (opts.onError) client._onError = opts.onError;
|
|
507
|
+
db[key] = client;
|
|
508
|
+
}
|
|
509
|
+
db.close = () => sql.end();
|
|
510
|
+
db.sql = sql;
|
|
511
|
+
return db;
|
|
512
|
+
}
|
|
513
|
+
//#endregion
|
|
514
|
+
export { Column, KadakError, Table, TableClient, camelToSnake, connect, kadak, table, wrapPgError };
|
|
515
|
+
|
|
516
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/schema.ts","../src/error.ts","../src/query.ts","../src/connect.ts"],"sourcesContent":["// src/schema.ts — The heart of KadakORM\n// Purely declarative — no SQL generation, no database calls.\n// Same metadata drives DDL, query building, Zod validation, AND TypeScript inference.\n\nimport { z } from 'zod'\n\n// ============================================================\n// Utilities\n// ============================================================\n\n/** Converts camelCase to snake_case for PostgreSQL column naming */\n// JS convention is camelCase, PG convention is snake_case.\n// Users write camelCase, KadakORM translates to snake_case in SQL automatically.\nexport function camelToSnake(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)\n}\n\n/** Flattens intersection types so IDE tooltips show clean objects, not A & B */\ntype Simplify<T> = { [K in keyof T]: T[K] } & {}\n\n// ============================================================\n// Column Configuration — runtime metadata for every column\n// ============================================================\n\n/** @internal Runtime metadata describing a column's PG type, constraints, behavior */\nexport interface ColumnConfig {\n pgType: string\n nullable: boolean\n hasDefault: boolean\n isPrimaryKey: boolean\n isUnique: boolean\n defaultValue?: unknown\n /** SQL expression for default — 'now()' or 'gen_random_uuid()' */\n defaultSql?: string\n /** DB auto-generates this value (IDENTITY columns) */\n isGenerated: boolean\n /** Zod validation hint — 'email', 'uuid' */\n zodCheck?: string\n /** Stored Zod schema — only for JSONB columns */\n zodSchema?: z.ZodType\n /** Auto-update on every UPDATE — for updatedAt patterns */\n autoUpdate: boolean\n /** Soft delete marker — findMany auto-filters, delete sets timestamp */\n softDelete: boolean\n min?: number\n max?: number\n}\n\n// ============================================================\n// Column Class — builder pattern with compile-time type tracking\n// ============================================================\n\n/**\n * A column definition carrying runtime metadata AND compile-time types.\n *\n * Three generics flow through the entire ORM:\n * - TType: JS type (string, number, boolean, Date)\n * - TNullable: whether NULL is allowed (adds `| null`)\n * - THasDefault: whether INSERT can omit this (adds `?`)\n *\n * Every modifier returns a NEW Column — immutable builder.\n */\nexport class Column<\n TType,\n TNullable extends boolean = false,\n THasDefault extends boolean = false,\n> {\n readonly _config: ColumnConfig\n\n // Brand fields — TypeScript needs these to distinguish Column<string> from Column<number>\n // at the structural type level. They're never assigned a value at runtime.\n declare readonly _type: TType\n declare readonly _nullable: TNullable\n declare readonly _hasDefault: THasDefault\n\n constructor(config: ColumnConfig) {\n // Freeze prevents accidental mutation — immutability is a core invariant\n this._config = Object.freeze({ ...config })\n }\n\n /** NOT NULL — already the default. Exists for explicit readability. */\n required(): Column<TType, false, THasDefault> {\n return new Column({ ...this._config, nullable: false })\n }\n\n /** Allow NULL — column type becomes TType | null in TypeScript */\n optional(): Column<TType, true, THasDefault> {\n return new Column({ ...this._config, nullable: true })\n }\n\n /** Add UNIQUE constraint */\n unique(): Column<TType, TNullable, THasDefault> {\n return new Column({ ...this._config, isUnique: true })\n }\n\n /** Mark as PRIMARY KEY */\n primaryKey(): Column<TType, TNullable, THasDefault> {\n return new Column({ ...this._config, isPrimaryKey: true })\n }\n\n /**\n * Set default value — makes field optional in INSERT.\n *\n * Named shortcuts are resolved per column type to avoid ambiguity:\n * - 'now' on TIMESTAMPTZ → SQL DEFAULT now()\n * - 'uuid' on UUID → SQL DEFAULT gen_random_uuid()\n * On other types, these strings are treated as literal values.\n */\n default(value: TType extends Date ? TType | 'now' : TType): Column<TType, TNullable, true> {\n // Shortcuts scoped by PG type — 'now' is only special on TIMESTAMPTZ, not TEXT\n const typeShortcuts: Record<string, Record<string, string>> = {\n TIMESTAMPTZ: { now: 'now()' },\n UUID: { uuid: 'gen_random_uuid()' },\n }\n const shortcuts = typeShortcuts[this._config.pgType] ?? {}\n const sqlExpr = typeof value === 'string' ? shortcuts[value as string] : undefined\n\n return new Column({\n ...this._config,\n hasDefault: true,\n defaultValue: sqlExpr ? undefined : value,\n defaultSql: sqlExpr ?? this._config.defaultSql,\n })\n }\n\n /** Min constraint — string length for text, numeric value for numbers */\n min(n: number): Column<TType, TNullable, THasDefault> {\n return new Column({ ...this._config, min: n })\n }\n\n /** Max constraint — string length for text, numeric value for numbers */\n max(n: number): Column<TType, TNullable, THasDefault> {\n return new Column({ ...this._config, max: n })\n }\n\n /** Auto-update on every UPDATE query — designed for updatedAt columns */\n autoUpdate(): Column<TType, TNullable, true> {\n // Implies hasDefault because the ORM injects the value automatically\n return new Column({ ...this._config, autoUpdate: true, hasDefault: true })\n }\n\n /**\n * Soft delete marker. When set on a column:\n * - findMany() auto-adds WHERE column IS NULL\n * - delete() sets this column to now() instead of DELETE\n * - findMany({ withDeleted: true }) bypasses the filter\n */\n softDelete(): Column<TType, true, true> {\n // Always nullable (null = not deleted) and has default (null)\n return new Column({ ...this._config, softDelete: true, nullable: true, hasDefault: true })\n }\n}\n\n// ============================================================\n// Column Factories — the `kadak` namespace\n// ============================================================\n\n/** Base config with required-by-default semantics */\nfunction baseConfig(pgType: string): ColumnConfig {\n return {\n pgType,\n nullable: false,\n hasDefault: false,\n isPrimaryKey: false,\n isUnique: false,\n isGenerated: false,\n autoUpdate: false,\n softDelete: false,\n }\n}\n\n/**\n * Column factory namespace. Every column definition starts here.\n *\n * ```ts\n * const users = table('users', {\n * id: kadak.id(),\n * name: kadak.text().required(),\n * email: kadak.email().unique(),\n * })\n * ```\n */\nexport const kadak = {\n /**\n * Modern auto-incrementing integer primary key.\n * Uses IDENTITY (not deprecated SERIAL).\n * DDL: INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY\n */\n id: (): Column<number, false, true> =>\n new Column({ ...baseConfig('INTEGER'), isPrimaryKey: true, isGenerated: true, hasDefault: true }),\n\n /**\n * UUID primary key with auto-generation via gen_random_uuid().\n * Built into PostgreSQL 13+ — no extensions needed.\n * Ideal for distributed systems and secure-by-default IDs.\n */\n uuidId: (): Column<string, false, true> =>\n new Column({ ...baseConfig('UUID'), isPrimaryKey: true, hasDefault: true, defaultSql: 'gen_random_uuid()' }),\n\n /**\n * Legacy SERIAL primary key — use kadak.id() for new projects.\n * Kept for compatibility with existing schemas.\n */\n serialId: (): Column<number, false, true> =>\n new Column({ ...baseConfig('SERIAL'), isPrimaryKey: true, isGenerated: true, hasDefault: true }),\n\n /** Unlimited-length text. Use .min()/.max() for Zod length validation. */\n text: (): Column<string, false, false> =>\n new Column(baseConfig('TEXT')),\n\n /** 4-byte integer. For whole numbers — counts, ages, quantities. */\n int: (): Column<number, false, false> =>\n new Column(baseConfig('INTEGER')),\n\n /**\n * Alias for kadak.int(). Maps to INTEGER.\n * ⚠️ This is NOT floating point — 9.99 becomes 9 in the database.\n * Use kadak.float() for decimals or kadak.decimal() for money.\n */\n number: (): Column<number, false, false> =>\n new Column(baseConfig('INTEGER')),\n\n /**\n * 8-byte floating point (DOUBLE PRECISION).\n * For scientific data, coordinates, measurements.\n * ⚠️ NEVER use for money — use kadak.decimal() instead.\n */\n float: (): Column<number, false, false> =>\n new Column(baseConfig('FLOAT8')),\n\n /**\n * Arbitrary precision decimal (NUMERIC).\n * Returns STRING in TypeScript — intentional to preserve precision.\n * JS floats lose precision: 0.1 + 0.2 !== 0.3\n * ✅ Use for: money, prices, financial math\n */\n decimal: (): Column<string, false, false> =>\n new Column(baseConfig('NUMERIC')),\n\n /** Boolean true/false */\n boolean: (): Column<boolean, false, false> =>\n new Column(baseConfig('BOOLEAN')),\n\n /**\n * Timestamp with timezone — always TIMESTAMPTZ.\n * Prevents the #1 PostgreSQL date/time footgun (timezone-naive timestamps).\n * Supports .default('now') for SQL DEFAULT now().\n */\n timestamp: (): Column<Date, false, false> =>\n new Column(baseConfig('TIMESTAMPTZ')),\n\n /**\n * TEXT with automatic Zod email validation.\n * PostgreSQL has no EMAIL type — this is TEXT underneath.\n * The email check is applied for free in validator().\n */\n email: (): Column<string, false, false> =>\n new Column({ ...baseConfig('TEXT'), zodCheck: 'email' }),\n\n /** UUID column (non-primary-key). For foreign keys, tokens, etc. */\n uuid: (): Column<string, false, false> =>\n new Column({ ...baseConfig('UUID'), zodCheck: 'uuid' }),\n\n /**\n * JSONB column with required Zod schema for runtime validation.\n * The schema provides BOTH the TS type (via z.infer) AND runtime validation.\n *\n * For unstructured JSON, pass z.unknown():\n * ```ts\n * metadata: kadak.json(z.unknown())\n * ```\n */\n json: <T extends z.ZodType>(schema: T): Column<z.infer<T>, false, false> =>\n new Column({ ...baseConfig('JSONB'), zodSchema: schema }),\n} as const\n\n// ============================================================\n// Table Options — table-level constraints\n// ============================================================\n\n/** Table-level constraints beyond what individual columns express */\nexport interface TableOptions {\n /**\n * Compound unique constraints — each array is a group of columns\n * that must be unique together.\n * ```ts\n * { unique: [['userId', 'teamId']] }\n * ```\n */\n unique?: string[][]\n}\n\n// ============================================================\n// Table Class\n// ============================================================\n\ntype ColumnsRecord = Record<string, Column<any, any, any>>\n\n/**\n * A table definition. Created via the `table()` function.\n * Holds column metadata and generates typed Zod validators.\n */\nexport class Table<TName extends string, TColumns extends ColumnsRecord> {\n readonly _name: TName\n readonly _columns: TColumns\n readonly _options?: TableOptions\n /** Maps camelCase TS keys → snake_case PG column names */\n readonly _columnMap: Record<string, string>\n\n constructor(name: TName, columns: TColumns, options?: TableOptions) {\n this._name = name\n this._columns = columns\n this._options = options\n\n // Pre-compute mapping once — reused by query builder and DDL generator\n this._columnMap = {}\n for (const key of Object.keys(columns)) {\n this._columnMap[key] = camelToSnake(key)\n }\n }\n\n /** Alias for insertValidator() — the most common use case (validating user input) */\n validator() {\n return this.insertValidator()\n }\n\n /** Validates data for INSERT — generated/defaulted fields are optional */\n insertValidator() {\n return z.object(this._buildShape('insert'))\n }\n\n /** Validates data for SELECT — all columns present, nullability applied */\n selectValidator() {\n return z.object(this._buildShape('select'))\n }\n\n /** Validates data for UPDATE — everything optional (partial update) */\n updateValidator() {\n return z.object(this._buildShape('update'))\n }\n\n /** @internal Builds Zod shape for a given operation mode */\n private _buildShape(mode: 'insert' | 'select' | 'update'): Record<string, z.ZodType> {\n const shape: Record<string, z.ZodType> = {}\n\n for (const [key, col] of Object.entries(this._columns)) {\n let zodType = this._baseZodType(col._config)\n\n // Apply nullable before optional — Zod order matters\n if (col._config.nullable) {\n zodType = zodType.nullable()\n }\n\n // Mode-specific optionality\n if (mode === 'insert' && (col._config.hasDefault || col._config.isGenerated)) {\n zodType = zodType.optional()\n } else if (mode === 'update') {\n // Everything is optional in an update — you only send what changed\n zodType = zodType.optional()\n }\n // mode === 'select' — nothing is optional, every column comes back from DB\n\n shape[key] = zodType\n }\n\n return shape\n }\n\n /** @internal Creates the base Zod type for a column, without nullable/optional */\n private _baseZodType(config: ColumnConfig): z.ZodType {\n // JSONB columns carry their own Zod schema — use it directly\n if (config.pgType === 'JSONB' && config.zodSchema) {\n return config.zodSchema\n }\n\n switch (config.pgType) {\n case 'TEXT': {\n let s = z.string()\n if (config.zodCheck === 'email') s = s.email()\n if (config.min !== undefined) s = s.min(config.min)\n if (config.max !== undefined) s = s.max(config.max)\n return s\n }\n case 'INTEGER': {\n let n = z.number().int()\n if (config.min !== undefined) n = n.min(config.min)\n if (config.max !== undefined) n = n.max(config.max)\n return n\n }\n case 'FLOAT8': {\n let n = z.number()\n if (config.min !== undefined) n = n.min(config.min)\n if (config.max !== undefined) n = n.max(config.max)\n return n\n }\n case 'NUMERIC':\n // Decimal returns string — validated as decimal format\n return z.string().regex(/^-?\\d+(\\.\\d+)?$/, 'Must be a valid decimal number')\n case 'BOOLEAN':\n return z.boolean()\n case 'TIMESTAMPTZ':\n // Coerce accepts both Date objects and ISO strings — handles API input gracefully\n return z.coerce.date()\n case 'UUID':\n return z.string().uuid()\n case 'SERIAL':\n return z.number().int()\n default:\n return z.unknown()\n }\n }\n}\n\n// ============================================================\n// table() Factory\n// ============================================================\n\n/**\n * Define a table. This is the primary API for declaring your database schema.\n *\n * ```ts\n * export const users = table('users', {\n * id: kadak.id(),\n * name: kadak.text().required(),\n * email: kadak.email().unique(),\n * age: kadak.int().optional(),\n * createdAt: kadak.timestamp().default('now'),\n * })\n * ```\n */\nexport function table<TName extends string, TColumns extends ColumnsRecord>(\n name: TName,\n columns: TColumns,\n options?: TableOptions,\n): Table<TName, TColumns> {\n return new Table(name, columns, options)\n}\n\n// ============================================================\n// Type Inference Utilities\n// ============================================================\n\n/** Extract the SELECT type for a column — what the database returns */\ntype ColumnSelectType<C> = C extends Column<infer T, infer N, any>\n ? N extends true ? T | null : T\n : never\n\n/** Keys of columns that are REQUIRED in INSERT (no default, not generated) */\ntype RequiredInsertKeys<C extends ColumnsRecord> = {\n [K in keyof C]: C[K] extends Column<any, any, true> ? never : K\n}[keyof C]\n\n/** Keys of columns that are OPTIONAL in INSERT (has default or is generated) */\ntype OptionalInsertKeys<C extends ColumnsRecord> = {\n [K in keyof C]: C[K] extends Column<any, any, true> ? K : never\n}[keyof C]\n\n/**\n * Infer the SELECT type — what you get back from queries.\n * Every column is present. Nullable columns have `| null`.\n *\n * ```ts\n * type User = Infer<typeof users>\n * // { id: number, name: string, email: string, age: number | null, createdAt: Date }\n * ```\n */\nexport type Infer<T extends Table<any, any>> = Simplify<{\n [K in keyof T['_columns']]: ColumnSelectType<T['_columns'][K]>\n}>\n\n/**\n * Infer the INSERT type — what you provide to create a row.\n * Generated/defaulted columns become optional. Nullable columns accept null.\n *\n * ```ts\n * type NewUser = InferInsert<typeof users>\n * // { name: string, email: string, age?: number | null, id?: number, createdAt?: Date }\n * ```\n */\nexport type InferInsert<T extends Table<any, any>> = Simplify<\n // Required fields — no default, must provide\n { [K in RequiredInsertKeys<T['_columns']>]: ColumnSelectType<T['_columns'][K]> } &\n // Optional fields — has default or generated, can omit\n { [K in OptionalInsertKeys<T['_columns']>]?: ColumnSelectType<T['_columns'][K]> }\n>\n\n/**\n * Infer the UPDATE type — what you provide for partial updates.\n * Everything is optional. Nullable columns accept null.\n *\n * ```ts\n * type UserUpdate = InferUpdate<typeof users>\n * // { id?: number, name?: string, email?: string, age?: number | null, createdAt?: Date }\n * ```\n */\nexport type InferUpdate<T extends Table<any, any>> = Simplify<\n Partial<Infer<T>>\n>\n","// src/error.ts — Human-readable error wrapping for PostgreSQL errors\n// Every raw pg error gets caught and wrapped before it reaches the user.\n\n/** All error codes KadakORM can produce */\nexport type KadakErrorCode =\n | 'NOT_NULL_VIOLATION'\n | 'UNIQUE_VIOLATION'\n | 'FOREIGN_KEY_VIOLATION'\n | 'TABLE_NOT_FOUND'\n | 'COLUMN_NOT_FOUND'\n | 'CONNECTION_ERROR'\n | 'AUTH_ERROR'\n | 'QUERY_TIMEOUT'\n | 'UNKNOWN'\n\n/** Maps PostgreSQL error codes to human-readable KadakORM codes + hints */\nconst PG_ERROR_MAP: Record<string, { code: KadakErrorCode; hint: string }> = {\n '23505': { code: 'UNIQUE_VIOLATION', hint: 'A row with this value already exists. Check your unique columns.' },\n '23502': { code: 'NOT_NULL_VIOLATION', hint: 'A required column is missing. Add .optional() if it should be nullable.' },\n '23503': { code: 'FOREIGN_KEY_VIOLATION', hint: 'The referenced row does not exist in the related table.' },\n '42P01': { code: 'TABLE_NOT_FOUND', hint: 'Did you run migrations? The table does not exist yet.' },\n '42703': { code: 'COLUMN_NOT_FOUND', hint: 'Check your schema — this column is not in the table.' },\n '08006': { code: 'CONNECTION_ERROR', hint: 'Cannot reach the database. Check your DATABASE_URL and network.' },\n '08001': { code: 'CONNECTION_ERROR', hint: 'Connection refused. Is PostgreSQL running?' },\n '28P01': { code: 'AUTH_ERROR', hint: 'Wrong username or password in your connection string.' },\n '3D000': { code: 'CONNECTION_ERROR', hint: 'This database does not exist. Check the database name in your URL.' },\n '57014': { code: 'QUERY_TIMEOUT', hint: 'Query took too long. Consider adding an index or simplifying the query.' },\n}\n\n/**\n * The one error class users ever see from KadakORM.\n * Always includes: what went wrong, which table/column, and how to fix it.\n */\nexport class KadakError extends Error {\n readonly code: KadakErrorCode\n readonly table?: string\n readonly column?: string\n readonly constraint?: string\n readonly hint: string\n /** Always preserved — the raw pg error for advanced debugging */\n readonly originalError: unknown\n\n constructor(opts: {\n code: KadakErrorCode\n message: string\n hint: string\n table?: string\n column?: string\n constraint?: string\n originalError: unknown\n }) {\n super(opts.message)\n this.name = 'KadakError'\n this.code = opts.code\n this.hint = opts.hint\n this.table = opts.table\n this.column = opts.column\n this.constraint = opts.constraint\n this.originalError = opts.originalError\n }\n}\n\n/** Wraps any postgres.js error into a KadakError with context */\nexport function wrapPgError(err: unknown, tableName?: string): KadakError {\n const pgErr = err as Record<string, any> | undefined\n const pgCode: string = pgErr?.code ?? ''\n const mapped = PG_ERROR_MAP[pgCode] ?? { code: 'UNKNOWN' as const, hint: 'An unexpected database error occurred.' }\n\n const table = pgErr?.table_name ?? tableName\n const column: string | undefined = pgErr?.column_name\n const constraint: string | undefined = pgErr?.constraint_name\n\n // Build a message that actually tells you what to do\n let message: string = pgErr?.message ?? 'Unknown database error'\n if (mapped.code === 'NOT_NULL_VIOLATION' && column) {\n message = `Column '${column}' in table '${table}' cannot be null. Did you forget to pass '${column}'?`\n } else if (mapped.code === 'UNIQUE_VIOLATION' && constraint) {\n message = `Duplicate value violates constraint '${constraint}' on table '${table}'.`\n } else if (mapped.code === 'TABLE_NOT_FOUND') {\n message = `Table '${table ?? 'unknown'}' does not exist. Have you run your migrations?`\n }\n\n return new KadakError({ code: mapped.code, message, hint: mapped.hint, table, column, constraint, originalError: err })\n}\n","// src/query.ts — Query builder for KadakORM\n// Thin layer over postgres.js tagged templates — no query string concatenation.\n// Handles camelCase↔snake_case, soft deletes, autoUpdate injection.\n\nimport type postgres from 'postgres'\nimport type { Table, Infer, InferInsert, InferUpdate, Column } from './schema'\nimport { wrapPgError } from './error'\n\n// ============================================================\n// Query Option Types\n// ============================================================\n\n/** Options for findMany/findFirst — without select (returns full row) */\nexport interface FindManyOptions<T extends Table<any, any>> {\n where?: Partial<Infer<T>>\n orderBy?: Partial<Record<keyof Infer<T>, 'asc' | 'desc'>>\n limit?: number\n offset?: number\n /** When true, includes soft-deleted rows */\n withDeleted?: boolean\n /** Reserved for relations — not yet implemented */\n include?: never\n}\n\n// ============================================================\n// TableClient — the query interface for a single table\n// ============================================================\n\n/**\n * Query client for a single table. Created by `connect()` — never instantiated directly.\n * Provides findMany, findFirst, insert, update, delete with full type safety.\n */\nexport class TableClient<T extends Table<any, any>> {\n /** @internal */ readonly _sql: postgres.Sql\n /** @internal */ readonly _table: T\n /** @internal */ readonly _reverseMap: Record<string, string>\n /** @internal */ _onError?: (err: Error) => void\n\n constructor(sql: postgres.Sql, table: T) {\n this._sql = sql\n this._table = table\n\n // Pre-compute reverse mapping: snake_case → camelCase\n // Used to transform query results back to JS convention\n this._reverseMap = {}\n for (const [camel, snake] of Object.entries(table._columnMap)) {\n this._reverseMap[snake] = camel\n }\n }\n\n // ----------------------------------------------------------\n // findMany — SELECT with filtering, ordering, pagination\n // ----------------------------------------------------------\n\n /** Find rows with optional select for precise return types */\n findMany<K extends keyof Infer<T>>(\n options: FindManyOptions<T> & { select: Record<K, true> }\n ): Promise<Pick<Infer<T>, K>[]>\n /** Find rows — returns full row type */\n findMany(options?: FindManyOptions<T>): Promise<Infer<T>[]>\n async findMany(options?: FindManyOptions<T> & { select?: Record<string, true> }): Promise<any[]> {\n try {\n const sql = this._sql\n const t = this._table\n\n // Build column list — either selected columns or all\n const cols = options?.select\n ? Object.keys(options.select).filter(k => options.select![k]).map(k => t._columnMap[k] ?? k)\n : Object.values(t._columnMap)\n\n // Assemble fragments\n const whereFragment = this._buildWhere(options?.where, options?.withDeleted)\n const orderFragment = this._buildOrderBy(options?.orderBy)\n const limitFragment = options?.limit !== undefined ? sql`LIMIT ${options.limit}` : sql``\n const offsetFragment = options?.offset !== undefined ? sql`OFFSET ${options.offset}` : sql``\n\n const rows = await sql`\n SELECT ${sql(cols)} FROM ${sql(t._name)}\n ${whereFragment} ${orderFragment} ${limitFragment} ${offsetFragment}\n `\n return rows.map(row => this._toJs(row))\n } catch (err) {\n throw wrapPgError(err, this._table._name)\n }\n }\n\n // ----------------------------------------------------------\n // findFirst — same as findMany but returns single row or null\n // ----------------------------------------------------------\n\n findFirst<K extends keyof Infer<T>>(\n options: FindManyOptions<T> & { select: Record<K, true> }\n ): Promise<Pick<Infer<T>, K> | null>\n findFirst(options?: FindManyOptions<T>): Promise<Infer<T> | null>\n async findFirst(options?: FindManyOptions<T> & { select?: Record<string, true> }): Promise<any> {\n const rows = await this.findMany({ ...options, limit: 1 } as any)\n return rows[0] ?? null\n }\n\n // ----------------------------------------------------------\n // insert — INSERT INTO with RETURNING *\n // ----------------------------------------------------------\n\n /** Insert a single row */\n async insert(data: InferInsert<T>): Promise<Infer<T>>\n /** Insert multiple rows */\n async insert(data: InferInsert<T>[]): Promise<Infer<T>[]>\n async insert(data: InferInsert<T> | InferInsert<T>[]): Promise<any> {\n try {\n const sql = this._sql\n const t = this._table\n const isBulk = Array.isArray(data)\n const items = isBulk ? data : [data]\n\n // Convert each item's keys from camelCase to snake_case\n const snakeItems = items.map(item => this._toDb(item as Record<string, unknown>))\n\n // postgres.js handles bulk insert when given an array of column names + values\n const cols = Object.keys(snakeItems[0])\n const rows = await sql`\n INSERT INTO ${sql(t._name)} ${sql(snakeItems, ...cols)} RETURNING *\n `\n\n const result = rows.map(row => this._toJs(row))\n return isBulk ? result : result[0]\n } catch (err) {\n throw wrapPgError(err, this._table._name)\n }\n }\n\n // ----------------------------------------------------------\n // update — UPDATE SET ... WHERE ... RETURNING *\n // ----------------------------------------------------------\n\n async update(options: { where: Partial<Infer<T>>; data: InferUpdate<T> }): Promise<Infer<T>[]> {\n try {\n const sql = this._sql\n const t = this._table\n\n // Convert update data to snake_case, stripping undefined values\n const snakeData: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(options.data as Record<string, unknown>)) {\n if (value !== undefined) {\n snakeData[t._columnMap[key] ?? key] = value\n }\n }\n\n // Auto-inject autoUpdate columns (e.g., updatedAt → new Date())\n for (const [key, col] of Object.entries(t._columns)) {\n if ((col as Column<any, any, any>)._config.autoUpdate) {\n snakeData[t._columnMap[key]] = new Date()\n }\n }\n\n const whereFragment = this._buildWhere(options.where, false)\n\n const rows = await sql`\n UPDATE ${sql(t._name)} SET ${sql(snakeData)} ${whereFragment} RETURNING *\n `\n return rows.map(row => this._toJs(row)) as Infer<T>[]\n } catch (err) {\n throw wrapPgError(err, this._table._name)\n }\n }\n\n // ----------------------------------------------------------\n // delete — soft delete if configured, otherwise hard delete\n // ----------------------------------------------------------\n\n async delete(options: { where: Partial<Infer<T>> }): Promise<Infer<T>[]> {\n // Find soft delete column if one exists\n const softDeleteEntry = Object.entries(this._table._columns)\n .find(([_, col]) => (col as Column<any, any, any>)._config.softDelete)\n\n if (softDeleteEntry) {\n // Soft delete — set the timestamp instead of deleting\n const [key] = softDeleteEntry\n const snakeCol = this._table._columnMap[key]\n return this.update({\n where: options.where,\n data: { [key]: new Date() } as any,\n })\n }\n\n return this.hardDelete(options)\n }\n\n /** Always performs a real DELETE — bypasses soft delete */\n async hardDelete(options: { where: Partial<Infer<T>> }): Promise<Infer<T>[]> {\n try {\n const sql = this._sql\n const whereFragment = this._buildWhere(options.where, true)\n\n const rows = await sql`\n DELETE FROM ${sql(this._table._name)} ${whereFragment} RETURNING *\n `\n return rows.map(row => this._toJs(row)) as Infer<T>[]\n } catch (err) {\n throw wrapPgError(err, this._table._name)\n }\n }\n\n // ============================================================\n // Internal helpers\n // ============================================================\n\n /** Builds a WHERE fragment from a JS object, including soft delete filter */\n private _buildWhere(\n where?: Partial<Infer<T>>,\n withDeleted?: boolean,\n ): postgres.PendingQuery<any> {\n const sql = this._sql\n const conditions: postgres.PendingQuery<any>[] = []\n\n // User-provided conditions\n if (where) {\n for (const [key, value] of Object.entries(where)) {\n const col = this._table._columnMap[key] ?? key\n if (value === undefined) continue\n if (value === null) {\n conditions.push(sql`${sql(col)} IS NULL`)\n } else {\n conditions.push(sql`${sql(col)} = ${value as postgres.SerializableParameter}`)\n }\n }\n }\n\n // Auto-add soft delete filter unless explicitly bypassed\n if (!withDeleted) {\n for (const [key, col] of Object.entries(this._table._columns)) {\n if ((col as Column<any, any, any>)._config.softDelete) {\n conditions.push(sql`${sql(this._table._columnMap[key])} IS NULL`)\n }\n }\n }\n\n if (conditions.length === 0) return sql``\n\n // Join conditions with AND using postgres.js fragment composition\n let combined = conditions[0]\n for (let i = 1; i < conditions.length; i++) {\n combined = sql`${combined} AND ${conditions[i]}`\n }\n return sql`WHERE ${combined}`\n }\n\n /** Builds an ORDER BY fragment */\n private _buildOrderBy(\n orderBy?: Partial<Record<string, 'asc' | 'desc'>>,\n ): postgres.PendingQuery<any> {\n const sql = this._sql\n if (!orderBy) return sql``\n\n const entries = Object.entries(orderBy)\n if (entries.length === 0) return sql``\n\n const parts = entries.map(([key, dir]) => {\n const col = this._table._columnMap[key] ?? key\n // postgres.js unsafe() needed for ASC/DESC since they're SQL keywords, not values\n return sql`${sql(col)} ${dir === 'desc' ? sql`DESC` : sql`ASC`}`\n })\n\n let combined = parts[0]\n for (let i = 1; i < parts.length; i++) {\n combined = sql`${combined}, ${parts[i]}`\n }\n return sql`ORDER BY ${combined}`\n }\n\n /** Converts a JS object (camelCase) to DB format (snake_case) */\n private _toDb(obj: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(obj)) {\n if (value !== undefined) {\n result[this._table._columnMap[key] ?? key] = value\n }\n }\n return result\n }\n\n /** Converts a DB row (snake_case) back to JS format (camelCase) */\n private _toJs(row: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n for (const [snake, value] of Object.entries(row as Record<string, unknown>)) {\n result[this._reverseMap[snake] ?? snake] = value\n }\n return result\n }\n}\n","// src/connect.ts — PostgreSQL connection with sensible defaults\n// SSL auto-detection, pooling, and typed database client creation.\n\nimport postgres from 'postgres'\nimport type { Table } from './schema'\nimport { TableClient } from './query'\n\n/** Connection config for production use */\nexport interface ConnectionConfig {\n /** PostgreSQL connection URL */\n url: string\n /** Max connections in pool — default 10 */\n max?: number\n /**\n * SSL mode — default: auto-detected.\n * Localhost → false. Everything else → 'require'.\n * This means Supabase, Neon, Railway, Vercel Postgres work out of the box.\n */\n ssl?: boolean | 'require' | 'prefer' | 'allow'\n /** Query timeout in seconds */\n timeout?: number\n /** Called on connection-level errors */\n onError?: (err: Error) => void\n}\n\n/** The database client — each key is a table name mapped to its query client */\nexport type Database<TTables extends Record<string, Table<any, any>>> = {\n [K in keyof TTables]: TTables[K] extends Table<any, any> ? TableClient<TTables[K]> : never\n} & {\n /** Close all connections in the pool */\n close(): Promise<void>\n /** Raw postgres.js client — escape hatch for custom queries */\n sql: postgres.Sql\n}\n\n/**\n * Connect to PostgreSQL and create a typed database client.\n *\n * ```ts\n * const db = connect('postgresql://user:pass@localhost/mydb', { users, posts })\n * const allUsers = await db.users.findMany()\n * await db.close()\n * ```\n */\nexport function connect<TTables extends Record<string, Table<any, any>>>(\n config: string | ConnectionConfig,\n tables: TTables,\n): Database<TTables> {\n const isString = typeof config === 'string'\n const url = isString ? config : config.url\n const opts = isString ? {} as ConnectionConfig : config\n\n // Auto-detect SSL: localhost = no SSL, everything else = require\n // This single default prevents 90% of \"connection refused\" issues\n // with managed PostgreSQL services\n const isLocal = url.includes('localhost') || url.includes('127.0.0.1')\n const ssl = opts.ssl ?? (isLocal ? false : 'require')\n\n const sql = postgres(url, {\n max: opts.max ?? 10,\n idle_timeout: 20,\n connect_timeout: 30,\n ssl,\n max_lifetime: 60 * 30,\n onnotice: () => {}, // suppress PostgreSQL NOTICE messages — they confuse beginners\n })\n\n // Create a TableClient for each registered table\n const db = {} as any\n for (const [key, table] of Object.entries(tables)) {\n const client = new TableClient(sql, table as Table<any, any>)\n if (opts.onError) client._onError = opts.onError\n db[key] = client\n }\n\n db.close = () => sql.end()\n db.sql = sql\n\n return db as Database<TTables>\n}\n"],"mappings":"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"}
|