crudora 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/cli.js +55 -65
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.cjs +1692 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +404 -0
  6. package/dist/index.d.ts +404 -15
  7. package/dist/index.js +1685 -27
  8. package/dist/index.js.map +1 -1
  9. package/dist/scripts/copy-assets.js +0 -21
  10. package/package.json +5 -4
  11. package/scripts/copy-assets.js +0 -21
  12. package/dist/core/crudora.d.ts +0 -49
  13. package/dist/core/crudora.d.ts.map +0 -1
  14. package/dist/core/crudora.js +0 -370
  15. package/dist/core/crudora.js.map +0 -1
  16. package/dist/core/crudoraServer.d.ts +0 -84
  17. package/dist/core/crudoraServer.d.ts.map +0 -1
  18. package/dist/core/crudoraServer.js +0 -202
  19. package/dist/core/crudoraServer.js.map +0 -1
  20. package/dist/core/drizzleTableBuilder.d.ts +0 -6
  21. package/dist/core/drizzleTableBuilder.d.ts.map +0 -1
  22. package/dist/core/drizzleTableBuilder.js +0 -175
  23. package/dist/core/drizzleTableBuilder.js.map +0 -1
  24. package/dist/core/model.d.ts +0 -58
  25. package/dist/core/model.d.ts.map +0 -1
  26. package/dist/core/model.js +0 -64
  27. package/dist/core/model.js.map +0 -1
  28. package/dist/core/repository.d.ts +0 -106
  29. package/dist/core/repository.d.ts.map +0 -1
  30. package/dist/core/repository.js +0 -607
  31. package/dist/core/repository.js.map +0 -1
  32. package/dist/core/schemaGenerator.d.ts +0 -6
  33. package/dist/core/schemaGenerator.d.ts.map +0 -1
  34. package/dist/core/schemaGenerator.js +0 -248
  35. package/dist/core/schemaGenerator.js.map +0 -1
  36. package/dist/decorators/model.d.ts +0 -64
  37. package/dist/decorators/model.d.ts.map +0 -1
  38. package/dist/decorators/model.js +0 -138
  39. package/dist/decorators/model.js.map +0 -1
  40. package/dist/index.d.ts.map +0 -1
  41. package/dist/types/logger.type.d.ts +0 -7
  42. package/dist/types/logger.type.d.ts.map +0 -1
  43. package/dist/types/logger.type.js +0 -3
  44. package/dist/types/logger.type.js.map +0 -1
  45. package/dist/types/model.type.d.ts +0 -38
  46. package/dist/types/model.type.d.ts.map +0 -1
  47. package/dist/types/model.type.js +0 -3
  48. package/dist/types/model.type.js.map +0 -1
  49. package/dist/utils/validation.d.ts +0 -7
  50. package/dist/utils/validation.d.ts.map +0 -1
  51. package/dist/utils/validation.js +0 -107
  52. package/dist/utils/validation.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,28 +1,1686 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getRelationMetadata = exports.getFieldMetadata = exports.BelongsToMany = exports.BelongsTo = exports.HasOne = exports.HasMany = exports.Field = exports.Model = exports.ValidationGenerator = exports.DrizzleTableBuilder = exports.SchemaGenerator = exports.NotFoundError = exports.Repository = exports.CrudoraServer = exports.Crudora = void 0;
