@shyk/kadak 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,60 +1,34 @@
1
- import { z } from "zod";
1
+ import { ZodError, z } from "zod";
2
2
  import postgres from "postgres";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
3
5
  //#region src/schema.ts
4
- /** Converts camelCase to snake_case for PostgreSQL column naming */
5
6
  function camelToSnake(str) {
6
7
  return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
7
8
  }
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
9
  var Column = class Column {
19
10
  constructor(config) {
20
11
  this._config = Object.freeze({ ...config });
21
12
  }
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
13
  optional() {
31
14
  return new Column({
32
15
  ...this._config,
33
- nullable: true
16
+ nullable: true,
17
+ isOptional: true
34
18
  });
35
19
  }
36
- /** Add UNIQUE constraint */
37
20
  unique() {
38
21
  return new Column({
39
22
  ...this._config,
40
23
  isUnique: true
41
24
  });
42
25
  }
43
- /** Mark as PRIMARY KEY */
44
26
  primaryKey() {
45
27
  return new Column({
46
28
  ...this._config,
47
29
  isPrimaryKey: true
48
30
  });
49
31
  }
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
32
  default(value) {
59
33
  const shortcuts = {
60
34
  TIMESTAMPTZ: { now: "now()" },
@@ -68,21 +42,18 @@ var Column = class Column {
68
42
  defaultSql: sqlExpr ?? this._config.defaultSql
69
43
  });
70
44
  }
71
- /** Min constraint — string length for text, numeric value for numbers */
72
45
  min(n) {
73
46
  return new Column({
74
47
  ...this._config,
75
48
  min: n
76
49
  });
77
50
  }
78
- /** Max constraint — string length for text, numeric value for numbers */
79
51
  max(n) {
80
52
  return new Column({
81
53
  ...this._config,
82
54
  max: n
83
55
  });
84
56
  }
85
- /** Auto-update on every UPDATE query — designed for updatedAt columns */
86
57
  autoUpdate() {
87
58
  return new Column({
88
59
  ...this._config,
@@ -90,12 +61,6 @@ var Column = class Column {
90
61
  hasDefault: true
91
62
  });
92
63
  }
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
64
  softDelete() {
100
65
  return new Column({
101
66
  ...this._config,
@@ -105,7 +70,6 @@ var Column = class Column {
105
70
  });
106
71
  }
107
72
  };
108
- /** Base config with required-by-default semantics */
109
73
  function baseConfig(pgType) {
110
74
  return {
111
75
  pgType,
@@ -115,20 +79,10 @@ function baseConfig(pgType) {
115
79
  isUnique: false,
116
80
  isGenerated: false,
117
81
  autoUpdate: false,
118
- softDelete: false
82
+ softDelete: false,
83
+ isOptional: false
119
84
  };
120
85
  }
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
86
  const kadak = {
133
87
  id: () => new Column({
134
88
  ...baseConfig("INTEGER"),
@@ -168,10 +122,6 @@ const kadak = {
168
122
  zodSchema: schema
169
123
  })
170
124
  };
171
- /**
172
- * A table definition. Created via the `table()` function.
173
- * Holds column metadata and generates typed Zod validators.
174
- */
175
125
  var Table = class {
176
126
  constructor(name, columns, options) {
177
127
  this._name = name;
@@ -180,35 +130,29 @@ var Table = class {
180
130
  this._columnMap = {};
181
131
  for (const key of Object.keys(columns)) this._columnMap[key] = camelToSnake(key);
182
132
  }
183
- /** Alias for insertValidator() — the most common use case (validating user input) */
184
133
  validator() {
185
134
  return this.insertValidator();
186
135
  }
187
- /** Validates data for INSERT — generated/defaulted fields are optional */
188
136
  insertValidator() {
189
137
  return z.object(this._buildShape("insert"));
190
138
  }
191
- /** Validates data for SELECT — all columns present, nullability applied */
192
139
  selectValidator() {
193
140
  return z.object(this._buildShape("select"));
194
141
  }
195
- /** Validates data for UPDATE — everything optional (partial update) */
196
142
  updateValidator() {
197
143
  return z.object(this._buildShape("update"));
198
144
  }
199
- /** @internal Builds Zod shape for a given operation mode */
200
145
  _buildShape(mode) {
201
146
  const shape = {};
202
147
  for (const [key, col] of Object.entries(this._columns)) {
203
148
  let zodType = this._baseZodType(col._config);
204
149
  if (col._config.nullable) zodType = zodType.nullable();
205
- if (mode === "insert" && (col._config.hasDefault || col._config.isGenerated)) zodType = zodType.optional();
150
+ if (mode === "insert" && (col._config.hasDefault || col._config.isGenerated || col._config.isOptional)) zodType = zodType.optional();
206
151
  else if (mode === "update") zodType = zodType.optional();
207
152
  shape[key] = zodType;
208
153
  }
209
154
  return shape;
210
155
  }
211
- /** @internal Creates the base Zod type for a column, without nullable/optional */
212
156
  _baseZodType(config) {
213
157
  if (config.pgType === "JSONB" && config.zodSchema) return config.zodSchema;
214
158
  switch (config.pgType) {
@@ -240,25 +184,66 @@ var Table = class {
240
184
  }
241
185
  }
242
186
  };
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
- */
187
+ function fromShorthand(key, definition) {
188
+ const optional = definition.endsWith("?");
189
+ const base = optional ? definition.slice(0, -1) : definition;
190
+ let column;
191
+ switch (base) {
192
+ case "id":
193
+ column = kadak.id();
194
+ break;
195
+ case "uuidId":
196
+ column = kadak.uuidId();
197
+ break;
198
+ case "serialId":
199
+ column = kadak.serialId();
200
+ break;
201
+ case "text":
202
+ column = kadak.text();
203
+ break;
204
+ case "int":
205
+ column = kadak.int();
206
+ break;
207
+ case "number":
208
+ column = kadak.number();
209
+ break;
210
+ case "float":
211
+ column = kadak.float();
212
+ break;
213
+ case "decimal":
214
+ column = kadak.decimal();
215
+ break;
216
+ case "boolean":
217
+ column = kadak.boolean();
218
+ break;
219
+ case "timestamp":
220
+ column = kadak.timestamp();
221
+ break;
222
+ case "email":
223
+ column = kadak.email().unique();
224
+ break;
225
+ case "uuid":
226
+ column = kadak.uuid();
227
+ break;
228
+ default: throw new Error(`Unsupported shorthand '${definition}' for column '${key}'`);
229
+ }
230
+ return optional ? column.optional() : column;
231
+ }
232
+ function normalizeColumns(columns, options) {
233
+ const normalized = {};
234
+ for (const [key, value] of Object.entries(columns)) normalized[key] = typeof value === "string" ? fromShorthand(key, value) : value;
235
+ if (options?.timestamps) {
236
+ if (!("createdAt" in normalized)) normalized.createdAt = kadak.timestamp().default("now");
237
+ if (!("updatedAt" in normalized)) normalized.updatedAt = kadak.timestamp().default("now").autoUpdate();
238
+ }
239
+ if (options?.softDelete && !("deletedAt" in normalized)) normalized.deletedAt = kadak.timestamp().softDelete();
240
+ return normalized;
241
+ }
256
242
  function table(name, columns, options) {
257
- return new Table(name, columns, options);
243
+ return new Table(name, normalizeColumns(columns, options), options);
258
244
  }
259
245
  //#endregion
260
246
  //#region src/error.ts
261
- /** Maps PostgreSQL error codes to human-readable KadakORM codes + hints */
262
247
  const PG_ERROR_MAP = {
263
248
  "23505": {
264
249
  code: "UNIQUE_VIOLATION",
@@ -266,7 +251,7 @@ const PG_ERROR_MAP = {
266
251
  },
267
252
  "23502": {
268
253
  code: "NOT_NULL_VIOLATION",
269
- hint: "A required column is missing. Add .optional() if it should be nullable."
254
+ hint: "A required column is missing. Add ? in schema if it should be optional."
270
255
  },
271
256
  "23503": {
272
257
  code: "FOREIGN_KEY_VIOLATION",
@@ -301,10 +286,6 @@ const PG_ERROR_MAP = {
301
286
  hint: "Query took too long. Consider adding an index or simplifying the query."
302
287
  }
303
288
  };
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
289
  var KadakError = class extends Error {
309
290
  constructor(opts) {
310
291
  super(opts.message);
@@ -317,7 +298,27 @@ var KadakError = class extends Error {
317
298
  this.originalError = opts.originalError;
318
299
  }
319
300
  };
320
- /** Wraps any postgres.js error into a KadakError with context */
301
+ function fromValidationError(error, tableName) {
302
+ if (error instanceof ZodError) {
303
+ const issue = error.issues[0];
304
+ const path = issue?.path.join(".") || "payload";
305
+ return new KadakError({
306
+ code: "VALIDATION_ERROR",
307
+ message: `Missing or invalid field: ${path}`,
308
+ hint: `Fix: ${issue?.message ?? "Check the value against your schema."}`,
309
+ table: tableName,
310
+ column: path,
311
+ originalError: error
312
+ });
313
+ }
314
+ return new KadakError({
315
+ code: "VALIDATION_ERROR",
316
+ message: "Invalid input for this query.",
317
+ hint: "Check the payload against your schema validators.",
318
+ table: tableName,
319
+ originalError: error
320
+ });
321
+ }
321
322
  function wrapPgError(err, tableName) {
322
323
  const pgErr = err;
323
324
  const mapped = PG_ERROR_MAP[pgErr?.code ?? ""] ?? {
@@ -328,147 +329,553 @@ function wrapPgError(err, tableName) {
328
329
  const column = pgErr?.column_name;
329
330
  const constraint = pgErr?.constraint_name;
330
331
  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?`;
332
+ if (mapped.code === "NOT_NULL_VIOLATION" && column) message = `Missing required field: ${column}`;
333
+ else if (mapped.code === "UNIQUE_VIOLATION") message = constraint ? `A record already exists with this value (violates '${constraint}')` : "A duplicate record already exists.";
334
+ else if (mapped.code === "TABLE_NOT_FOUND") message = `Table '${table ?? "unknown"}' does not exist.`;
334
335
  return new KadakError({
335
336
  code: mapped.code,
336
337
  message,
337
338
  hint: mapped.hint,
338
339
  table,
339
- column,
340
+ column: column || (constraint?.includes(table || "") ? constraint.replace(table + "_", "").replace("_key", "") : void 0),
340
341
  constraint,
341
342
  originalError: err
342
343
  });
343
344
  }
344
345
  //#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
- */
346
+ //#region src/query/analyzer.ts
347
+ function collectFields(condition) {
348
+ if (!condition) return [];
349
+ if (condition.isInternal) return [];
350
+ switch (condition.type) {
351
+ case "eq":
352
+ case "isNull":
353
+ case "in":
354
+ case "gt":
355
+ case "gte":
356
+ case "lt":
357
+ case "lte": return [condition.field];
358
+ case "and":
359
+ case "or": return condition.conditions.flatMap((entry) => collectFields(entry));
360
+ }
361
+ }
362
+ function isIndexedField(table, dbField) {
363
+ const jsField = Object.entries(table._columnMap).find(([, value]) => value === dbField)?.[0];
364
+ if (!jsField) return false;
365
+ const column = table._columns[jsField];
366
+ if (!column) return false;
367
+ return column._config.isPrimaryKey || column._config.isUnique;
368
+ }
369
+ function hasUserFilter(condition) {
370
+ if (!condition) return false;
371
+ if (condition.isInternal) return false;
372
+ if (condition.type === "and" || condition.type === "or") return condition.conditions.some((c) => hasUserFilter(c));
373
+ return true;
374
+ }
375
+ function analyzeAst(ast, table, config) {
376
+ const warnings = [];
377
+ const where = ast.type === "insert" ? void 0 : ast.where;
378
+ const hasFilter = hasUserFilter(where);
379
+ if (ast.type === "select" && !hasFilter) warnings.push({
380
+ code: "full_scan",
381
+ message: `Query on "${ast.table}" has no filter. This may cause a full table scan.`,
382
+ suggestion: "Add a filter like { id: 1 } or another narrowing field."
383
+ });
384
+ if (ast.type === "select" && ast.limit === void 0) warnings.push({
385
+ code: "missing_limit",
386
+ message: `Query on "${ast.table}" has no limit. This may load large datasets.`,
387
+ suggestion: "Add $limit: 100"
388
+ });
389
+ if (ast.type === "select" && ast.limit !== void 0 && ast.limit > config.largeLimitThreshold) warnings.push({
390
+ code: "large_limit",
391
+ message: `Query on "${ast.table}" uses a large limit (${ast.limit}).`,
392
+ suggestion: "Reduce the limit or paginate in smaller batches."
393
+ });
394
+ if ((ast.type === "update" || ast.type === "delete") && !hasFilter) warnings.push({
395
+ code: "unsafe_mutation",
396
+ message: `${ast.type.toUpperCase()} on "${ast.table}" has no filter and will affect every row.`,
397
+ suggestion: `Add a filter to ${ast.type} only the rows you intend to change.`
398
+ });
399
+ const nonIndexedFields = Array.from(new Set(collectFields(where))).filter((field) => !isIndexedField(table, field));
400
+ if (nonIndexedFields.length > 0 && (ast.type === "select" || ast.type === "update" || ast.type === "delete")) warnings.push({
401
+ code: "index_hint",
402
+ message: `Query on "${ast.table}" filters on non-indexed fields: ${nonIndexedFields.join(", ")}.`,
403
+ suggestion: "Consider adding an index if this query is common."
404
+ });
405
+ return warnings;
406
+ }
407
+ function analyzeExecutionWithConfig(ast, metrics, config) {
408
+ const warnings = [];
409
+ if (metrics.durationMs >= config.slowQueryMs) warnings.push({
410
+ code: "slow_query",
411
+ message: `Query on "${ast.table}" took ${metrics.durationMs}ms.`,
412
+ suggestion: "Check filters, indexes, or reduce the result size."
413
+ });
414
+ if (metrics.rows >= config.largeResultRows) warnings.push({
415
+ code: "large_result",
416
+ message: `Query on "${ast.table}" returned ${metrics.rows} rows.`,
417
+ suggestion: "Add a stricter filter or a smaller $limit."
418
+ });
419
+ return warnings;
420
+ }
421
+ function repeatedQueryWarning(table, count) {
422
+ return {
423
+ code: "repeated_query",
424
+ message: `Query pattern on "${table}" repeated ${count} times in this process.`,
425
+ suggestion: "Consider caching, batching, or moving this pattern behind a single query."
426
+ };
427
+ }
428
+ function formatWarning(warning) {
429
+ const lines = ["Kadak Warning:", warning.message];
430
+ if (warning.suggestion) lines.push("", "Suggestion:", `-> ${warning.suggestion}`);
431
+ return lines.join("\n");
432
+ }
433
+ //#endregion
434
+ //#region src/query/ast.ts
435
+ function andConditions(conditions) {
436
+ const compact = conditions.filter((condition) => Boolean(condition));
437
+ if (compact.length === 0) return void 0;
438
+ if (compact.length === 1) return compact[0];
439
+ return {
440
+ type: "and",
441
+ conditions: compact
442
+ };
443
+ }
444
+ function orConditions(conditions) {
445
+ const compact = conditions.filter((condition) => Boolean(condition));
446
+ if (compact.length === 0) return void 0;
447
+ if (compact.length === 1) return compact[0];
448
+ return {
449
+ type: "or",
450
+ conditions: compact
451
+ };
452
+ }
453
+ //#endregion
454
+ //#region src/query/normalize.ts
455
+ function isOperatorObject(value) {
456
+ if (!value || typeof value !== "object" || Array.isArray(value) || value instanceof Date) return false;
457
+ return Object.keys(value).some((key) => key.startsWith("$"));
458
+ }
459
+ function normalizeFieldValue(field, value) {
460
+ if (value === void 0) return void 0;
461
+ if (value === null) return {
462
+ type: "isNull",
463
+ field
464
+ };
465
+ if (Array.isArray(value)) return {
466
+ type: "in",
467
+ field,
468
+ values: value
469
+ };
470
+ if (!isOperatorObject(value)) return {
471
+ type: "eq",
472
+ field,
473
+ value
474
+ };
475
+ const operatorValue = value;
476
+ return andConditions([
477
+ operatorValue.$in ? {
478
+ type: "in",
479
+ field,
480
+ values: operatorValue.$in
481
+ } : void 0,
482
+ operatorValue.$gt !== void 0 ? {
483
+ type: "gt",
484
+ field,
485
+ value: operatorValue.$gt
486
+ } : void 0,
487
+ operatorValue.$gte !== void 0 ? {
488
+ type: "gte",
489
+ field,
490
+ value: operatorValue.$gte
491
+ } : void 0,
492
+ operatorValue.$lt !== void 0 ? {
493
+ type: "lt",
494
+ field,
495
+ value: operatorValue.$lt
496
+ } : void 0,
497
+ operatorValue.$lte !== void 0 ? {
498
+ type: "lte",
499
+ field,
500
+ value: operatorValue.$lte
501
+ } : void 0
502
+ ]);
503
+ }
504
+ function normalizeWhere(table, input) {
505
+ if (!input) return void 0;
506
+ const { $or, $limit, $order, ...plainFields } = input;
507
+ const conditions = [];
508
+ for (const [key, value] of Object.entries(plainFields)) {
509
+ const field = table._columnMap[key] ?? key;
510
+ conditions.push(normalizeFieldValue(field, value));
511
+ }
512
+ if (Array.isArray($or)) conditions.push(orConditions($or.map((entry) => normalizeWhere(table, entry))));
513
+ return andConditions(conditions);
514
+ }
515
+ function normalizeOrder(table, order) {
516
+ return Object.entries(order ?? {}).map(([key, direction]) => ({
517
+ field: table._columnMap[key] ?? key,
518
+ direction: direction === "desc" ? "desc" : "asc"
519
+ }));
520
+ }
521
+ function normalizeSelect(table, query, options) {
522
+ const softDeleteCondition = options?.includeSoftDeleted ? void 0 : normalizeSoftDelete(table);
523
+ const explicitLimit = typeof query?.$limit === "number" ? query.$limit : void 0;
524
+ return {
525
+ type: "select",
526
+ table: table._name,
527
+ where: andConditions([normalizeWhere(table, query), softDeleteCondition]),
528
+ order: normalizeOrder(table, query?.$order),
529
+ limit: explicitLimit ?? options?.defaultLimit
530
+ };
531
+ }
532
+ function normalizeUpdate(table, filter, data) {
533
+ const entries = Object.entries(data).filter(([, value]) => value !== void 0).map(([key, value]) => ({
534
+ field: table._columnMap[key] ?? key,
535
+ value
536
+ }));
537
+ return {
538
+ type: "update",
539
+ table: table._name,
540
+ where: normalizeWhere(table, filter),
541
+ data: entries
542
+ };
543
+ }
544
+ function normalizeDelete(table, filter) {
545
+ return {
546
+ type: "delete",
547
+ table: table._name,
548
+ where: normalizeWhere(table, filter)
549
+ };
550
+ }
551
+ function normalizeInsert(table, rows) {
552
+ const jsColumns = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
553
+ return {
554
+ type: "insert",
555
+ table: table._name,
556
+ columns: jsColumns.map((key) => table._columnMap[key] ?? key),
557
+ rows: rows.map((row) => jsColumns.map((columnKey) => columnKey in row ? {
558
+ kind: "value",
559
+ value: row[columnKey]
560
+ } : { kind: "default" }))
561
+ };
562
+ }
563
+ function normalizeSoftDelete(table) {
564
+ for (const [key, column] of Object.entries(table._columns)) if (column._config.softDelete) return {
565
+ type: "isNull",
566
+ field: table._columnMap[key],
567
+ isInternal: true
568
+ };
569
+ }
570
+ //#endregion
571
+ //#region src/query/sql.ts
572
+ function quoteIdentifier(value) {
573
+ return `"${value.replace(/"/g, "\"\"")}"`;
574
+ }
575
+ function buildCondition(condition, params) {
576
+ if (!condition) return "";
577
+ switch (condition.type) {
578
+ case "eq":
579
+ params.push(condition.value);
580
+ return `${quoteIdentifier(condition.field)} = $${params.length}`;
581
+ case "isNull": return `${quoteIdentifier(condition.field)} IS NULL`;
582
+ case "in":
583
+ if (condition.values.length === 0) return "FALSE";
584
+ return `${quoteIdentifier(condition.field)} IN (${condition.values.map((value) => {
585
+ params.push(value);
586
+ return `$${params.length}`;
587
+ }).join(", ")})`;
588
+ case "gt":
589
+ params.push(condition.value);
590
+ return `${quoteIdentifier(condition.field)} > $${params.length}`;
591
+ case "gte":
592
+ params.push(condition.value);
593
+ return `${quoteIdentifier(condition.field)} >= $${params.length}`;
594
+ case "lt":
595
+ params.push(condition.value);
596
+ return `${quoteIdentifier(condition.field)} < $${params.length}`;
597
+ case "lte":
598
+ params.push(condition.value);
599
+ return `${quoteIdentifier(condition.field)} <= $${params.length}`;
600
+ case "and": return `(${condition.conditions.map((entry) => buildCondition(entry, params)).join(" AND ")})`;
601
+ case "or": return `(${condition.conditions.map((entry) => buildCondition(entry, params)).join(" OR ")})`;
602
+ }
603
+ }
604
+ function buildWhere(where, params) {
605
+ const sql = buildCondition(where, params);
606
+ return sql ? ` WHERE ${sql}` : "";
607
+ }
608
+ function buildSelect(ast) {
609
+ const params = [];
610
+ const where = buildWhere(ast.where, params);
611
+ const order = ast.order.length > 0 ? ` ORDER BY ${ast.order.map((entry) => `${quoteIdentifier(entry.field)} ${entry.direction.toUpperCase()}`).join(", ")}` : "";
612
+ const limit = ast.limit !== void 0 ? ` LIMIT ${ast.limit}` : "";
613
+ return {
614
+ text: `SELECT * FROM ${quoteIdentifier(ast.table)}${where}${order}${limit}`,
615
+ params
616
+ };
617
+ }
618
+ function buildUpdate(ast) {
619
+ const params = [];
620
+ const setSql = ast.data.map((entry) => {
621
+ params.push(entry.value);
622
+ return `${quoteIdentifier(entry.field)} = $${params.length}`;
623
+ }).join(", ");
624
+ const where = ast.where ? ` WHERE ${buildCondition(ast.where, params)}` : "";
625
+ return {
626
+ text: `UPDATE ${quoteIdentifier(ast.table)} SET ${setSql}${where} RETURNING *`,
627
+ params
628
+ };
629
+ }
630
+ function buildDelete(ast) {
631
+ const params = [];
632
+ const where = buildWhere(ast.where, params);
633
+ return {
634
+ text: `DELETE FROM ${quoteIdentifier(ast.table)}${where} RETURNING *`,
635
+ params
636
+ };
637
+ }
638
+ function buildInsert(ast) {
639
+ if (ast.columns.length === 0) return {
640
+ text: `INSERT INTO ${quoteIdentifier(ast.table)} DEFAULT VALUES RETURNING *`,
641
+ params: []
642
+ };
643
+ const params = [];
644
+ const values = ast.rows.map((row) => `(${row.map((cell) => {
645
+ if (cell.kind === "default") return "DEFAULT";
646
+ params.push(cell.value);
647
+ return `$${params.length}`;
648
+ }).join(", ")})`).join(", ");
649
+ return {
650
+ text: `INSERT INTO ${quoteIdentifier(ast.table)} (${ast.columns.map(quoteIdentifier).join(", ")}) VALUES ${values} RETURNING *`,
651
+ params
652
+ };
653
+ }
654
+ function buildSql(ast) {
655
+ switch (ast.type) {
656
+ case "select": return buildSelect(ast);
657
+ case "update": return buildUpdate(ast);
658
+ case "delete": return buildDelete(ast);
659
+ case "insert": return buildInsert(ast);
660
+ }
661
+ }
662
+ //#endregion
663
+ //#region src/query/index.ts
350
664
  var TableClient = class {
351
- constructor(sql, table) {
665
+ constructor(sql, table, config = {}) {
352
666
  this._sql = sql;
667
+ this._readSql = config.readSql ?? [];
353
668
  this._table = table;
669
+ this._debug = config.debug ?? false;
670
+ this._warn = config.warn ?? ((message) => console.warn(message));
671
+ this._onQuery = config.onQuery;
672
+ this._warningMode = config.warningMode ?? "relaxed";
673
+ this._retryAttempts = Math.max(0, config.retryAttempts ?? 0);
674
+ this._retryDelayMs = Math.max(0, config.retryDelayMs ?? 200);
675
+ this._retryOnCodes = new Set(config.retryOnCodes ?? [
676
+ "08006",
677
+ "08001",
678
+ "57P01",
679
+ "40001",
680
+ "40P01"
681
+ ]);
682
+ this._retryWrites = config.retryWrites ?? false;
683
+ this._defaultLimit = config.defaultLimit;
684
+ this._validateResults = config.validateResults ?? false;
685
+ this._analyzerConfig = {
686
+ slowQueryMs: config.slowQueryMs ?? 250,
687
+ largeResultRows: config.largeResultRows ?? 1e3,
688
+ largeLimitThreshold: config.largeLimitThreshold ?? 1e3
689
+ };
690
+ this._repeatedQueryThreshold = config.repeatedQueryThreshold ?? 25;
691
+ this._queryFingerprintCount = /* @__PURE__ */ new Map();
692
+ this._selectValidator = table.selectValidator();
693
+ this._explainAnalyze = config.explainAnalyze ?? false;
694
+ this._explainThresholdMs = config.explainThresholdMs ?? 300;
695
+ this._metrics = config.metrics;
696
+ this._replicaIndex = 0;
354
697
  this._reverseMap = {};
355
698
  for (const [camel, snake] of Object.entries(table._columnMap)) this._reverseMap[snake] = camel;
356
699
  }
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
- }
700
+ async findMany(query) {
701
+ const ast = normalizeSelect(this._table, query, { defaultLimit: this._defaultLimit });
702
+ return this._executeAst(ast);
373
703
  }
374
- async findFirst(options) {
704
+ async findFirst(query) {
375
705
  return (await this.findMany({
376
- ...options,
377
- limit: 1
706
+ ...query ?? {},
707
+ $limit: 1
378
708
  }))[0] ?? null;
379
709
  }
380
710
  async insert(data) {
711
+ const isBulk = Array.isArray(data);
712
+ const validator = this._table.insertValidator();
713
+ const rawRows = (isBulk ? data : [data]).map((row) => this._applyInsertAutomation(this._parseOrThrow(validator, row)));
714
+ const ast = normalizeInsert(this._table, rawRows);
715
+ const rows = await this._executeAst(ast);
716
+ return isBulk ? rows : rows[0];
717
+ }
718
+ async update(filter, data) {
719
+ const parsed = this._applyUpdateAutomation(this._parseOrThrow(this._table.updateValidator(), data));
720
+ const ast = normalizeUpdate(this._table, filter, parsed);
721
+ if (ast.data.length === 0) throw new KadakError({
722
+ code: "VALIDATION_ERROR",
723
+ message: `Nothing to update in table '${this._table._name}'.`,
724
+ hint: "Pass at least one defined field in the update payload.",
725
+ table: this._table._name,
726
+ originalError: null
727
+ });
728
+ return this._executeAst(ast);
729
+ }
730
+ async delete(filter) {
731
+ const softDeleteKey = this._getSoftDeleteKey();
732
+ if (softDeleteKey) return this.update(filter, { [softDeleteKey]: /* @__PURE__ */ new Date() });
733
+ return this.hardDelete(filter);
734
+ }
735
+ async hardDelete(filter) {
736
+ const ast = normalizeDelete(this._table, filter);
737
+ return this._executeAst(ast);
738
+ }
739
+ async _executeAst(ast) {
740
+ const statement = buildSql(ast);
741
+ this._registerQueryFingerprint(ast, statement.text);
742
+ const astWarnings = analyzeAst(ast, this._table, this._analyzerConfig);
743
+ this._handleWarnings(astWarnings, ast);
744
+ const startedAt = Date.now();
381
745
  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);
746
+ const rows = await this._executeWithRetry(ast, () => this._pickSqlClient(ast).unsafe(statement.text, statement.params));
747
+ const durationMs = Date.now() - startedAt;
748
+ const executionWarnings = analyzeExecutionWithConfig(ast, {
749
+ durationMs,
750
+ rows: rows.length
751
+ }, this._analyzerConfig);
752
+ this._handleWarnings(executionWarnings, ast);
753
+ if (this._explainAnalyze && ast.type === "select" && durationMs >= this._explainThresholdMs) {
754
+ const explainWarnings = await this._explainWarnings(statement.text, statement.params, ast.table);
755
+ this._handleWarnings(explainWarnings, ast);
756
+ }
757
+ if (this._debug) this._logDebug(ast.type, statement.text, durationMs, rows.length);
758
+ this._recordTelemetry({
759
+ table: ast.table,
760
+ operation: ast.type,
761
+ sql: statement.text,
762
+ durationMs,
763
+ rows: rows.length,
764
+ warned: astWarnings.length + executionWarnings.length > 0,
765
+ warningCount: astWarnings.length + executionWarnings.length
766
+ });
767
+ return rows.map((row) => this._parseResultOrThrow(this._toJs(row)));
768
+ } catch (error) {
769
+ throw wrapPgError(error, this._table._name);
393
770
  }
394
771
  }
395
- async update(options) {
772
+ _pickSqlClient(ast) {
773
+ if (ast.type !== "select") return this._sql;
774
+ if (this._readSql.length === 0) return this._sql;
775
+ const client = this._readSql[this._replicaIndex % this._readSql.length];
776
+ this._replicaIndex += 1;
777
+ return client;
778
+ }
779
+ async _explainWarnings(sqlText, params, table) {
396
780
  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);
781
+ const plan = (await this._sql.unsafe(`EXPLAIN (ANALYZE, FORMAT JSON) ${sqlText}`, params))?.[0]?.["QUERY PLAN"]?.[0]?.Plan;
782
+ if (!plan) return [];
783
+ const warnings = [];
784
+ if (plan["Node Type"] === "Seq Scan") warnings.push({
785
+ code: "index_hint",
786
+ message: `EXPLAIN on "${table}" shows a sequential scan.`,
787
+ suggestion: "Consider adding an index for frequent filter columns."
788
+ });
789
+ return warnings;
790
+ } catch {
791
+ return [];
408
792
  }
409
793
  }
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() }
794
+ async _executeWithRetry(ast, fn) {
795
+ let attempt = 0;
796
+ while (true) try {
797
+ return await fn();
798
+ } catch (error) {
799
+ if (!this._shouldRetry(ast, error, attempt)) throw error;
800
+ attempt += 1;
801
+ await this._sleep(this._retryDelayMs * attempt);
802
+ }
803
+ }
804
+ _shouldRetry(ast, error, attempt) {
805
+ if (attempt >= this._retryAttempts) return false;
806
+ if (ast.type !== "select" && !this._retryWrites) return false;
807
+ const code = error?.code;
808
+ return Boolean(code && this._retryOnCodes.has(code));
809
+ }
810
+ _sleep(ms) {
811
+ return new Promise((resolve) => setTimeout(resolve, ms));
812
+ }
813
+ _registerQueryFingerprint(ast, sqlText) {
814
+ const key = `${ast.type}:${sqlText}`;
815
+ const count = (this._queryFingerprintCount.get(key) ?? 0) + 1;
816
+ this._queryFingerprintCount.set(key, count);
817
+ if (count === this._repeatedQueryThreshold) this._handleWarnings([repeatedQueryWarning(ast.table, count)], ast);
818
+ }
819
+ _handleWarnings(warnings, ast) {
820
+ if (this._warningMode === "silent") return;
821
+ for (const warning of warnings) {
822
+ const message = formatWarning(warning);
823
+ if (this._warningMode === "strict") throw new KadakError({
824
+ code: "QUERY_WARNING",
825
+ message,
826
+ hint: `Fix the warning or switch warnings mode from 'strict'.`,
827
+ table: ast.table,
828
+ originalError: warning
418
829
  });
830
+ this._warn(message);
419
831
  }
420
- return this.hardDelete(options);
421
832
  }
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);
833
+ _applyInsertAutomation(data) {
834
+ const next = { ...data };
835
+ for (const [key, column] of Object.entries(this._table._columns)) {
836
+ const config = column._config;
837
+ if (next[key] !== void 0) continue;
838
+ if (config.autoUpdate) next[key] = /* @__PURE__ */ new Date();
839
+ else if (config.defaultValue !== void 0 && !config.defaultSql) next[key] = config.defaultValue;
432
840
  }
841
+ return next;
433
842
  }
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}`);
843
+ _applyUpdateAutomation(data) {
844
+ const next = { ...data };
845
+ for (const [key, column] of Object.entries(this._table._columns)) if (column._config.autoUpdate) next[key] = /* @__PURE__ */ new Date();
846
+ return next;
847
+ }
848
+ _parseOrThrow(validator, data) {
849
+ try {
850
+ return validator.parse(data);
851
+ } catch (error) {
852
+ throw fromValidationError(error, this._table._name);
443
853
  }
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`);
854
+ }
855
+ _parseResultOrThrow(row) {
856
+ if (!this._validateResults) return row;
857
+ try {
858
+ return this._selectValidator.parse(row);
859
+ } catch (error) {
860
+ throw fromValidationError(error, this._table._name);
446
861
  }
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
862
  }
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;
863
+ _recordTelemetry(event) {
864
+ this._metrics?.record(event);
865
+ this._onQuery?.(event);
866
+ }
867
+ _getSoftDeleteKey() {
868
+ for (const [key, column] of Object.entries(this._table._columns)) if (column._config.softDelete) return key;
869
+ }
870
+ _logDebug(operation, sqlText, durationMs, rows) {
871
+ console.log([
872
+ "[Kadak]",
873
+ `Query: ${this._table._name}.${operation}`,
874
+ `SQL: ${sqlText}`,
875
+ `Time: ${durationMs}ms`,
876
+ `Rows: ${rows}`
877
+ ].join("\n"));
470
878
  }
471
- /** Converts a DB row (snake_case) back to JS format (camelCase) */
472
879
  _toJs(row) {
473
880
  const result = {};
474
881
  for (const [snake, value] of Object.entries(row)) result[this._reverseMap[snake] ?? snake] = value;
@@ -477,40 +884,168 @@ var TableClient = class {
477
884
  };
478
885
  //#endregion
479
886
  //#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
- */
887
+ function buildDatabase(sql, readSql, tables, runtimeConfig, canClose) {
888
+ const db = {};
889
+ for (const [key, table] of Object.entries(tables)) db[key] = new TableClient(sql, table, {
890
+ readSql,
891
+ debug: runtimeConfig.debug,
892
+ warn: runtimeConfig.onWarn,
893
+ onQuery: runtimeConfig.onQuery,
894
+ warningMode: runtimeConfig.warnings,
895
+ retryAttempts: runtimeConfig.retryAttempts,
896
+ retryDelayMs: runtimeConfig.retryDelayMs,
897
+ retryOnCodes: runtimeConfig.retryOnCodes,
898
+ retryWrites: runtimeConfig.retryWrites,
899
+ defaultLimit: runtimeConfig.defaultLimit,
900
+ validateResults: runtimeConfig.validateResults,
901
+ slowQueryMs: runtimeConfig.slowQueryMs,
902
+ largeResultRows: runtimeConfig.largeResultRows,
903
+ largeLimitThreshold: runtimeConfig.largeLimitThreshold,
904
+ repeatedQueryThreshold: runtimeConfig.repeatedQueryThreshold,
905
+ explainAnalyze: runtimeConfig.explainAnalyze,
906
+ explainThresholdMs: runtimeConfig.explainThresholdMs,
907
+ metrics: runtimeConfig.metrics
908
+ });
909
+ db.close = async () => {
910
+ if (!canClose) return;
911
+ await Promise.all([sql.end(), ...readSql.map((replica) => replica.end())]);
912
+ };
913
+ db.sql = sql;
914
+ db.readSql = readSql;
915
+ db.transaction = async (fn) => {
916
+ return sql.begin(async (txSql) => {
917
+ return fn(buildDatabase(txSql, [], tables, runtimeConfig, false));
918
+ });
919
+ };
920
+ return db;
921
+ }
489
922
  function connect(config, tables) {
490
923
  const isString = typeof config === "string";
491
924
  const url = isString ? config : config.url;
492
925
  const opts = isString ? {} : config;
493
926
  const isLocal = url.includes("localhost") || url.includes("127.0.0.1");
494
927
  const ssl = opts.ssl ?? (isLocal ? false : "require");
495
- const sql = postgres(url, {
928
+ return buildDatabase(postgres(url, {
496
929
  max: opts.max ?? 10,
497
930
  idle_timeout: 20,
498
- connect_timeout: 30,
931
+ connect_timeout: opts.timeout ?? 30,
499
932
  ssl,
500
933
  max_lifetime: 1800,
501
934
  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;
935
+ }), (opts.readReplicas ?? []).map((replicaUrl) => postgres(replicaUrl, {
936
+ max: opts.max ?? 10,
937
+ idle_timeout: 20,
938
+ connect_timeout: opts.timeout ?? 30,
939
+ ssl,
940
+ max_lifetime: 1800,
941
+ onnotice: () => {}
942
+ })), tables, opts, true);
943
+ }
944
+ //#endregion
945
+ //#region src/migrate.ts
946
+ async function ensureMigrationTable(sql, tableName) {
947
+ await sql.unsafe(`
948
+ CREATE TABLE IF NOT EXISTS "${tableName}" (
949
+ id BIGSERIAL PRIMARY KEY,
950
+ name TEXT NOT NULL UNIQUE,
951
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
952
+ )
953
+ `);
954
+ }
955
+ async function runMigrations(config) {
956
+ const dir = config.dir ?? path.resolve(process.cwd(), "migrations");
957
+ const tableName = config.tableName ?? "kadak_migrations";
958
+ const sql = postgres(config.url);
959
+ try {
960
+ await ensureMigrationTable(sql, tableName);
961
+ const files = (await fs.readdir(dir).catch(() => [])).filter((entry) => entry.endsWith(".sql")).sort();
962
+ const rows = await sql.unsafe(`SELECT name FROM "${tableName}"`);
963
+ const appliedSet = new Set(rows.map((row) => row.name));
964
+ const applied = [];
965
+ const skipped = [];
966
+ for (const file of files) {
967
+ if (appliedSet.has(file)) {
968
+ skipped.push(file);
969
+ continue;
970
+ }
971
+ const sqlText = await fs.readFile(path.join(dir, file), "utf8");
972
+ await sql.begin(async (tx) => {
973
+ await tx.unsafe(sqlText);
974
+ await tx.unsafe(`INSERT INTO "${tableName}" (name) VALUES ($1)`, [file]);
975
+ });
976
+ applied.push(file);
977
+ }
978
+ return {
979
+ applied,
980
+ skipped
981
+ };
982
+ } finally {
983
+ await sql.end();
508
984
  }
509
- db.close = () => sql.end();
510
- db.sql = sql;
511
- return db;
512
985
  }
986
+ async function createMigration(name, dir = path.resolve(process.cwd(), "migrations")) {
987
+ const safeName = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
988
+ const fileName = `${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:TZ.]/g, "").slice(0, 14)}_${safeName || "migration"}.sql`;
989
+ const filePath = path.join(dir, fileName);
990
+ await fs.mkdir(dir, { recursive: true });
991
+ await fs.writeFile(filePath, "-- Write your SQL migration here\n");
992
+ return filePath;
993
+ }
994
+ //#endregion
995
+ //#region src/observability.ts
996
+ var KadakMetrics = class {
997
+ constructor() {
998
+ this.totalQueries = 0;
999
+ this.totalDurationMs = 0;
1000
+ this.warningQueries = 0;
1001
+ this.byOperation = {};
1002
+ }
1003
+ record(event) {
1004
+ this.totalQueries += 1;
1005
+ this.totalDurationMs += event.durationMs;
1006
+ if (event.warned) this.warningQueries += 1;
1007
+ const bucket = this.byOperation[event.operation] ?? {
1008
+ count: 0,
1009
+ totalDurationMs: 0,
1010
+ rows: 0
1011
+ };
1012
+ bucket.count += 1;
1013
+ bucket.totalDurationMs += event.durationMs;
1014
+ bucket.rows += event.rows;
1015
+ this.byOperation[event.operation] = bucket;
1016
+ }
1017
+ snapshot() {
1018
+ return {
1019
+ totalQueries: this.totalQueries,
1020
+ totalDurationMs: this.totalDurationMs,
1021
+ avgDurationMs: this.totalQueries === 0 ? 0 : this.totalDurationMs / this.totalQueries,
1022
+ warningQueries: this.warningQueries,
1023
+ byOperation: { ...this.byOperation }
1024
+ };
1025
+ }
1026
+ exportPrometheus(prefix = "kadak") {
1027
+ const lines = [];
1028
+ lines.push(`# HELP ${prefix}_queries_total Total queries executed`);
1029
+ lines.push(`# TYPE ${prefix}_queries_total counter`);
1030
+ lines.push(`${prefix}_queries_total ${this.totalQueries}`);
1031
+ lines.push(`# HELP ${prefix}_query_duration_ms_total Total query duration in milliseconds`);
1032
+ lines.push(`# TYPE ${prefix}_query_duration_ms_total counter`);
1033
+ lines.push(`${prefix}_query_duration_ms_total ${this.totalDurationMs}`);
1034
+ lines.push(`# HELP ${prefix}_warning_queries_total Queries that emitted at least one warning`);
1035
+ lines.push(`# TYPE ${prefix}_warning_queries_total counter`);
1036
+ lines.push(`${prefix}_warning_queries_total ${this.warningQueries}`);
1037
+ for (const [operation, value] of Object.entries(this.byOperation)) {
1038
+ lines.push(`${prefix}_operation_queries_total{operation="${operation}"} ${value.count}`);
1039
+ lines.push(`${prefix}_operation_duration_ms_total{operation="${operation}"} ${value.totalDurationMs}`);
1040
+ lines.push(`${prefix}_operation_rows_total{operation="${operation}"} ${value.rows}`);
1041
+ }
1042
+ return lines.join("\n");
1043
+ }
1044
+ };
1045
+ //#endregion
1046
+ //#region src/version.ts
1047
+ const KADAK_API_VERSION = "1.0.0";
513
1048
  //#endregion
514
- export { Column, KadakError, Table, TableClient, camelToSnake, connect, kadak, table, wrapPgError };
1049
+ export { Column, KADAK_API_VERSION, KadakError, KadakMetrics, Table, TableClient, camelToSnake, connect, createMigration, kadak, runMigrations, table, wrapPgError };
515
1050
 
516
1051
  //# sourceMappingURL=index.mjs.map