4
- require("reflect-metadata");
5
- var crudora_1 = require("./core/crudora");
6
- Object.defineProperty(exports, "Crudora", { enumerable: true, get: function () { return crudora_1.Crudora; } });
7
- var crudoraServer_1 = require("./core/crudoraServer");
8
- Object.defineProperty(exports, "CrudoraServer", { enumerable: true, get: function () { return crudoraServer_1.CrudoraServer; } });
9
- var repository_1 = require("./core/repository");
10
- Object.defineProperty(exports, "Repository", { enumerable: true, get: function () { return repository_1.Repository; } });
11
- Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return repository_1.NotFoundError; } });
12
- var schemaGenerator_1 = require("./core/schemaGenerator");
13
- Object.defineProperty(exports, "SchemaGenerator", { enumerable: true, get: function () { return schemaGenerator_1.SchemaGenerator; } });
14
- var drizzleTableBuilder_1 = require("./core/drizzleTableBuilder");
15
- Object.defineProperty(exports, "DrizzleTableBuilder", { enumerable: true, get: function () { return drizzleTableBuilder_1.DrizzleTableBuilder; } });
16
- var validation_1 = require("./utils/validation");
17
- Object.defineProperty(exports, "ValidationGenerator", { enumerable: true, get: function () { return validation_1.ValidationGenerator; } });
18
- var model_1 = require("./core/model");
19
- Object.defineProperty(exports, "Model", { enumerable: true, get: function () { return model_1.Model; } });
20
- var model_2 = require("./decorators/model");
21
- Object.defineProperty(exports, "Field", { enumerable: true, get: function () { return model_2.Field; } });
22
- Object.defineProperty(exports, "HasMany", { enumerable: true, get: function () { return model_2.HasMany; } });
23
- Object.defineProperty(exports, "HasOne", { enumerable: true, get: function () { return model_2.HasOne; } });
24
- Object.defineProperty(exports, "BelongsTo", { enumerable: true, get: function () { return model_2.BelongsTo; } });
25
- Object.defineProperty(exports, "BelongsToMany", { enumerable: true, get: function () { return model_2.BelongsToMany; } });
26
- Object.defineProperty(exports, "getFieldMetadata", { enumerable: true, get: function () { return model_2.getFieldMetadata; } });
27
- Object.defineProperty(exports, "getRelationMetadata", { enumerable: true, get: function () { return model_2.getRelationMetadata; } });
1
+ // src/index.ts
2
+ import "reflect-metadata";
3
+
4
+ // src/core/crudora.ts
5
+ import { z as z2 } from "zod";
6
+
7
+ // src/core/repository.ts
8
+ import {
9
+ eq,
10
+ and,
11
+ or,
12
+ gt,
13
+ gte,
14
+ lt,
15
+ lte,
16
+ like,
17
+ ne,
18
+ inArray,
19
+ count as drizzleCount,
20
+ getTableColumns,
21
+ isNull,
22
+ asc,
23
+ desc
24
+ } from "drizzle-orm";
25
+ import { randomUUID } from "crypto";
26
+
27
+ // src/decorators/model.ts
28
+ import "reflect-metadata";
29
+ var FIELD_METADATA_KEY = /* @__PURE__ */ Symbol("field");
30
+ function Field(options) {
31
+ return function(target, context) {
32
+ if (typeof context === "string") {
33
+ const propertyKey = context;
34
+ const existingFields = Reflect.getMetadata(FIELD_METADATA_KEY, target.constructor) || {};
35
+ existingFields[propertyKey] = options;
36
+ Reflect.defineMetadata(FIELD_METADATA_KEY, existingFields, target.constructor);
37
+ } else {
38
+ const propertyKey = context.name;
39
+ const constructor = target.constructor;
40
+ const existingFields = Reflect.getMetadata(FIELD_METADATA_KEY, constructor) || {};
41
+ existingFields[propertyKey] = options;
42
+ Reflect.defineMetadata(FIELD_METADATA_KEY, existingFields, constructor);
43
+ }
44
+ };
45
+ }
46
+ function getFieldMetadata(target) {
47
+ return Reflect.getMetadata(FIELD_METADATA_KEY, target) || {};
48
+ }
49
+ var RELATION_METADATA_KEY = /* @__PURE__ */ Symbol("relations");
50
+ function defineRelation(type, model, foreignKey, relatedKey) {
51
+ return function(target, propertyKey) {
52
+ const constructor = target.constructor;
53
+ const existing = Reflect.getMetadata(RELATION_METADATA_KEY, constructor) || {};
54
+ existing[propertyKey] = { type, model, foreignKey, relatedKey };
55
+ Reflect.defineMetadata(RELATION_METADATA_KEY, existing, constructor);
56
+ };
57
+ }
58
+ function HasMany(model, foreignKey, localKey) {
59
+ return defineRelation("hasMany", model, foreignKey, localKey);
60
+ }
61
+ function HasOne(model, foreignKey, localKey) {
62
+ return defineRelation("hasOne", model, foreignKey, localKey);
63
+ }
64
+ function BelongsTo(model, foreignKey, ownerKey) {
65
+ return defineRelation("belongsTo", model, foreignKey, ownerKey);
66
+ }
67
+ function BelongsToMany(model, pivotModel, pivotForeignKey, pivotRelatedKey, localKey) {
68
+ return function(target, propertyKey) {
69
+ const constructor = target.constructor;
70
+ const existing = Reflect.getMetadata(RELATION_METADATA_KEY, constructor) || {};
71
+ existing[propertyKey] = {
72
+ type: "belongsToMany",
73
+ model,
74
+ foreignKey: pivotForeignKey,
75
+ relatedKey: localKey,
76
+ pivotModel,
77
+ pivotRelatedKey
78
+ };
79
+ Reflect.defineMetadata(RELATION_METADATA_KEY, existing, constructor);
80
+ };
81
+ }
82
+ function getRelationMetadata(target) {
83
+ return Reflect.getMetadata(RELATION_METADATA_KEY, target) || {};
84
+ }
85
+
86
+ // src/core/repository.ts
87
+ var SYSTEM_FIELDS = ["createdAt", "updatedAt", "deletedAt"];
88
+ var MAX_IN_VALUES = 500;
89
+ var MAX_LIKE_LENGTH = 200;
90
+ var MAX_RELATIONS = 5;
91
+ var INSERT_BATCH_SIZE = 500;
92
+ var SAFE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*(_gt|_gte|_lt|_lte|_ne|_like|_in)?$/;
93
+ var OPERATOR_MAP = {
94
+ _gt: (col, val) => gt(col, val),
95
+ _gte: (col, val) => gte(col, val),
96
+ _lt: (col, val) => lt(col, val),
97
+ _lte: (col, val) => lte(col, val),
98
+ _ne: (col, val) => ne(col, val),
99
+ _like: (col, val) => like(col, `%${String(val).slice(0, MAX_LIKE_LENGTH)}%`),
100
+ // _in splits on literal commas. Values that contain commas must be URL-encoded (%2C)
101
+ // before reaching this layer. This is safe for UUID/integer FK values which never contain commas.
102
+ _in: (col, val) => inArray(col, String(val).split(",").filter((v) => v !== "").slice(0, MAX_IN_VALUES))
103
+ };
104
+ function encodeCursor(cursorField, cursorValue, pk, pkValue) {
105
+ return Buffer.from(JSON.stringify({ [cursorField]: cursorValue, [pk]: pkValue })).toString("base64");
106
+ }
107
+ function decodeCursor(cursor) {
108
+ try {
109
+ return JSON.parse(Buffer.from(cursor, "base64").toString("utf-8"));
110
+ } catch {
111
+ return {};
112
+ }
113
+ }
114
+ var NotFoundError = class extends Error {
115
+ constructor(message) {
116
+ super(message);
117
+ this.code = "NOT_FOUND";
118
+ this.name = "NotFoundError";
119
+ }
120
+ };
121
+ var Repository = class _Repository {
122
+ constructor(modelClass, db, table, registry) {
123
+ this.modelClass = modelClass;
124
+ this.db = db;
125
+ this.table = table;
126
+ this.registry = registry;
127
+ }
128
+ // ─── Internal helpers ───────────────────────────────────────────────────────
129
+ /**
130
+ * Builds the Drizzle select-column object, merging hidden-field exclusion with
131
+ * an optional explicit field list. Returns `undefined` to select all columns.
132
+ */
133
+ buildSelectCols(selectFields, includeHidden = false) {
134
+ const hidden = [
135
+ ...includeHidden ? [] : this.modelClass.hidden ?? [],
136
+ // deletedAt is an internal implementation detail — never expose it by default
137
+ ...this.modelClass.softDelete ? ["deletedAt"] : []
138
+ ];
139
+ if (!hidden.length && !selectFields?.length) return void 0;
140
+ const all = getTableColumns(this.table);
141
+ let entries = Object.entries(all).filter(([col]) => !hidden.includes(col));
142
+ if (selectFields?.length) {
143
+ entries = entries.filter(([col]) => selectFields.includes(col));
144
+ }
145
+ return Object.fromEntries(entries);
146
+ }
147
+ buildWhere(where) {
148
+ if (!where) return void 0;
149
+ const cols = this.table;
150
+ const conds = [];
151
+ for (const [key, val] of Object.entries(where)) {
152
+ if (!Object.prototype.hasOwnProperty.call(where, key)) continue;
153
+ if (!SAFE_KEY_RE.test(key)) continue;
154
+ if (val === void 0 || val === "") continue;
155
+ const suffix = Object.keys(OPERATOR_MAP).find((op) => key.endsWith(op));
156
+ if (suffix) {
157
+ const colName = key.slice(0, -suffix.length);
158
+ if (Object.prototype.hasOwnProperty.call(cols, colName)) {
159
+ conds.push(OPERATOR_MAP[suffix](cols[colName], val));
160
+ }
161
+ } else {
162
+ if (Object.prototype.hasOwnProperty.call(cols, key)) {
163
+ conds.push(eq(cols[key], val));
164
+ }
165
+ }
166
+ }
167
+ if (conds.length === 0) return void 0;
168
+ return conds.length === 1 ? conds[0] : and(...conds);
169
+ }
170
+ softDeleteClause() {
171
+ if (!this.modelClass.softDelete) return void 0;
172
+ const cols = this.table;
173
+ if (!cols.deletedAt) {
174
+ throw new Error(
175
+ `Model "${this.modelClass.name}" has softDelete enabled but the table is missing a "deletedAt" column.`
176
+ );
177
+ }
178
+ return isNull(cols.deletedAt);
179
+ }
180
+ combineWhere(...clauses) {
181
+ const active = clauses.filter(Boolean);
182
+ if (active.length === 0) return void 0;
183
+ if (active.length === 1) return active[0];
184
+ return and(...active);
185
+ }
186
+ // ─── Relation loading ───────────────────────────────────────────────────────
187
+ /**
188
+ * Batch-loads relations for a list of records. Uses the _in operator so the
189
+ * total number of DB round-trips equals the number of requested relations.
190
+ */
191
+ async loadRelations(items, withRelations) {
192
+ if (!this.registry || !items.length || !withRelations.length) return items;
193
+ const relDefs = getRelationMetadata(this.modelClass);
194
+ let result = [...items];
195
+ for (const relName of withRelations.slice(0, MAX_RELATIONS)) {
196
+ const relDef = relDefs[relName];
197
+ if (!relDef) continue;
198
+ const RelModel = relDef.model();
199
+ const relRepo = this.registry.get(RelModel.name);
200
+ if (!relRepo) continue;
201
+ const pk = this.modelClass.getPrimaryKey();
202
+ if (relDef.type === "hasMany" || relDef.type === "hasOne") {
203
+ const localKey = relDef.relatedKey ?? pk;
204
+ const localVals = [...new Set(result.map((r) => r[localKey]).filter(Boolean))];
205
+ if (!localVals.length) {
206
+ result = result.map((r) => ({ ...r, [relName]: relDef.type === "hasOne" ? null : [] }));
207
+ continue;
208
+ }
209
+ const related = await relRepo.findAll({
210
+ where: { [`${relDef.foreignKey}_in`]: localVals.join(",") }
211
+ });
212
+ result = result.map((r) => {
213
+ const matches = related.filter((rel) => rel[relDef.foreignKey] === r[localKey]);
214
+ return { ...r, [relName]: relDef.type === "hasOne" ? matches[0] ?? null : matches };
215
+ });
216
+ } else if (relDef.type === "belongsTo") {
217
+ const ownerKey = relDef.relatedKey ?? RelModel.getPrimaryKey();
218
+ const fkVals = [...new Set(result.map((r) => r[relDef.foreignKey]).filter(Boolean))];
219
+ if (!fkVals.length) {
220
+ result = result.map((r) => ({ ...r, [relName]: null }));
221
+ continue;
222
+ }
223
+ const related = await relRepo.findAll({
224
+ where: { [`${ownerKey}_in`]: fkVals.join(",") }
225
+ });
226
+ result = result.map((r) => {
227
+ const match = related.find((rel) => rel[ownerKey] === r[relDef.foreignKey]);
228
+ return { ...r, [relName]: match ?? null };
229
+ });
230
+ } else if (relDef.type === "belongsToMany") {
231
+ const localKey = relDef.relatedKey ?? pk;
232
+ const localVals = [...new Set(result.map((r) => r[localKey]).filter(Boolean))];
233
+ if (!localVals.length) {
234
+ result = result.map((r) => ({ ...r, [relName]: [] }));
235
+ continue;
236
+ }
237
+ const PivotModel = relDef.pivotModel();
238
+ const pivotRepo = this.registry.get(PivotModel.name);
239
+ if (!pivotRepo) continue;
240
+ const pivotRows = await pivotRepo.findAll({
241
+ where: { [`${relDef.foreignKey}_in`]: localVals.join(",") }
242
+ });
243
+ const relatedIds = [
244
+ ...new Set(pivotRows.map((p) => p[relDef.pivotRelatedKey]).filter(Boolean))
245
+ ];
246
+ if (!relatedIds.length) {
247
+ result = result.map((r) => ({ ...r, [relName]: [] }));
248
+ continue;
249
+ }
250
+ const relatedPk = RelModel.getPrimaryKey();
251
+ const relatedRows = await relRepo.findAll({
252
+ where: { [`${relatedPk}_in`]: relatedIds.join(",") }
253
+ });
254
+ result = result.map((r) => {
255
+ const myPivots = pivotRows.filter((p) => p[relDef.foreignKey] === r[localKey]);
256
+ const myIds = new Set(myPivots.map((p) => p[relDef.pivotRelatedKey]));
257
+ return { ...r, [relName]: relatedRows.filter((rel) => myIds.has(rel[relatedPk])) };
258
+ });
259
+ }
260
+ }
261
+ return result;
262
+ }
263
+ // ─── Public API ─────────────────────────────────────────────────────────────
264
+ async create(data) {
265
+ let processed = { ...data };
266
+ for (const f of SYSTEM_FIELDS) delete processed[f];
267
+ if (this.modelClass.beforeCreate) {
268
+ processed = await this.modelClass.beforeCreate(processed);
269
+ }
270
+ const pkField = this.modelClass.getPrimaryKey();
271
+ if (!processed[pkField]) {
272
+ processed[pkField] = randomUUID();
273
+ }
274
+ for (const f of SYSTEM_FIELDS) delete processed[f];
275
+ await this.db.insert(this.table).values(processed);
276
+ const result = await this.findById(processed[pkField]);
277
+ if (!result) throw new Error(`Create failed: could not retrieve record after insert (id: ${processed[pkField]})`);
278
+ let final = result;
279
+ if (this.modelClass.afterCreate) {
280
+ final = await this.modelClass.afterCreate(processed, result);
281
+ }
282
+ return final;
283
+ }
284
+ async findById(id, opts) {
285
+ const pk = this.modelClass.getPrimaryKey();
286
+ const selectCols = this.buildSelectCols(opts?.select, opts?.includeHidden);
287
+ let hookOptions = { where: { [pk]: id } };
288
+ if (this.modelClass.beforeFind) {
289
+ hookOptions = await this.modelClass.beforeFind(hookOptions);
290
+ }
291
+ const extraWhere = this.buildWhere(hookOptions?.where ? { ...hookOptions.where, [pk]: void 0 } : void 0);
292
+ let query = selectCols ? this.db.select(selectCols).from(this.table) : this.db.select().from(this.table);
293
+ const whereClause = this.combineWhere(
294
+ eq(this.table[pk], id),
295
+ opts?.withDeleted ? void 0 : this.softDeleteClause(),
296
+ extraWhere
297
+ );
298
+ query = query.where(whereClause).limit(1);
299
+ const [result] = await query;
300
+ let found = result ?? null;
301
+ if (found && this.modelClass.afterFind) {
302
+ [found] = await this.modelClass.afterFind([found]);
303
+ }
304
+ if (found && opts?.with?.length) {
305
+ const [withResult] = await this.loadRelations([found], opts.with);
306
+ return withResult ?? null;
307
+ }
308
+ return found;
309
+ }
310
+ async findAll(options) {
311
+ let queryOptions = options ? { ...options } : void 0;
312
+ if (this.modelClass.beforeFind) {
313
+ queryOptions = await this.modelClass.beforeFind(queryOptions);
314
+ }
315
+ const selectCols = this.buildSelectCols(queryOptions?.select, queryOptions?.includeHidden);
316
+ const whereClause = this.combineWhere(
317
+ this.buildWhere(queryOptions?.where),
318
+ queryOptions?.withDeleted ? void 0 : this.softDeleteClause()
319
+ );
320
+ let query = selectCols ? this.db.select(selectCols).from(this.table) : this.db.select().from(this.table);
321
+ if (whereClause) query = query.where(whereClause);
322
+ if (queryOptions?.orderBy) {
323
+ const fields = Array.isArray(queryOptions.orderBy) ? queryOptions.orderBy : [queryOptions.orderBy];
324
+ const orders = Array.isArray(queryOptions.order) ? queryOptions.order : [queryOptions.order ?? "asc"];
325
+ const orderClauses = fields.map((field, i) => {
326
+ if (!SAFE_KEY_RE.test(field)) return null;
327
+ if (!Object.prototype.hasOwnProperty.call(this.table, field)) return null;
328
+ const col = this.table[field];
329
+ return (orders[i] ?? orders[0] ?? "asc") === "desc" ? desc(col) : asc(col);
330
+ }).filter(Boolean);
331
+ if (orderClauses.length) query = query.orderBy(...orderClauses);
332
+ }
333
+ if (queryOptions?.skip !== void 0) query = query.offset(queryOptions.skip);
334
+ if (queryOptions?.take !== void 0) query = query.limit(queryOptions.take);
335
+ let results = await query;
336
+ if (this.modelClass.afterFind) {
337
+ results = await this.modelClass.afterFind(results);
338
+ }
339
+ if (queryOptions?.with?.length) {
340
+ results = await this.loadRelations(results, queryOptions.with);
341
+ }
342
+ return results;
343
+ }
344
+ /**
345
+ * Cursor-based pagination — more efficient than offset for large datasets.
346
+ *
347
+ * @example
348
+ * const page1 = await userRepo.findWithCursor({ take: 10 });
349
+ * const page2 = await userRepo.findWithCursor({ take: 10, cursor: page1.nextCursor });
350
+ */
351
+ async findWithCursor(options) {
352
+ const {
353
+ take = 10,
354
+ cursor,
355
+ order = "asc",
356
+ where,
357
+ select,
358
+ with: withRelations,
359
+ withDeleted,
360
+ includeHidden
361
+ } = options;
362
+ const pk = this.modelClass.getPrimaryKey();
363
+ const requestedCursorField = options.cursorField ?? pk;
364
+ const cursorField = SAFE_KEY_RE.test(requestedCursorField) && Object.prototype.hasOwnProperty.call(this.table, requestedCursorField) ? requestedCursorField : pk;
365
+ const col = this.table[cursorField];
366
+ const pkCol = this.table[pk];
367
+ const cursorFieldIsPk = cursorField === pk;
368
+ let cursorValue = null;
369
+ let cursorPkValue = null;
370
+ if (cursor) {
371
+ const decoded = decodeCursor(cursor);
372
+ cursorValue = decoded[cursorField] ?? null;
373
+ cursorPkValue = decoded[pk] ?? null;
374
+ }
375
+ let cursorCond = void 0;
376
+ if (cursorValue !== null && col) {
377
+ if (cursorFieldIsPk || cursorPkValue === null) {
378
+ cursorCond = order === "desc" ? lt(col, cursorValue) : gt(col, cursorValue);
379
+ } else {
380
+ cursorCond = or(
381
+ order === "desc" ? lt(col, cursorValue) : gt(col, cursorValue),
382
+ and(
383
+ eq(col, cursorValue),
384
+ order === "desc" ? lt(pkCol, cursorPkValue) : gt(pkCol, cursorPkValue)
385
+ )
386
+ );
387
+ }
388
+ }
389
+ const whereClause = this.combineWhere(
390
+ this.buildWhere(where),
391
+ withDeleted ? void 0 : this.softDeleteClause(),
392
+ cursorCond
393
+ );
394
+ const selectCols = this.buildSelectCols(select, includeHidden);
395
+ let query = selectCols ? this.db.select(selectCols).from(this.table) : this.db.select().from(this.table);
396
+ if (whereClause) query = query.where(whereClause);
397
+ if (col) {
398
+ query = !cursorFieldIsPk && pkCol ? query.orderBy(order === "desc" ? desc(col) : asc(col), order === "desc" ? desc(pkCol) : asc(pkCol)) : query.orderBy(order === "desc" ? desc(col) : asc(col));
399
+ }
400
+ query = query.limit(take + 1);
401
+ let results = await query;
402
+ const hasMore = results.length > take;
403
+ const data = hasMore ? results.slice(0, take) : results;
404
+ const nextCursor = hasMore && data.length > 0 ? encodeCursor(cursorField, data[data.length - 1][cursorField], pk, data[data.length - 1][pk]) : null;
405
+ if (this.modelClass.afterFind) {
406
+ const processed = await this.modelClass.afterFind(data);
407
+ const finalData2 = withRelations?.length ? await this.loadRelations(processed, withRelations) : processed;
408
+ return { data: finalData2, nextCursor, hasMore };
409
+ }
410
+ const finalData = withRelations?.length ? await this.loadRelations(data, withRelations) : data;
411
+ return { data: finalData, nextCursor, hasMore };
412
+ }
413
+ async update(id, data) {
414
+ let processed = { ...data };
415
+ const pk = this.modelClass.getPrimaryKey();
416
+ delete processed[pk];
417
+ delete processed["createdAt"];
418
+ delete processed["deletedAt"];
419
+ if (this.modelClass.beforeUpdate) {
420
+ processed = await this.modelClass.beforeUpdate(id, processed);
421
+ }
422
+ delete processed[pk];
423
+ delete processed["createdAt"];
424
+ delete processed["deletedAt"];
425
+ if (Object.keys(processed).length === 0) {
426
+ const existing = await this.findById(id);
427
+ if (!existing) throw new NotFoundError(`Record with id "${id}" not found`);
428
+ return existing;
429
+ }
430
+ if (this.modelClass.timestamps !== false) {
431
+ const cols = getTableColumns(this.table);
432
+ if ("updatedAt" in cols) {
433
+ processed.updatedAt = /* @__PURE__ */ new Date();
434
+ }
435
+ }
436
+ if (this.modelClass.softDelete) {
437
+ const tbl = this.table;
438
+ const [row] = await this.db.select({ _deleted: tbl.deletedAt }).from(this.table).where(eq(tbl[pk], id)).limit(1);
439
+ if (!row) throw new NotFoundError(`Record with id "${id}" not found`);
440
+ if (row._deleted !== null && row._deleted !== void 0) {
441
+ throw new NotFoundError(`Record with id "${id}" is soft-deleted and cannot be updated`);
442
+ }
443
+ }
444
+ const updateWhere = this.combineWhere(
445
+ eq(this.table[pk], id),
446
+ this.softDeleteClause()
447
+ );
448
+ await this.db.update(this.table).set(processed).where(updateWhere);
449
+ const result = await this.findById(id);
450
+ if (!result) throw new NotFoundError(`Record with id "${id}" not found`);
451
+ let final = result;
452
+ if (this.modelClass.afterUpdate) {
453
+ final = await this.modelClass.afterUpdate(id, processed, result);
454
+ }
455
+ return final;
456
+ }
457
+ async delete(id) {
458
+ const existing = await this.findById(id);
459
+ if (!existing) throw new NotFoundError(`Record with id "${id}" not found`);
460
+ if (this.modelClass.beforeDelete) {
461
+ await this.modelClass.beforeDelete(id);
462
+ }
463
+ const pk = this.modelClass.getPrimaryKey();
464
+ if (this.modelClass.softDelete) {
465
+ await this.db.update(this.table).set({ deletedAt: /* @__PURE__ */ new Date() }).where(eq(this.table[pk], id));
466
+ } else {
467
+ await this.db.delete(this.table).where(eq(this.table[pk], id));
468
+ }
469
+ let final = existing;
470
+ if (this.modelClass.afterDelete) {
471
+ final = await this.modelClass.afterDelete(id, existing);
472
+ }
473
+ return final;
474
+ }
475
+ async hardDelete(id) {
476
+ const existing = await this.findById(id, { withDeleted: true });
477
+ if (!existing) throw new NotFoundError(`Record with id "${id}" not found`);
478
+ const pk = this.modelClass.getPrimaryKey();
479
+ await this.db.delete(this.table).where(eq(this.table[pk], id));
480
+ return existing;
481
+ }
482
+ async restore(id) {
483
+ const pk = this.modelClass.getPrimaryKey();
484
+ const cols = this.table;
485
+ if (!cols.deletedAt) throw new Error("softDelete is not enabled on this model");
486
+ const [raw] = await this.db.select().from(this.table).where(eq(cols[pk], id)).limit(1);
487
+ if (!raw) throw new NotFoundError(`Record with id "${id}" not found`);
488
+ if (!raw.deletedAt) throw new Error(`Record with id "${id}" is not soft-deleted`);
489
+ await this.db.update(this.table).set({ deletedAt: null }).where(eq(cols[pk], id));
490
+ const selectCols = this.buildSelectCols();
491
+ let query = selectCols ? this.db.select(selectCols).from(this.table) : this.db.select().from(this.table);
492
+ query = query.where(eq(cols[pk], id)).limit(1);
493
+ const [result] = await query;
494
+ return result ?? null;
495
+ }
496
+ async count(where, withDeleted) {
497
+ const whereClause = this.combineWhere(
498
+ this.buildWhere(where),
499
+ withDeleted ? void 0 : this.softDeleteClause()
500
+ );
501
+ let query = this.db.select({ count: drizzleCount() }).from(this.table);
502
+ if (whereClause) query = query.where(whereClause);
503
+ const [result] = await query;
504
+ return Number(result.count);
505
+ }
506
+ /** Returns the first record matching `where`, or null. Builds a direct LIMIT 1 query — does not delegate to findAll. */
507
+ async findOne(where, opts) {
508
+ const selectCols = this.buildSelectCols(opts?.select, opts?.includeHidden);
509
+ const whereClause = this.combineWhere(
510
+ this.buildWhere(where),
511
+ opts?.withDeleted ? void 0 : this.softDeleteClause()
512
+ );
513
+ let query = selectCols ? this.db.select(selectCols).from(this.table) : this.db.select().from(this.table);
514
+ if (whereClause) query = query.where(whereClause);
515
+ query = query.limit(1);
516
+ const [result] = await query;
517
+ let found = result ?? null;
518
+ if (found && this.modelClass.afterFind) {
519
+ [found] = await this.modelClass.afterFind([found]);
520
+ }
521
+ if (found && opts?.with?.length) {
522
+ const [withResult] = await this.loadRelations([found], opts.with);
523
+ return withResult ?? null;
524
+ }
525
+ return found;
526
+ }
527
+ /**
528
+ * Returns true if at least one record matches `where`.
529
+ * Uses SELECT pk LIMIT 1 — more efficient than COUNT(*) because the DB stops at the first match.
530
+ */
531
+ async exists(where, withDeleted) {
532
+ const pk = this.modelClass.getPrimaryKey();
533
+ const whereClause = this.combineWhere(
534
+ this.buildWhere(where),
535
+ withDeleted ? void 0 : this.softDeleteClause()
536
+ );
537
+ let query = this.db.select({ _pk: this.table[pk] }).from(this.table);
538
+ if (whereClause) query = query.where(whereClause);
539
+ query = query.limit(1);
540
+ const [result] = await query;
541
+ return result !== void 0;
542
+ }
543
+ /**
544
+ * Inserts multiple records in a single INSERT statement.
545
+ * Calls `beforeCreate` for each row. Calls `afterCreateMany` once with all
546
+ * results — use this hook for batch-aware side effects (e.g. bulk notifications).
547
+ * Individual `afterCreate` is intentionally NOT called per row for performance.
548
+ */
549
+ async createMany(data) {
550
+ if (data.length === 0) return [];
551
+ const pk = this.modelClass.getPrimaryKey();
552
+ const processed = await Promise.all(
553
+ data.map(async (item) => {
554
+ let row = { ...item };
555
+ for (const f of SYSTEM_FIELDS) delete row[f];
556
+ if (this.modelClass.beforeCreate) {
557
+ row = await this.modelClass.beforeCreate(row);
558
+ }
559
+ for (const f of SYSTEM_FIELDS) delete row[f];
560
+ if (!row[pk]) row[pk] = randomUUID();
561
+ return row;
562
+ })
563
+ );
564
+ for (let i = 0; i < processed.length; i += INSERT_BATCH_SIZE) {
565
+ await this.db.insert(this.table).values(processed.slice(i, i + INSERT_BATCH_SIZE));
566
+ }
567
+ const pks = processed.map((r) => r[pk]);
568
+ const results = [];
569
+ for (let i = 0; i < pks.length; i += MAX_IN_VALUES) {
570
+ const batch = pks.slice(i, i + MAX_IN_VALUES);
571
+ const rows = await this.findAll({ where: { [`${pk}_in`]: batch.join(",") } });
572
+ results.push(...rows);
573
+ }
574
+ const pkOrder = new Map(pks.map((id, i) => [id, i]));
575
+ results.sort((a, b) => (pkOrder.get(a[pk]) ?? 0) - (pkOrder.get(b[pk]) ?? 0));
576
+ if (this.modelClass.afterCreateMany) {
577
+ return this.modelClass.afterCreateMany(results);
578
+ }
579
+ return results;
580
+ }
581
+ /**
582
+ * Runs `fn` inside a database transaction. The callback receives a new
583
+ * Repository bound to the transaction connection.
584
+ *
585
+ * @example
586
+ * await userRepo.transaction(async (trx) => {
587
+ * await trx.create({ name: 'Alice' });
588
+ * await trx.update(id, { name: 'Bob' });
589
+ * });
590
+ */
591
+ async transaction(fn) {
592
+ return this.db.transaction(async (trx) => {
593
+ let trxRegistry;
594
+ if (this.registry) {
595
+ trxRegistry = /* @__PURE__ */ new Map();
596
+ for (const [name, repo] of this.registry.entries()) {
597
+ trxRegistry.set(name, new _Repository(repo.modelClass, trx, repo.table, trxRegistry));
598
+ }
599
+ }
600
+ return fn(new _Repository(this.modelClass, trx, this.table, trxRegistry));
601
+ });
602
+ }
603
+ };
604
+
605
+ // src/core/schemaGenerator.ts
606
+ function safe(s) {
607
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
608
+ }
609
+ function pgColumnDef(name, opts) {
610
+ const type = opts.type;
611
+ let col;
612
+ const n = safe(name);
613
+ switch (type) {
614
+ case "uuid":
615
+ col = `uuid('${n}')`;
616
+ break;
617
+ case "text":
618
+ col = `text('${n}')`;
619
+ break;
620
+ case "integer":
621
+ col = `integer('${n}')`;
622
+ break;
623
+ case "number":
624
+ col = `doublePrecision('${n}')`;
625
+ break;
626
+ case "boolean":
627
+ col = `boolean('${n}')`;
628
+ break;
629
+ case "date":
630
+ col = `timestamp('${n}', { mode: 'date' })`;
631
+ break;
632
+ case "decimal":
633
+ col = `decimal('${n}', { precision: ${opts.precision ?? 10}, scale: ${opts.scale ?? 2} })`;
634
+ break;
635
+ case "json":
636
+ col = `json('${n}')`;
637
+ break;
638
+ case "enum":
639
+ col = `text('${n}')`;
640
+ break;
641
+ case "bigint":
642
+ col = `bigint('${n}', { mode: 'number' })`;
643
+ break;
644
+ case "serial":
645
+ col = `serial('${n}')`;
646
+ break;
647
+ case "array":
648
+ col = `text('${n}').array()`;
649
+ break;
650
+ case "string":
651
+ default:
652
+ col = `varchar('${n}', { length: ${opts.length ?? 255} })`;
653
+ }
654
+ if (opts.primary) col += ".primaryKey()";
655
+ if (opts.required && !opts.nullable && opts.type !== "serial") col += ".notNull()";
656
+ if (opts.unique) col += ".unique()";
657
+ if (opts.default !== void 0 && opts.type !== "serial") col += `.default(${JSON.stringify(opts.default)})`;
658
+ return col;
659
+ }
660
+ function mysqlColumnDef(name, opts) {
661
+ const type = opts.type;
662
+ let col;
663
+ const n = safe(name);
664
+ switch (type) {
665
+ case "uuid":
666
+ col = `varchar('${n}', { length: 36 })`;
667
+ break;
668
+ case "text":
669
+ col = `text('${n}')`;
670
+ break;
671
+ case "integer":
672
+ col = `int('${n}')`;
673
+ break;
674
+ case "number":
675
+ col = `double('${n}')`;
676
+ break;
677
+ case "boolean":
678
+ col = `boolean('${n}')`;
679
+ break;
680
+ case "date":
681
+ col = `datetime('${n}', { mode: 'date' })`;
682
+ break;
683
+ case "decimal":
684
+ col = `decimal('${n}', { precision: ${opts.precision ?? 10}, scale: ${opts.scale ?? 2} })`;
685
+ break;
686
+ case "json":
687
+ col = `json('${n}')`;
688
+ break;
689
+ case "enum":
690
+ col = `mysqlEnum('${n}', [${(opts.enumValues ?? []).map((v) => `'${safe(v)}'`).join(", ")}])`;
691
+ break;
692
+ case "bigint":
693
+ col = `bigint('${n}', { mode: 'number' })`;
694
+ break;
695
+ case "serial":
696
+ col = `int('${n}').autoincrement()`;
697
+ break;
698
+ case "array":
699
+ col = `json('${n}')`;
700
+ break;
701
+ case "string":
702
+ default:
703
+ col = `varchar('${n}', { length: ${opts.length ?? 255} })`;
704
+ }
705
+ if (opts.primary) col += ".primaryKey()";
706
+ if (opts.required && !opts.nullable && opts.type !== "serial") col += ".notNull()";
707
+ if (opts.unique) col += ".unique()";
708
+ if (opts.default !== void 0 && opts.type !== "serial") col += `.default(${JSON.stringify(opts.default)})`;
709
+ return col;
710
+ }
711
+ function generateModelBlock(modelClass, dialect, schemaVars) {
712
+ const fields = getFieldMetadata(modelClass);
713
+ const tableName = modelClass.getTableName();
714
+ const schemaName = modelClass.schema;
715
+ const exportName = modelClass.name.charAt(0).toLowerCase() + modelClass.name.slice(1) + "Table";
716
+ const columnDef = dialect === "postgresql" ? pgColumnDef : mysqlColumnDef;
717
+ const cols = [];
718
+ for (const [fieldName, opts] of Object.entries(fields)) {
719
+ cols.push(` ${fieldName}: ${columnDef(fieldName, opts)},`);
720
+ }
721
+ if (modelClass.timestamps !== false) {
722
+ if (dialect === "postgresql") {
723
+ cols.push(` createdAt: timestamp('createdAt', { mode: 'date' }).defaultNow().notNull(),`);
724
+ cols.push(` updatedAt: timestamp('updatedAt', { mode: 'date' }).defaultNow().notNull(),`);
725
+ } else {
726
+ cols.push(` createdAt: datetime('createdAt', { mode: 'date' }).notNull(),`);
727
+ cols.push(` updatedAt: datetime('updatedAt', { mode: 'date' }).notNull(),`);
728
+ }
729
+ }
730
+ if (modelClass.softDelete) {
731
+ if (dialect === "postgresql") {
732
+ cols.push(` deletedAt: timestamp('deletedAt', { mode: 'date' }),`);
733
+ } else {
734
+ cols.push(` deletedAt: datetime('deletedAt', { mode: 'date' }),`);
735
+ }
736
+ }
737
+ let tableCall;
738
+ if (schemaName) {
739
+ const schemaVar = schemaVars.get(schemaName);
740
+ tableCall = `${schemaVar}.table('${safe(tableName)}', {
741
+ ${cols.join("\n")}
742
+ })`;
743
+ } else {
744
+ const tableFactory = dialect === "postgresql" ? "pgTable" : "mysqlTable";
745
+ tableCall = `${tableFactory}('${safe(tableName)}', {
746
+ ${cols.join("\n")}
747
+ })`;
748
+ }
749
+ return `export const ${exportName} = ${tableCall};
750
+ `;
751
+ }
752
+ var SchemaGenerator = class {
753
+ static generateDrizzleSchema(models, dialect) {
754
+ const schemaNames = /* @__PURE__ */ new Set();
755
+ for (const m of models) {
756
+ const s = m.schema;
757
+ if (s) schemaNames.add(s);
758
+ }
759
+ const schemaVars = /* @__PURE__ */ new Map();
760
+ for (const name of schemaNames) {
761
+ schemaVars.set(name, name + "Schema");
762
+ }
763
+ const coreImports = /* @__PURE__ */ new Set();
764
+ const schemaFactory = dialect === "postgresql" ? "pgSchema" : "mysqlSchema";
765
+ const tableFactory = dialect === "postgresql" ? "pgTable" : "mysqlTable";
766
+ const pkg = dialect === "postgresql" ? "drizzle-orm/pg-core" : "drizzle-orm/mysql-core";
767
+ if (schemaNames.size > 0) coreImports.add(schemaFactory);
768
+ const hasDefaultSchema = models.some((m) => !m.schema);
769
+ if (hasDefaultSchema) coreImports.add(tableFactory);
770
+ const colImports = /* @__PURE__ */ new Set();
771
+ for (const modelClass of models) {
772
+ const fields = getFieldMetadata(modelClass);
773
+ for (const opts of Object.values(fields)) {
774
+ colImports.add(getColumnImport(opts.type, dialect));
775
+ }
776
+ if (modelClass.timestamps !== false) {
777
+ colImports.add(dialect === "postgresql" ? "timestamp" : "datetime");
778
+ }
779
+ }
780
+ const allImports = /* @__PURE__ */ new Set([...coreImports, ...colImports]);
781
+ let output = `// Auto-generated by Crudora \u2014 do not edit manually
782
+ `;
783
+ output += `import { ${[...allImports].sort().join(", ")} } from '${pkg}';
784
+
785
+ `;
786
+ for (const [name, varName] of schemaVars) {
787
+ output += `const ${varName} = ${schemaFactory}('${safe(name)}');
788
+ `;
789
+ }
790
+ if (schemaVars.size > 0) output += "\n";
791
+ for (const modelClass of models) {
792
+ output += generateModelBlock(modelClass, dialect, schemaVars) + "\n";
793
+ }
794
+ return output;
795
+ }
796
+ };
797
+ function getColumnImport(type, dialect) {
798
+ if (dialect === "postgresql") {
799
+ const map = {
800
+ uuid: "uuid",
801
+ text: "text",
802
+ string: "varchar",
803
+ integer: "integer",
804
+ number: "doublePrecision",
805
+ boolean: "boolean",
806
+ date: "timestamp",
807
+ decimal: "decimal",
808
+ json: "json",
809
+ enum: "text",
810
+ bigint: "bigint",
811
+ serial: "serial",
812
+ array: "text"
813
+ };
814
+ return map[type] ?? "varchar";
815
+ } else {
816
+ const map = {
817
+ uuid: "varchar",
818
+ text: "text",
819
+ string: "varchar",
820
+ integer: "int",
821
+ number: "double",
822
+ boolean: "boolean",
823
+ date: "datetime",
824
+ decimal: "decimal",
825
+ json: "json",
826
+ enum: "mysqlEnum",
827
+ bigint: "bigint",
828
+ serial: "int",
829
+ array: "json"
830
+ };
831
+ return map[type] ?? "varchar";
832
+ }
833
+ }
834
+
835
+ // src/utils/validation.ts
836
+ import { z } from "zod";
837
+ function zodTypeFor(opts, forStrict) {
838
+ const { type, required = false, nullable = false, length } = opts;
839
+ let base;
840
+ switch (type) {
841
+ case "integer":
842
+ base = z.number().int();
843
+ break;
844
+ case "number":
845
+ base = z.number();
846
+ break;
847
+ case "boolean":
848
+ base = z.boolean();
849
+ break;
850
+ case "date":
851
+ base = z.coerce.date();
852
+ break;
853
+ case "decimal":
854
+ base = z.string().regex(/^-?\d+(\.\d+)?$/, "Must be a valid decimal number");
855
+ break;
856
+ case "json":
857
+ base = z.union([z.record(z.string(), z.any()), z.array(z.any())]);
858
+ break;
859
+ case "uuid":
860
+ base = z.uuid();
861
+ break;
862
+ case "enum":
863
+ base = opts.enumValues?.length ? z.enum(opts.enumValues) : z.string();
864
+ break;
865
+ case "bigint":
866
+ base = z.number().int();
867
+ break;
868
+ case "serial":
869
+ base = z.number().int().positive();
870
+ break;
871
+ case "array":
872
+ base = z.array(z.string());
873
+ break;
874
+ case "string":
875
+ base = length ? z.string().max(length) : z.string();
876
+ break;
877
+ case "text":
878
+ default:
879
+ base = z.string();
880
+ }
881
+ if (nullable) base = base.nullable();
882
+ if (forStrict) {
883
+ return required ? base : base.optional();
884
+ }
885
+ return base.optional();
886
+ }
887
+ var SYSTEM_FIELDS2 = /* @__PURE__ */ new Set(["createdAt", "updatedAt", "deletedAt"]);
888
+ function resolveFields(modelClass) {
889
+ const fieldMeta = getFieldMetadata(modelClass);
890
+ const hasMeta = Object.keys(fieldMeta).length > 0;
891
+ const fillable = modelClass.fillable;
892
+ if (hasMeta) {
893
+ let entries = Object.entries(fieldMeta).filter(
894
+ ([name, opts]) => !opts.primary && !SYSTEM_FIELDS2.has(name)
895
+ );
896
+ if (fillable?.length) {
897
+ entries = entries.filter(([name]) => fillable.includes(name));
898
+ }
899
+ return entries.map(([name, opts]) => ({ name, opts }));
900
+ }
901
+ return (fillable ?? []).map((name) => ({
902
+ name,
903
+ opts: { type: "string", required: false }
904
+ }));
905
+ }
906
+ var ValidationGenerator = class {
907
+ static generateZodSchema(modelClass) {
908
+ const fields = resolveFields(modelClass);
909
+ if (fields.length === 0) return z.object({}).partial();
910
+ const shape = {};
911
+ for (const { name, opts } of fields) {
912
+ shape[name] = zodTypeFor(opts, false);
913
+ }
914
+ return z.object(shape);
915
+ }
916
+ static generateStrictZodSchema(modelClass) {
917
+ const fields = resolveFields(modelClass);
918
+ if (fields.length === 0) return z.object({});
919
+ const shape = {};
920
+ for (const { name, opts } of fields) {
921
+ shape[name] = zodTypeFor(opts, true);
922
+ }
923
+ return z.object(shape);
924
+ }
925
+ };
926
+
927
+ // src/core/drizzleTableBuilder.ts
928
+ import {
929
+ pgTable,
930
+ pgSchema,
931
+ text,
932
+ varchar,
933
+ integer,
934
+ doublePrecision,
935
+ boolean,
936
+ timestamp,
937
+ decimal,
938
+ json,
939
+ uuid,
940
+ bigint as pgBigint,
941
+ serial as pgSerial
942
+ } from "drizzle-orm/pg-core";
943
+ import {
944
+ mysqlTable,
945
+ mysqlSchema,
946
+ varchar as mysqlVarchar,
947
+ int,
948
+ double,
949
+ boolean as mysqlBoolean,
950
+ datetime,
951
+ decimal as mysqlDecimal,
952
+ json as mysqlJson,
953
+ text as mysqlText,
954
+ bigint as mysqlBigint,
955
+ mysqlEnum
956
+ } from "drizzle-orm/mysql-core";
957
+ function buildPgColumn(name, opts) {
958
+ const type = opts.type;
959
+ let col;
960
+ switch (type) {
961
+ case "uuid":
962
+ col = uuid(name);
963
+ break;
964
+ case "text":
965
+ col = text(name);
966
+ break;
967
+ case "integer":
968
+ col = integer(name);
969
+ break;
970
+ case "number":
971
+ col = doublePrecision(name);
972
+ break;
973
+ case "boolean":
974
+ col = boolean(name);
975
+ break;
976
+ case "date":
977
+ col = timestamp(name, { mode: "date" });
978
+ break;
979
+ case "decimal":
980
+ col = decimal(name, {
981
+ precision: opts.precision ?? 10,
982
+ scale: opts.scale ?? 2
983
+ });
984
+ break;
985
+ case "json":
986
+ col = json(name);
987
+ break;
988
+ case "enum":
989
+ col = text(name);
990
+ break;
991
+ case "bigint":
992
+ col = pgBigint(name, { mode: "number" });
993
+ break;
994
+ case "serial":
995
+ col = pgSerial(name);
996
+ break;
997
+ case "array":
998
+ col = text(name).array();
999
+ break;
1000
+ case "string":
1001
+ default:
1002
+ col = varchar(name, { length: opts.length ?? 255 });
1003
+ }
1004
+ if (opts.primary) col = col.primaryKey();
1005
+ if (opts.required && !opts.nullable && opts.type !== "serial") col = col.notNull();
1006
+ if (opts.unique) col = col.unique();
1007
+ if (opts.default !== void 0 && opts.type !== "serial") col = col.default(opts.default);
1008
+ return col;
1009
+ }
1010
+ function buildMysqlColumn(name, opts) {
1011
+ const type = opts.type;
1012
+ let col;
1013
+ switch (type) {
1014
+ case "uuid":
1015
+ col = mysqlVarchar(name, { length: 36 });
1016
+ break;
1017
+ case "text":
1018
+ col = mysqlText(name);
1019
+ break;
1020
+ case "integer":
1021
+ col = int(name);
1022
+ break;
1023
+ case "number":
1024
+ col = double(name);
1025
+ break;
1026
+ case "boolean":
1027
+ col = mysqlBoolean(name);
1028
+ break;
1029
+ case "date":
1030
+ col = datetime(name, { mode: "date" });
1031
+ break;
1032
+ case "decimal":
1033
+ col = mysqlDecimal(name, {
1034
+ precision: opts.precision ?? 10,
1035
+ scale: opts.scale ?? 2
1036
+ });
1037
+ break;
1038
+ case "json":
1039
+ col = mysqlJson(name);
1040
+ break;
1041
+ case "enum":
1042
+ if (!opts.enumValues?.length) {
1043
+ throw new Error(`Field "${name}" of type "enum" requires the enumValues option`);
1044
+ }
1045
+ col = mysqlEnum(name, opts.enumValues);
1046
+ break;
1047
+ case "bigint":
1048
+ col = mysqlBigint(name, { mode: "number" });
1049
+ break;
1050
+ case "serial":
1051
+ col = int(name).autoincrement();
1052
+ break;
1053
+ case "array":
1054
+ throw new Error(`Field type "array" is not supported in MySQL dialect. Use "json" instead.`);
1055
+ case "string":
1056
+ default:
1057
+ col = mysqlVarchar(name, { length: opts.length ?? 255 });
1058
+ }
1059
+ if (opts.primary) col = col.primaryKey();
1060
+ if (opts.required && !opts.nullable && opts.type !== "serial") col = col.notNull();
1061
+ if (opts.unique) col = col.unique();
1062
+ if (opts.default !== void 0 && opts.type !== "serial") col = col.default(opts.default);
1063
+ return col;
1064
+ }
1065
+ function buildPgColumns(fields, modelClass) {
1066
+ const cols = {};
1067
+ for (const [name, opts] of Object.entries(fields)) {
1068
+ cols[name] = buildPgColumn(name, opts);
1069
+ }
1070
+ if (modelClass.timestamps !== false) {
1071
+ cols.createdAt = timestamp("createdAt", { mode: "date" }).defaultNow().notNull();
1072
+ cols.updatedAt = timestamp("updatedAt", { mode: "date" }).defaultNow().notNull();
1073
+ }
1074
+ if (modelClass.softDelete) {
1075
+ cols.deletedAt = timestamp("deletedAt", { mode: "date" });
1076
+ }
1077
+ return cols;
1078
+ }
1079
+ function buildMysqlColumns(fields, modelClass) {
1080
+ const cols = {};
1081
+ for (const [name, opts] of Object.entries(fields)) {
1082
+ cols[name] = buildMysqlColumn(name, opts);
1083
+ }
1084
+ if (modelClass.timestamps !== false) {
1085
+ cols.createdAt = datetime("createdAt", { mode: "date" }).notNull();
1086
+ cols.updatedAt = datetime("updatedAt", { mode: "date" }).notNull();
1087
+ }
1088
+ if (modelClass.softDelete) {
1089
+ cols.deletedAt = datetime("deletedAt", { mode: "date" });
1090
+ }
1091
+ return cols;
1092
+ }
1093
+ var DrizzleTableBuilder = class {
1094
+ static build(modelClass, dialect) {
1095
+ const fields = getFieldMetadata(modelClass);
1096
+ const tableName = modelClass.getTableName();
1097
+ const schemaName = modelClass.schema;
1098
+ if (dialect === "postgresql") {
1099
+ const cols = buildPgColumns(fields, modelClass);
1100
+ return schemaName ? pgSchema(schemaName).table(tableName, cols) : pgTable(tableName, cols);
1101
+ }
1102
+ if (dialect === "mysql") {
1103
+ const cols = buildMysqlColumns(fields, modelClass);
1104
+ return schemaName ? mysqlSchema(schemaName).table(tableName, cols) : mysqlTable(tableName, cols);
1105
+ }
1106
+ throw new Error(`Unsupported dialect: ${dialect}. Use 'postgresql' or 'mysql'.`);
1107
+ }
1108
+ };
1109
+
1110
+ // src/core/crudora.ts
1111
+ var MAX_LIMIT = 1e3;
1112
+ var MAX_RELATIONS2 = 5;
1113
+ var SAFE_KEY_RE2 = /^[a-zA-Z_][a-zA-Z0-9_]*(_gt|_gte|_lt|_lte|_ne|_like|_in)?$/;
1114
+ function ok(data, meta) {
1115
+ return meta !== void 0 ? { success: true, data, meta } : { success: true, data };
1116
+ }
1117
+ function fail(code, message, details) {
1118
+ const error = { code, message };
1119
+ if (details !== void 0) error.details = details;
1120
+ return { success: false, error };
1121
+ }
1122
+ function zodDetails(issues) {
1123
+ return issues.map((issue) => ({
1124
+ field: issue.path.length ? issue.path.join(".") : "_root",
1125
+ message: issue.message
1126
+ }));
1127
+ }
1128
+ var Crudora = class {
1129
+ constructor(db, dialect, logger) {
1130
+ this.models = /* @__PURE__ */ new Map();
1131
+ this.tables = /* @__PURE__ */ new Map();
1132
+ this.repositories = /* @__PURE__ */ new Map();
1133
+ this.customRoutes = [];
1134
+ if (!db) {
1135
+ throw new Error(
1136
+ 'Crudora: db is required. Provide a Drizzle db instance:\n import { drizzle } from "drizzle-orm/node-postgres";\n import { Pool } from "pg";\n const db = drizzle(new Pool({ connectionString: process.env.DATABASE_URL }));'
1137
+ );
1138
+ }
1139
+ if (typeof db.select !== "function" || typeof db.insert !== "function") {
1140
+ throw new Error(
1141
+ "Crudora: the provided db object does not look like a Drizzle ORM instance. Make sure drizzle-orm is installed (npm install drizzle-orm) and that you are passing the result of drizzle(pool) or drizzle(client), not a raw pool/connection."
1142
+ );
1143
+ }
1144
+ this.db = db;
1145
+ this.dialect = dialect;
1146
+ this.logger = logger;
1147
+ }
1148
+ registerModel(...modelClasses) {
1149
+ for (const modelClass of modelClasses) {
1150
+ const table = DrizzleTableBuilder.build(modelClass, this.dialect);
1151
+ const repository = new Repository(modelClass, this.db, table, this.repositories);
1152
+ this.models.set(modelClass.name, modelClass);
1153
+ this.tables.set(modelClass.name, table);
1154
+ this.repositories.set(modelClass.name, repository);
1155
+ }
1156
+ return this;
1157
+ }
1158
+ /**
1159
+ * Register a model against a pre-built Drizzle table object (e.g. from `drizzle-kit introspect`).
1160
+ * Skips `DrizzleTableBuilder` — useful when the database already exists and the schema
1161
+ * was generated via introspection rather than Crudora decorators.
1162
+ *
1163
+ * Validation works if the model defines `@Field()` decorators matching the table columns.
1164
+ * Without decorators, POST/PUT bodies pass through without Zod validation.
1165
+ */
1166
+ registerTable(modelClass, table) {
1167
+ const repository = new Repository(modelClass, this.db, table, this.repositories);
1168
+ this.models.set(modelClass.name, modelClass);
1169
+ this.tables.set(modelClass.name, table);
1170
+ this.repositories.set(modelClass.name, repository);
1171
+ return this;
1172
+ }
1173
+ getRepository(modelClass) {
1174
+ const repository = this.repositories.get(modelClass.name);
1175
+ if (!repository) {
1176
+ throw new Error(`Repository for ${modelClass.name} not found. Did you register the model?`);
1177
+ }
1178
+ return repository;
1179
+ }
1180
+ /**
1181
+ * Returns the Drizzle table object for a registered model.
1182
+ * Useful for raw queries that need access to columns excluded by `hidden`
1183
+ * (e.g. fetching a password hash for authentication).
1184
+ *
1185
+ * @example
1186
+ * const usersTable = crudora.getTable(User);
1187
+ * const [row] = await db.select().from(usersTable).where(eq(usersTable.email, email)).limit(1);
1188
+ */
1189
+ getTable(modelClass) {
1190
+ const table = this.tables.get(modelClass.name);
1191
+ if (!table) {
1192
+ throw new Error(`Table for ${modelClass.name} not found. Did you register the model?`);
1193
+ }
1194
+ return table;
1195
+ }
1196
+ generateDrizzleSchema() {
1197
+ const modelClasses = Array.from(this.models.values());
1198
+ return SchemaGenerator.generateDrizzleSchema(modelClasses, this.dialect);
1199
+ }
1200
+ getValidationSchema(modelClass) {
1201
+ return ValidationGenerator.generateZodSchema(modelClass);
1202
+ }
1203
+ getStrictValidationSchema(modelClass) {
1204
+ return ValidationGenerator.generateStrictZodSchema(modelClass);
1205
+ }
1206
+ get(path, ...handlers) {
1207
+ this.customRoutes.push({ method: "GET", path, handlers });
1208
+ return this;
1209
+ }
1210
+ post(path, ...handlers) {
1211
+ this.customRoutes.push({ method: "POST", path, handlers });
1212
+ return this;
1213
+ }
1214
+ put(path, ...handlers) {
1215
+ this.customRoutes.push({ method: "PUT", path, handlers });
1216
+ return this;
1217
+ }
1218
+ delete(path, ...handlers) {
1219
+ this.customRoutes.push({ method: "DELETE", path, handlers });
1220
+ return this;
1221
+ }
1222
+ patch(path, ...handlers) {
1223
+ this.customRoutes.push({ method: "PATCH", path, handlers });
1224
+ return this;
1225
+ }
1226
+ /** Runs `fn` inside a database transaction and returns its result. */
1227
+ async transaction(fn) {
1228
+ return this.db.transaction(fn);
1229
+ }
1230
+ generateRoutes(app, basePath = "/api") {
1231
+ app.get(basePath, (_req, res) => {
1232
+ const routes = [];
1233
+ for (const [, modelClass] of this.models) {
1234
+ const routePath = `${basePath}/${modelClass.getTableName()}`;
1235
+ routes.push(
1236
+ { method: "GET", path: routePath, description: `List all ${modelClass.getTableName()}`, type: "CRUD" },
1237
+ { method: "GET", path: `${routePath}/:id`, description: `Get ${modelClass.getTableName()} by ID`, type: "CRUD" },
1238
+ { method: "POST", path: routePath, description: `Create new ${modelClass.getTableName()}`, type: "CRUD" },
1239
+ { method: "PUT", path: `${routePath}/:id`, description: `Replace ${modelClass.getTableName()} by ID`, type: "CRUD" },
1240
+ { method: "PATCH", path: `${routePath}/:id`, description: `Partial update ${modelClass.getTableName()} by ID`, type: "CRUD" },
1241
+ { method: "DELETE", path: `${routePath}/:id`, description: `Delete ${modelClass.getTableName()} by ID`, type: "CRUD" }
1242
+ );
1243
+ }
1244
+ for (const route of this.customRoutes) {
1245
+ routes.push({
1246
+ method: route.method,
1247
+ path: `${basePath}${route.path}`,
1248
+ description: `Custom ${route.method} route`,
1249
+ type: "Custom"
1250
+ });
1251
+ }
1252
+ res.json(ok({ routes }));
1253
+ });
1254
+ for (const [, modelClass] of this.models) {
1255
+ const repository = this.getRepository(modelClass);
1256
+ const validationSchema = this.getValidationSchema(modelClass);
1257
+ const strictValidationSchema = this.getStrictValidationSchema(modelClass);
1258
+ const routePath = `${basePath}/${modelClass.getTableName()}`;
1259
+ app.get(routePath, async (req, res) => {
1260
+ try {
1261
+ const {
1262
+ page,
1263
+ limit = "10",
1264
+ orderBy,
1265
+ order,
1266
+ cursor,
1267
+ // presence triggers cursor mode
1268
+ cursorField,
1269
+ select,
1270
+ // comma-separated field names
1271
+ with: withStr,
1272
+ // comma-separated relation names
1273
+ withDeleted,
1274
+ ...filters
1275
+ } = req.query;
1276
+ const selectFields = select ? select.split(",").map((f) => f.trim()).filter((f) => SAFE_KEY_RE2.test(f)) : void 0;
1277
+ const withRelations = withStr ? withStr.split(",").map((r) => r.trim()).filter((r) => SAFE_KEY_RE2.test(r)).slice(0, MAX_RELATIONS2) : void 0;
1278
+ const safeFilters = {};
1279
+ for (const [k, v] of Object.entries(filters)) {
1280
+ if (SAFE_KEY_RE2.test(k)) safeFilters[k] = v;
1281
+ }
1282
+ const whereFilters = Object.keys(safeFilters).length ? safeFilters : void 0;
1283
+ const rawLimit = Number(limit);
1284
+ const limitNum = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, MAX_LIMIT) : 10;
1285
+ const includeDeleted = withDeleted === "true";
1286
+ const orderByFields = orderBy ? orderBy.split(",").map((f) => f.trim()).filter(Boolean) : void 0;
1287
+ const orderValues = order ? order.split(",").map((o) => o.trim()).filter(Boolean) : void 0;
1288
+ const orderByArg = orderByFields && orderByFields.length === 1 ? orderByFields[0] : orderByFields;
1289
+ const orderArg = orderValues && orderValues.length === 1 ? orderValues[0] : orderValues;
1290
+ if (cursor !== void 0) {
1291
+ const result = await repository.findWithCursor({
1292
+ take: limitNum,
1293
+ cursor: cursor || null,
1294
+ cursorField,
1295
+ order: orderValues?.[0] ?? (order === "desc" ? "desc" : "asc"),
1296
+ where: whereFilters,
1297
+ select: selectFields,
1298
+ with: withRelations,
1299
+ withDeleted: includeDeleted
1300
+ });
1301
+ return res.json(ok(result.data, {
1302
+ cursor: { next: result.nextCursor, hasMore: result.hasMore }
1303
+ }));
1304
+ }
1305
+ const rawPage = Number(page ?? 1);
1306
+ const pageNum = Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
1307
+ const skip = (pageNum - 1) * limitNum;
1308
+ const items = await repository.findAll({
1309
+ skip,
1310
+ take: limitNum,
1311
+ where: whereFilters,
1312
+ orderBy: orderByArg,
1313
+ order: orderArg,
1314
+ select: selectFields,
1315
+ with: withRelations,
1316
+ withDeleted: includeDeleted
1317
+ });
1318
+ const total = await repository.count(whereFilters, includeDeleted);
1319
+ return res.json(ok(items, {
1320
+ pagination: { page: pageNum, limit: limitNum, total, pages: total === 0 ? 0 : Math.ceil(total / limitNum) }
1321
+ }));
1322
+ } catch (err) {
1323
+ this.logger?.error("GET request failed", {
1324
+ path: routePath,
1325
+ correlationId: req.correlationId,
1326
+ error: err instanceof Error ? err.message : String(err)
1327
+ });
1328
+ res.status(500).json(fail("INTERNAL_ERROR", "Internal server error"));
1329
+ }
1330
+ });
1331
+ app.get(`${routePath}/:id`, async (req, res) => {
1332
+ try {
1333
+ const { select, with: withStr, withDeleted } = req.query;
1334
+ const selectFields = select ? select.split(",").map((f) => f.trim()).filter((f) => SAFE_KEY_RE2.test(f)) : void 0;
1335
+ const withRelations = withStr ? withStr.split(",").map((r) => r.trim()).filter((r) => SAFE_KEY_RE2.test(r)).slice(0, MAX_RELATIONS2) : void 0;
1336
+ const item = await repository.findById(req.params.id, {
1337
+ select: selectFields,
1338
+ with: withRelations,
1339
+ withDeleted: withDeleted === "true"
1340
+ });
1341
+ if (!item) return res.status(404).json(fail("NOT_FOUND", "Resource not found"));
1342
+ return res.json(ok(item));
1343
+ } catch (err) {
1344
+ this.logger?.error("GET by ID request failed", {
1345
+ path: `${routePath}/:id`,
1346
+ id: req.params.id,
1347
+ correlationId: req.correlationId,
1348
+ error: err instanceof Error ? err.message : String(err)
1349
+ });
1350
+ res.status(500).json(fail("INTERNAL_ERROR", "Internal server error"));
1351
+ }
1352
+ });
1353
+ app.post(routePath, async (req, res) => {
1354
+ try {
1355
+ const validatedData = strictValidationSchema.parse(req.body);
1356
+ const item = await repository.create(validatedData);
1357
+ return res.status(201).json(ok(item));
1358
+ } catch (error) {
1359
+ if (error instanceof z2.ZodError) {
1360
+ return res.status(422).json(fail("VALIDATION_ERROR", "Validation failed", zodDetails(error.issues)));
1361
+ }
1362
+ this.logger?.error("POST request failed", {
1363
+ path: routePath,
1364
+ correlationId: req.correlationId,
1365
+ error: error instanceof Error ? error.message : String(error)
1366
+ });
1367
+ res.status(500).json(fail("INTERNAL_ERROR", "Internal server error"));
1368
+ }
1369
+ });
1370
+ app.put(`${routePath}/:id`, async (req, res) => {
1371
+ try {
1372
+ const validatedData = strictValidationSchema.parse(req.body);
1373
+ const item = await repository.update(req.params.id, validatedData);
1374
+ return res.json(ok(item));
1375
+ } catch (error) {
1376
+ if (error instanceof NotFoundError) return res.status(404).json(fail("NOT_FOUND", error.message));
1377
+ if (error instanceof z2.ZodError) {
1378
+ return res.status(422).json(fail("VALIDATION_ERROR", "Validation failed", zodDetails(error.issues)));
1379
+ }
1380
+ this.logger?.error("PUT request failed", {
1381
+ path: `${routePath}/:id`,
1382
+ id: req.params.id,
1383
+ correlationId: req.correlationId,
1384
+ error: error instanceof Error ? error.message : String(error)
1385
+ });
1386
+ res.status(500).json(fail("INTERNAL_ERROR", "Internal server error"));
1387
+ }
1388
+ });
1389
+ app.patch(`${routePath}/:id`, async (req, res) => {
1390
+ try {
1391
+ const validatedData = validationSchema.parse(req.body);
1392
+ const item = await repository.update(req.params.id, validatedData);
1393
+ return res.json(ok(item));
1394
+ } catch (error) {
1395
+ if (error instanceof NotFoundError) return res.status(404).json(fail("NOT_FOUND", error.message));
1396
+ if (error instanceof z2.ZodError) {
1397
+ return res.status(422).json(fail("VALIDATION_ERROR", "Validation failed", zodDetails(error.issues)));
1398
+ }
1399
+ this.logger?.error("PATCH request failed", {
1400
+ path: `${routePath}/:id`,
1401
+ id: req.params.id,
1402
+ correlationId: req.correlationId,
1403
+ error: error instanceof Error ? error.message : String(error)
1404
+ });
1405
+ res.status(500).json(fail("INTERNAL_ERROR", "Internal server error"));
1406
+ }
1407
+ });
1408
+ app.delete(`${routePath}/:id`, async (req, res) => {
1409
+ try {
1410
+ await repository.delete(req.params.id);
1411
+ return res.status(204).send();
1412
+ } catch (error) {
1413
+ if (error instanceof NotFoundError) return res.status(404).json(fail("NOT_FOUND", error.message));
1414
+ this.logger?.error("DELETE request failed", {
1415
+ path: `${routePath}/:id`,
1416
+ id: req.params.id,
1417
+ correlationId: req.correlationId,
1418
+ error: error instanceof Error ? error.message : String(error)
1419
+ });
1420
+ res.status(500).json(fail("INTERNAL_ERROR", "Internal server error"));
1421
+ }
1422
+ });
1423
+ }
1424
+ for (const route of this.customRoutes) {
1425
+ const fullPath = `${basePath}${route.path}`;
1426
+ const { method, handlers } = route;
1427
+ switch (method.toLowerCase()) {
1428
+ case "get":
1429
+ app.get(fullPath, ...handlers);
1430
+ break;
1431
+ case "post":
1432
+ app.post(fullPath, ...handlers);
1433
+ break;
1434
+ case "put":
1435
+ app.put(fullPath, ...handlers);
1436
+ break;
1437
+ case "delete":
1438
+ app.delete(fullPath, ...handlers);
1439
+ break;
1440
+ case "patch":
1441
+ app.patch(fullPath, ...handlers);
1442
+ break;
1443
+ }
1444
+ }
1445
+ }
1446
+ };
1447
+
1448
+ // src/core/crudoraServer.ts
1449
+ import express from "express";
1450
+ import { randomUUID as randomUUID2 } from "crypto";
1451
+ function createRateLimiter(config) {
1452
+ const store = /* @__PURE__ */ new Map();
1453
+ const timer = setInterval(() => {
1454
+ const cutoff = Date.now() - config.windowMs;
1455
+ for (const [key, hits] of store.entries()) {
1456
+ const active = hits.filter((t) => t > cutoff);
1457
+ if (active.length === 0) store.delete(key);
1458
+ else store.set(key, active);
1459
+ }
1460
+ }, config.windowMs);
1461
+ if (timer.unref) timer.unref();
1462
+ return (req, res, next) => {
1463
+ const key = config.keyGenerator(req);
1464
+ const now = Date.now();
1465
+ const cutoff = now - config.windowMs;
1466
+ const hits = (store.get(key) ?? []).filter((t) => t > cutoff);
1467
+ const remaining = Math.max(0, config.max - hits.length - 1);
1468
+ const resetAt = Math.ceil((now + config.windowMs) / 1e3);
1469
+ res.setHeader("X-RateLimit-Limit", config.max);
1470
+ res.setHeader("X-RateLimit-Remaining", remaining);
1471
+ res.setHeader("X-RateLimit-Reset", resetAt);
1472
+ if (hits.length >= config.max) {
1473
+ res.setHeader("Retry-After", Math.ceil(config.windowMs / 1e3));
1474
+ return res.status(429).json({
1475
+ success: false,
1476
+ error: { code: "RATE_LIMIT_EXCEEDED", message: config.message }
1477
+ });
1478
+ }
1479
+ hits.push(now);
1480
+ store.set(key, hits);
1481
+ next();
1482
+ };
1483
+ }
1484
+ function createDefaultLogger() {
1485
+ const fmt = (level, msg, ctx) => JSON.stringify({ level, time: (/* @__PURE__ */ new Date()).toISOString(), msg, ...ctx });
1486
+ return {
1487
+ error: (msg, ctx) => console.error(fmt("error", msg, ctx)),
1488
+ warn: (msg, ctx) => console.warn(fmt("warn", msg, ctx)),
1489
+ info: (msg, ctx) => console.info(fmt("info", msg, ctx)),
1490
+ debug: (msg, ctx) => console.debug(fmt("debug", msg, ctx))
1491
+ };
1492
+ }
1493
+ var CrudoraServer = class {
1494
+ constructor(config) {
1495
+ const resolvedLogger = config.logger === void 0 ? createDefaultLogger() : config.logger;
1496
+ const resolvedRateLimit = config.rateLimit === false ? false : {
1497
+ windowMs: config.rateLimit?.windowMs ?? 6e4,
1498
+ max: config.rateLimit?.max ?? 100,
1499
+ message: config.rateLimit?.message ?? "Too many requests",
1500
+ keyGenerator: config.rateLimit?.keyGenerator ?? ((req) => String(req.ip ?? "unknown"))
1501
+ };
1502
+ this.config = {
1503
+ port: 3e3,
1504
+ cors: true,
1505
+ bodyParser: true,
1506
+ bodyParserLimit: "100kb",
1507
+ basePath: "/api",
1508
+ ...config,
1509
+ logger: resolvedLogger,
1510
+ rateLimit: resolvedRateLimit
1511
+ };
1512
+ this.app = express();
1513
+ this.crudora = new Crudora(
1514
+ config.db,
1515
+ config.dialect,
1516
+ resolvedLogger === false ? void 0 : resolvedLogger
1517
+ );
1518
+ this.setupMiddleware();
1519
+ }
1520
+ setupMiddleware() {
1521
+ this.app.use((_req, res, next) => {
1522
+ res.setHeader("X-Content-Type-Options", "nosniff");
1523
+ res.setHeader("X-Frame-Options", "DENY");
1524
+ res.setHeader("X-XSS-Protection", "0");
1525
+ next();
1526
+ });
1527
+ this.app.use((req, _res, next) => {
1528
+ req.correlationId = randomUUID2();
1529
+ next();
1530
+ });
1531
+ if (this.config.rateLimit !== false) {
1532
+ this.app.use(createRateLimiter(this.config.rateLimit));
1533
+ }
1534
+ if (this.config.bodyParser) {
1535
+ this.app.use(express.json({ limit: this.config.bodyParserLimit }));
1536
+ this.app.use(express.urlencoded({ extended: true, limit: this.config.bodyParserLimit }));
1537
+ }
1538
+ if (this.config.cors !== false) {
1539
+ this.app.use((req, res, next) => {
1540
+ const cors = this.config.cors;
1541
+ if (cors === true || cors === void 0) {
1542
+ res.header("Access-Control-Allow-Origin", "*");
1543
+ } else if (typeof cors === "string") {
1544
+ res.header("Access-Control-Allow-Origin", cors);
1545
+ res.header("Vary", "Origin");
1546
+ } else if (Array.isArray(cors)) {
1547
+ const requestOrigin = req.headers.origin;
1548
+ if (requestOrigin && cors.includes(requestOrigin)) {
1549
+ res.header("Access-Control-Allow-Origin", requestOrigin);
1550
+ res.header("Vary", "Origin");
1551
+ }
1552
+ }
1553
+ res.header("Access-Control-Allow-Methods", "GET,PUT,PATCH,POST,DELETE,OPTIONS");
1554
+ res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
1555
+ if (req.method === "OPTIONS") {
1556
+ res.sendStatus(204);
1557
+ } else {
1558
+ next();
1559
+ }
1560
+ });
1561
+ }
1562
+ }
1563
+ registerModel(...modelClasses) {
1564
+ this.crudora.registerModel(...modelClasses);
1565
+ return this;
1566
+ }
1567
+ registerTable(modelClass, table) {
1568
+ this.crudora.registerTable(modelClass, table);
1569
+ return this;
1570
+ }
1571
+ generateRoutes() {
1572
+ this.crudora.generateRoutes(this.app, this.config.basePath);
1573
+ return this;
1574
+ }
1575
+ use(middleware) {
1576
+ this.app.use(middleware);
1577
+ return this;
1578
+ }
1579
+ listen(callback) {
1580
+ this.app.listen(this.config.port, () => {
1581
+ console.log(`\u{1F680} Crudora server running on port ${this.config.port}`);
1582
+ console.log(`\u{1F4DA} API available at http://localhost:${this.config.port}${this.config.basePath}`);
1583
+ if (callback) callback();
1584
+ });
1585
+ }
1586
+ getApp() {
1587
+ return this.app;
1588
+ }
1589
+ getCrudora() {
1590
+ return this.crudora;
1591
+ }
1592
+ /**
1593
+ * Returns the Drizzle table object for a registered model — delegates to `Crudora.getTable()`.
1594
+ * Use this when you need raw DB access to columns excluded by `static hidden`
1595
+ * (e.g. a login route that must read the password hash).
1596
+ *
1597
+ * @example
1598
+ * const usersTable = server.getTable(User);
1599
+ * const [row] = await db.select().from(usersTable).where(eq(usersTable.email, email)).limit(1);
1600
+ */
1601
+ getTable(modelClass) {
1602
+ return this.crudora.getTable(modelClass);
1603
+ }
1604
+ get(path, ...handlers) {
1605
+ this.crudora.get(path, ...handlers);
1606
+ return this;
1607
+ }
1608
+ post(path, ...handlers) {
1609
+ this.crudora.post(path, ...handlers);
1610
+ return this;
1611
+ }
1612
+ put(path, ...handlers) {
1613
+ this.crudora.put(path, ...handlers);
1614
+ return this;
1615
+ }
1616
+ delete(path, ...handlers) {
1617
+ this.crudora.delete(path, ...handlers);
1618
+ return this;
1619
+ }
1620
+ patch(path, ...handlers) {
1621
+ this.crudora.patch(path, ...handlers);
1622
+ return this;
1623
+ }
1624
+ };
1625
+
1626
+ // src/core/model.ts
1627
+ var Model = class {
1628
+ static getTableName() {
1629
+ return this.tableName || this.name.toLowerCase() + "s";
1630
+ }
1631
+ static getPrimaryKey() {
1632
+ return this.primaryKey || "id";
1633
+ }
1634
+ // Lifecycle Hooks
1635
+ static async beforeCreate(_data) {
1636
+ return _data;
1637
+ }
1638
+ static async afterCreate(_data, result) {
1639
+ return result;
1640
+ }
1641
+ static async afterCreateMany(_records) {
1642
+ return _records;
1643
+ }
1644
+ static async beforeUpdate(_id, _data) {
1645
+ return _data;
1646
+ }
1647
+ static async afterUpdate(_id, _data, result) {
1648
+ return result;
1649
+ }
1650
+ static async beforeDelete(_id) {
1651
+ }
1652
+ static async afterDelete(_id, result) {
1653
+ return result;
1654
+ }
1655
+ static async beforeFind(options) {
1656
+ return options;
1657
+ }
1658
+ static async afterFind(results) {
1659
+ return results;
1660
+ }
1661
+ async beforeSave() {
1662
+ }
1663
+ async afterSave() {
1664
+ }
1665
+ };
1666
+ Model.primaryKey = "id";
1667
+ Model.timestamps = true;
1668
+ Model.softDelete = false;
1669
+ export {
1670
+ BelongsTo,
1671
+ BelongsToMany,
1672
+ Crudora,
1673
+ CrudoraServer,
1674
+ DrizzleTableBuilder,
1675
+ Field,
1676
+ HasMany,
1677
+ HasOne,
1678
+ Model,
1679
+ NotFoundError,
1680
+ Repository,
1681
+ SchemaGenerator,
1682
+ ValidationGenerator,
1683
+ getFieldMetadata,
1684
+ getRelationMetadata
1685
+ };
28
1686
  //# sourceMappingURL=index.js.map