@gzl10/nexus-backend 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1675 @@
1
+ // src/testing/test-helpers.ts
2
+ import { vi } from "vitest";
3
+
4
+ // src/db/filter-helpers.ts
5
+ function applyInMemoryFilters(items, filters) {
6
+ return items.filter((item) => {
7
+ const record = item;
8
+ return matchRecordFilters(record, filters);
9
+ });
10
+ }
11
+ function matchRecordFilters(record, filters) {
12
+ for (const [key, value] of Object.entries(filters)) {
13
+ if (value === void 0) continue;
14
+ if (key === "$or") {
15
+ if (Array.isArray(value) && value.length > 0) {
16
+ const orMatch = value.some(
17
+ (group) => matchRecordFilters(record, group)
18
+ );
19
+ if (!orMatch) return false;
20
+ }
21
+ continue;
22
+ }
23
+ const itemValue = record[key];
24
+ if (value === null) {
25
+ if (itemValue !== null && itemValue !== void 0) return false;
26
+ continue;
27
+ }
28
+ if (value === "") continue;
29
+ if (Array.isArray(value)) {
30
+ if (value.length === 0) continue;
31
+ if (!value.includes(itemValue)) return false;
32
+ continue;
33
+ }
34
+ if (typeof value === "object" && !Array.isArray(value)) {
35
+ if (!matchFilterOperators(itemValue, value)) return false;
36
+ continue;
37
+ }
38
+ if (itemValue !== value) return false;
39
+ }
40
+ return true;
41
+ }
42
+ function matchFilterOperators(itemValue, operators) {
43
+ for (const [op, val] of Object.entries(operators)) {
44
+ if (val === void 0) continue;
45
+ switch (op) {
46
+ case "$eq":
47
+ if (val === null) {
48
+ if (itemValue !== null && itemValue !== void 0) return false;
49
+ } else if (itemValue !== val) return false;
50
+ break;
51
+ case "$ne":
52
+ if (val === null) {
53
+ if (itemValue === null || itemValue === void 0) return false;
54
+ } else if (itemValue === val) return false;
55
+ break;
56
+ case "$gt":
57
+ if (typeof itemValue !== "number" && typeof itemValue !== "string") return false;
58
+ if (!(itemValue > val)) return false;
59
+ break;
60
+ case "$gte":
61
+ if (typeof itemValue !== "number" && typeof itemValue !== "string") return false;
62
+ if (!(itemValue >= val)) return false;
63
+ break;
64
+ case "$lt":
65
+ if (typeof itemValue !== "number" && typeof itemValue !== "string") return false;
66
+ if (!(itemValue < val)) return false;
67
+ break;
68
+ case "$lte":
69
+ if (typeof itemValue !== "number" && typeof itemValue !== "string") return false;
70
+ if (!(itemValue <= val)) return false;
71
+ break;
72
+ case "$contains":
73
+ if (typeof itemValue !== "string" || typeof val !== "string") return false;
74
+ if (!itemValue.toLowerCase().includes(val.toLowerCase())) return false;
75
+ break;
76
+ case "$startswith":
77
+ if (typeof itemValue !== "string" || typeof val !== "string") return false;
78
+ if (!itemValue.toLowerCase().startsWith(val.toLowerCase())) return false;
79
+ break;
80
+ case "$endswith":
81
+ if (typeof itemValue !== "string" || typeof val !== "string") return false;
82
+ if (!itemValue.toLowerCase().endsWith(val.toLowerCase())) return false;
83
+ break;
84
+ case "$in":
85
+ if (!Array.isArray(val) || val.length === 0) break;
86
+ if (!val.includes(itemValue)) return false;
87
+ break;
88
+ case "$nin":
89
+ if (!Array.isArray(val) || val.length === 0) break;
90
+ if (val.includes(itemValue)) return false;
91
+ break;
92
+ case "$isnull":
93
+ if (val === true && itemValue !== null && itemValue !== void 0) return false;
94
+ if (val === false && (itemValue === null || itemValue === void 0)) return false;
95
+ break;
96
+ case "$between":
97
+ if (!Array.isArray(val) || val.length !== 2) break;
98
+ if (typeof itemValue !== "number" && typeof itemValue !== "string") return false;
99
+ if (!(itemValue >= val[0] && itemValue <= val[1])) return false;
100
+ break;
101
+ }
102
+ }
103
+ return true;
104
+ }
105
+
106
+ // src/db/memory-adapter.ts
107
+ var DEFAULT_PAGE = 1;
108
+ var DEFAULT_LIMIT = 20;
109
+ var DEFAULT_MAX_LIMIT = 1e4;
110
+ function generateId() {
111
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
112
+ const r = Math.random() * 16 | 0;
113
+ const v = c === "x" ? r : r & 3 | 8;
114
+ return v.toString(16);
115
+ });
116
+ }
117
+ var InMemoryAdapter = class {
118
+ tables = /* @__PURE__ */ new Map();
119
+ ttlMap = /* @__PURE__ */ new Map();
120
+ get raw() {
121
+ return this.tables;
122
+ }
123
+ clear() {
124
+ for (const metadata of this.ttlMap.values()) {
125
+ clearTimeout(metadata.timerId);
126
+ }
127
+ this.ttlMap.clear();
128
+ this.tables.clear();
129
+ }
130
+ clearTable(table) {
131
+ this.tables.delete(table);
132
+ }
133
+ seed(table, records) {
134
+ const tableData = this.getOrCreateTable(table);
135
+ for (const record of records) {
136
+ const id = record["id"] || generateId();
137
+ const now = (/* @__PURE__ */ new Date()).toISOString();
138
+ tableData.set(id, {
139
+ ...record,
140
+ id,
141
+ created_at: record["created_at"] || now,
142
+ updated_at: record["updated_at"] || now
143
+ });
144
+ }
145
+ }
146
+ getOrCreateTable(table) {
147
+ let tableData = this.tables.get(table);
148
+ if (!tableData) {
149
+ tableData = /* @__PURE__ */ new Map();
150
+ this.tables.set(table, tableData);
151
+ }
152
+ return tableData;
153
+ }
154
+ // ---- DatabaseAdapter interface ----
155
+ async findMany(table, query) {
156
+ const maxLimit = query?.maxLimit ?? DEFAULT_MAX_LIMIT;
157
+ const page = Math.max(1, query?.page ?? DEFAULT_PAGE);
158
+ const limit = Math.min(maxLimit, Math.max(1, query?.limit ?? DEFAULT_LIMIT));
159
+ const offset = (page - 1) * limit;
160
+ const tableData = this.tables.get(table);
161
+ if (!tableData) {
162
+ return { items: [], total: 0, page, limit, totalPages: 0, hasNext: false };
163
+ }
164
+ let items = Array.from(tableData.values());
165
+ if (query?.filters) {
166
+ items = applyInMemoryFilters(items, query.filters);
167
+ }
168
+ if (query?.sort) {
169
+ const sortField = query.sort;
170
+ const order = query.order ?? "asc";
171
+ items.sort((a, b) => {
172
+ const aVal = a[sortField];
173
+ const bVal = b[sortField];
174
+ if (aVal === bVal) return 0;
175
+ if (aVal === null || aVal === void 0) return 1;
176
+ if (bVal === null || bVal === void 0) return -1;
177
+ const comparison = aVal < bVal ? -1 : 1;
178
+ return order === "asc" ? comparison : -comparison;
179
+ });
180
+ }
181
+ const total = items.length;
182
+ items = items.slice(offset, offset + limit);
183
+ const totalPages = Math.ceil(total / limit);
184
+ return { items, total, page, limit, totalPages, hasNext: page < totalPages };
185
+ }
186
+ async findOne(table, filters) {
187
+ const tableData = this.tables.get(table);
188
+ if (!tableData) return null;
189
+ for (const record of tableData.values()) {
190
+ const match = Object.entries(filters).every(([k, v]) => record[k] === v);
191
+ if (match) return record;
192
+ }
193
+ return null;
194
+ }
195
+ async findById(table, id) {
196
+ const tableData = this.tables.get(table);
197
+ if (!tableData) return null;
198
+ return tableData.get(id) ?? null;
199
+ }
200
+ async count(table, filters) {
201
+ const tableData = this.tables.get(table);
202
+ if (!tableData) return 0;
203
+ if (!filters || Object.keys(filters).length === 0) return tableData.size;
204
+ const items = applyInMemoryFilters(Array.from(tableData.values()), filters);
205
+ return items.length;
206
+ }
207
+ async insert(table, data) {
208
+ const tableData = this.getOrCreateTable(table);
209
+ const id = data["id"] || generateId();
210
+ const now = (/* @__PURE__ */ new Date()).toISOString();
211
+ const record = { ...data, id, created_at: now, updated_at: now };
212
+ tableData.set(id, record);
213
+ return record;
214
+ }
215
+ async update(table, id, data) {
216
+ const tableData = this.tables.get(table);
217
+ if (!tableData) throw new Error(`Table "${table}" not found`);
218
+ const existing = tableData.get(id);
219
+ if (!existing) throw new Error(`Record "${id}" not found in table "${table}"`);
220
+ const updated = {
221
+ ...existing,
222
+ ...data,
223
+ id,
224
+ created_at: existing.created_at,
225
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
226
+ };
227
+ tableData.set(id, updated);
228
+ return updated;
229
+ }
230
+ async delete(table, id) {
231
+ const tableData = this.tables.get(table);
232
+ if (!tableData) return false;
233
+ return tableData.delete(id);
234
+ }
235
+ async transaction(fn) {
236
+ return fn(this);
237
+ }
238
+ // ---- TTL Methods ----
239
+ getTtlKey(table, id) {
240
+ return `${table}:${id}`;
241
+ }
242
+ async insertWithTtl(table, data, ttlSeconds) {
243
+ const tableData = this.getOrCreateTable(table);
244
+ const id = data["id"] || generateId();
245
+ const now = (/* @__PURE__ */ new Date()).toISOString();
246
+ const expires_at = new Date(Date.now() + ttlSeconds * 1e3).toISOString();
247
+ const record = { ...data, id, created_at: now, updated_at: now, expires_at };
248
+ tableData.set(id, record);
249
+ const ttlKey = this.getTtlKey(table, id);
250
+ const timerId = setTimeout(() => this.deleteWithTtlCleanup(table, id), ttlSeconds * 1e3);
251
+ this.ttlMap.set(ttlKey, { expiresAt: Date.now() + ttlSeconds * 1e3, timerId });
252
+ return record;
253
+ }
254
+ deleteWithTtlCleanup(table, id) {
255
+ const tableData = this.tables.get(table);
256
+ if (tableData) tableData.delete(id);
257
+ const ttlKey = this.getTtlKey(table, id);
258
+ const metadata = this.ttlMap.get(ttlKey);
259
+ if (metadata) {
260
+ clearTimeout(metadata.timerId);
261
+ this.ttlMap.delete(ttlKey);
262
+ }
263
+ }
264
+ async setTtl(table, id, ttlSeconds) {
265
+ const tableData = this.tables.get(table);
266
+ if (!tableData) return false;
267
+ const record = tableData.get(id);
268
+ if (!record) return false;
269
+ record["expires_at"] = new Date(Date.now() + ttlSeconds * 1e3).toISOString();
270
+ record["updated_at"] = (/* @__PURE__ */ new Date()).toISOString();
271
+ const ttlKey = this.getTtlKey(table, id);
272
+ const existing = this.ttlMap.get(ttlKey);
273
+ if (existing) clearTimeout(existing.timerId);
274
+ const timerId = setTimeout(() => this.deleteWithTtlCleanup(table, id), ttlSeconds * 1e3);
275
+ this.ttlMap.set(ttlKey, { expiresAt: Date.now() + ttlSeconds * 1e3, timerId });
276
+ return true;
277
+ }
278
+ async getTtl(table, id) {
279
+ const tableData = this.tables.get(table);
280
+ if (!tableData || !tableData.has(id)) return -2;
281
+ const ttlKey = this.getTtlKey(table, id);
282
+ const metadata = this.ttlMap.get(ttlKey);
283
+ if (!metadata) return -1;
284
+ const remainingMs = metadata.expiresAt - Date.now();
285
+ return remainingMs <= 0 ? -2 : Math.ceil(remainingMs / 1e3);
286
+ }
287
+ async cleanupExpiredIds(_table) {
288
+ return 0;
289
+ }
290
+ };
291
+ function createInMemoryAdapter() {
292
+ return new InMemoryAdapter();
293
+ }
294
+
295
+ // src/core/cache/lru-cache.ts
296
+ var LRUCache = class {
297
+ cache;
298
+ maxEntries;
299
+ defaultTTL;
300
+ hits = 0;
301
+ misses = 0;
302
+ constructor(options) {
303
+ this.cache = /* @__PURE__ */ new Map();
304
+ this.maxEntries = options?.maxEntries ?? 100;
305
+ this.defaultTTL = options?.defaultTTL ?? 60;
306
+ }
307
+ /**
308
+ * Get value from cache
309
+ * Returns null if not found or expired
310
+ * Moves entry to end (most recently used)
311
+ */
312
+ get(key) {
313
+ const entry = this.cache.get(key);
314
+ if (!entry) {
315
+ this.misses++;
316
+ return null;
317
+ }
318
+ if (entry.expires > 0 && entry.expires < Date.now()) {
319
+ this.cache.delete(key);
320
+ this.misses++;
321
+ return null;
322
+ }
323
+ this.cache.delete(key);
324
+ this.cache.set(key, entry);
325
+ this.hits++;
326
+ return entry.data;
327
+ }
328
+ /**
329
+ * Set value in cache
330
+ * Evicts oldest entry if max entries reached
331
+ * @param ttl TTL in seconds (0 = no expiration, undefined = use default)
332
+ */
333
+ set(key, data, ttl) {
334
+ this.cache.delete(key);
335
+ if (this.cache.size >= this.maxEntries) {
336
+ const oldestKey = this.cache.keys().next().value;
337
+ if (oldestKey) {
338
+ this.cache.delete(oldestKey);
339
+ }
340
+ }
341
+ const actualTTL = ttl ?? this.defaultTTL;
342
+ const expires = actualTTL > 0 ? Date.now() + actualTTL * 1e3 : 0;
343
+ this.cache.set(key, { data, expires });
344
+ }
345
+ /**
346
+ * Delete specific key
347
+ */
348
+ delete(key) {
349
+ return this.cache.delete(key);
350
+ }
351
+ /**
352
+ * Delete all keys matching prefix
353
+ * @returns Number of deleted entries
354
+ */
355
+ deleteByPrefix(prefix) {
356
+ let deleted = 0;
357
+ for (const key of this.cache.keys()) {
358
+ if (key.startsWith(prefix)) {
359
+ this.cache.delete(key);
360
+ deleted++;
361
+ }
362
+ }
363
+ return deleted;
364
+ }
365
+ /**
366
+ * Clear all entries
367
+ */
368
+ clear() {
369
+ this.cache.clear();
370
+ }
371
+ /**
372
+ * Check if key exists and is not expired
373
+ */
374
+ has(key) {
375
+ const entry = this.cache.get(key);
376
+ if (!entry) return false;
377
+ if (entry.expires > 0 && entry.expires < Date.now()) {
378
+ this.cache.delete(key);
379
+ return false;
380
+ }
381
+ return true;
382
+ }
383
+ /**
384
+ * Get current size
385
+ */
386
+ get size() {
387
+ return this.cache.size;
388
+ }
389
+ /**
390
+ * Get cache statistics
391
+ */
392
+ getStats() {
393
+ const total = this.hits + this.misses;
394
+ return {
395
+ hits: this.hits,
396
+ misses: this.misses,
397
+ hitRate: total > 0 ? this.hits / total : 0,
398
+ size: this.cache.size,
399
+ maxEntries: this.maxEntries
400
+ };
401
+ }
402
+ /**
403
+ * Reset statistics
404
+ */
405
+ resetStats() {
406
+ this.hits = 0;
407
+ this.misses = 0;
408
+ }
409
+ /**
410
+ * Prune expired entries
411
+ * Call periodically to clean up memory
412
+ */
413
+ prune() {
414
+ const now = Date.now();
415
+ let pruned = 0;
416
+ for (const [key, entry] of this.cache.entries()) {
417
+ if (entry.expires > 0 && entry.expires < now) {
418
+ this.cache.delete(key);
419
+ pruned++;
420
+ }
421
+ }
422
+ return pruned;
423
+ }
424
+ };
425
+
426
+ // src/core/cache/managed-cache.ts
427
+ var ManagedCacheImpl = class {
428
+ constructor(name, options, onAddRules) {
429
+ this.name = name;
430
+ this.cache = new LRUCache({
431
+ maxEntries: options?.maxEntries,
432
+ defaultTTL: options?.defaultTTL
433
+ });
434
+ this.onAddRules = onAddRules;
435
+ }
436
+ cache;
437
+ onAddRules;
438
+ async get(key) {
439
+ return this.cache.get(key);
440
+ }
441
+ async set(key, value, ttl) {
442
+ this.cache.set(key, value, ttl);
443
+ }
444
+ async delete(key) {
445
+ return this.cache.delete(key);
446
+ }
447
+ async deleteByPrefix(prefix) {
448
+ return this.cache.deleteByPrefix(prefix);
449
+ }
450
+ async has(key) {
451
+ return this.cache.has(key);
452
+ }
453
+ async clear() {
454
+ this.cache.clear();
455
+ }
456
+ getStats() {
457
+ return this.cache.getStats();
458
+ }
459
+ resetStats() {
460
+ this.cache.resetStats();
461
+ }
462
+ addInvalidationRules(events) {
463
+ this.onAddRules?.(events);
464
+ }
465
+ async prune() {
466
+ return this.cache.prune();
467
+ }
468
+ async getSize() {
469
+ return this.cache.size;
470
+ }
471
+ };
472
+
473
+ // src/db/query-helpers.ts
474
+ var DEFAULT_PAGE2 = 1;
475
+ var DEFAULT_LIMIT2 = 20;
476
+ var DEFAULT_MAX_LIMIT2 = 100;
477
+ function getPagination(query) {
478
+ const maxLimit = query?.maxLimit ?? DEFAULT_MAX_LIMIT2;
479
+ const page = Math.max(1, query?.page ?? DEFAULT_PAGE2);
480
+ const limit = Math.min(maxLimit, Math.max(1, query?.limit ?? DEFAULT_LIMIT2));
481
+ const offset = (page - 1) * limit;
482
+ return { page, limit, offset };
483
+ }
484
+ function buildPaginatedResult(items, total, pagination) {
485
+ const { page, limit } = pagination;
486
+ const totalPages = Math.ceil(total / limit);
487
+ return {
488
+ items,
489
+ total,
490
+ page,
491
+ limit,
492
+ totalPages,
493
+ hasNext: page < totalPages
494
+ };
495
+ }
496
+ function getSearchableFields(definition) {
497
+ return Object.entries(definition.fields ?? {}).filter(([_, f]) => f.meta?.searchable === true).map(([name]) => name);
498
+ }
499
+ function applySearchFilter(qb, definition, search) {
500
+ const searchableFields = getSearchableFields(definition);
501
+ if (searchableFields.length === 0 && "labelField" in definition && definition.labelField) {
502
+ searchableFields.push(definition.labelField);
503
+ }
504
+ if (searchableFields.length > 0) {
505
+ const searchPattern = `%${search}%`;
506
+ qb.where(function() {
507
+ for (const field of searchableFields) {
508
+ this.orWhere(field, "like", searchPattern);
509
+ }
510
+ });
511
+ }
512
+ return qb;
513
+ }
514
+
515
+ // src/core/errors/error-codes.ts
516
+ var ErrorCodes = {
517
+ // Auth
518
+ AUTH_INVALID_CREDENTIALS: "AUTH_INVALID_CREDENTIALS",
519
+ AUTH_TOKEN_EXPIRED: "AUTH_TOKEN_EXPIRED",
520
+ AUTH_TOKEN_INVALID: "AUTH_TOKEN_INVALID",
521
+ AUTH_TOKEN_REQUIRED: "AUTH_TOKEN_REQUIRED",
522
+ AUTH_OTP_REQUIRED: "AUTH_OTP_REQUIRED",
523
+ AUTH_OTP_INVALID: "AUTH_OTP_INVALID",
524
+ AUTH_REFRESH_TOKEN_REQUIRED: "AUTH_REFRESH_TOKEN_REQUIRED",
525
+ AUTH_REFRESH_TOKEN_INVALID: "AUTH_REFRESH_TOKEN_INVALID",
526
+ AUTH_REFRESH_TOKEN_EXPIRED: "AUTH_REFRESH_TOKEN_EXPIRED",
527
+ AUTH_SESSION_NOT_FOUND: "AUTH_SESSION_NOT_FOUND",
528
+ AUTH_SESSION_SELF_REVOKE: "AUTH_SESSION_SELF_REVOKE",
529
+ AUTH_VERIFICATION_CODE_INVALID: "AUTH_VERIFICATION_CODE_INVALID",
530
+ AUTH_REGISTRATION_DISABLED: "AUTH_REGISTRATION_DISABLED",
531
+ AUTH_AUTO_CREATE_DISABLED: "AUTH_AUTO_CREATE_DISABLED",
532
+ // User
533
+ USER_NOT_FOUND: "USER_NOT_FOUND",
534
+ USER_EMAIL_EXISTS: "USER_EMAIL_EXISTS",
535
+ USER_NOT_AUTHENTICATED: "USER_NOT_AUTHENTICATED",
536
+ // Role
537
+ ROLE_NOT_FOUND: "ROLE_NOT_FOUND",
538
+ ROLE_NAME_EXISTS: "ROLE_NAME_EXISTS",
539
+ ROLE_SYSTEM_PROTECTED: "ROLE_SYSTEM_PROTECTED",
540
+ ROLE_HAS_USERS: "ROLE_HAS_USERS",
541
+ ROLE_DEFAULT_NOT_FOUND: "ROLE_DEFAULT_NOT_FOUND",
542
+ // Permission
543
+ PERMISSION_DENIED: "PERMISSION_DENIED",
544
+ // Validation
545
+ VALIDATION_ERROR: "VALIDATION_ERROR",
546
+ VALIDATION_FIELD_REQUIRED: "VALIDATION_FIELD_REQUIRED",
547
+ VALIDATION_FIELD_INVALID: "VALIDATION_FIELD_INVALID",
548
+ VALIDATION_JSON_MALFORMED: "VALIDATION_JSON_MALFORMED",
549
+ // Storage
550
+ STORAGE_FILE_NOT_FOUND: "STORAGE_FILE_NOT_FOUND",
551
+ STORAGE_FILE_TOO_LARGE: "STORAGE_FILE_TOO_LARGE",
552
+ STORAGE_FILE_TYPE_NOT_ALLOWED: "STORAGE_FILE_TYPE_NOT_ALLOWED",
553
+ STORAGE_PAYLOAD_TOO_LARGE: "STORAGE_PAYLOAD_TOO_LARGE",
554
+ // Resource (generic)
555
+ RESOURCE_NOT_FOUND: "RESOURCE_NOT_FOUND",
556
+ RESOURCE_CONFLICT: "RESOURCE_CONFLICT",
557
+ RESOURCE_CREATE_NOT_SUPPORTED: "RESOURCE_CREATE_NOT_SUPPORTED",
558
+ RESOURCE_UPDATE_NOT_SUPPORTED: "RESOURCE_UPDATE_NOT_SUPPORTED",
559
+ RESOURCE_DELETE_NOT_SUPPORTED: "RESOURCE_DELETE_NOT_SUPPORTED",
560
+ // Module
561
+ MODULE_NOT_FOUND: "MODULE_NOT_FOUND",
562
+ // HTTP standard
563
+ NOT_FOUND: "NOT_FOUND",
564
+ AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
565
+ // Database
566
+ DB_CONSTRAINT_UNIQUE: "DB_CONSTRAINT_UNIQUE",
567
+ DB_CONSTRAINT_FK: "DB_CONSTRAINT_FK",
568
+ DB_CONNECTION_ERROR: "DB_CONNECTION_ERROR",
569
+ DATABASE_NOT_READY: "DATABASE_NOT_READY",
570
+ // System
571
+ SYSTEM_INTERNAL_ERROR: "SYSTEM_INTERNAL_ERROR"
572
+ };
573
+
574
+ // src/testing/test-helpers.ts
575
+ var AppError = class extends Error {
576
+ constructor(message, statusCode = 500) {
577
+ super(message);
578
+ this.statusCode = statusCode;
579
+ this.name = "AppError";
580
+ }
581
+ };
582
+ var NotFoundError = class extends Error {
583
+ constructor(resource = "Resource") {
584
+ super(`${resource} not found`);
585
+ this.name = "NotFoundError";
586
+ }
587
+ };
588
+ var UnauthorizedError = class extends Error {
589
+ constructor(message = "Unauthorized") {
590
+ super(message);
591
+ this.name = "UnauthorizedError";
592
+ }
593
+ };
594
+ var ForbiddenError = class extends Error {
595
+ constructor(message = "Forbidden") {
596
+ super(message);
597
+ this.name = "ForbiddenError";
598
+ }
599
+ };
600
+ var ConflictError = class extends Error {
601
+ constructor(message = "Conflict") {
602
+ super(message);
603
+ this.name = "ConflictError";
604
+ }
605
+ };
606
+ var ValidationError = class extends Error {
607
+ details;
608
+ constructor(message = "Validation failed", details = []) {
609
+ super(message);
610
+ this.name = "ValidationError";
611
+ this.details = details;
612
+ }
613
+ };
614
+ function createMockQueryBuilder(data = []) {
615
+ const mockQb = {
616
+ _data: [...data],
617
+ _whereClauses: [],
618
+ _whereInClauses: [],
619
+ _whereNotInClauses: [],
620
+ _whereNotClauses: [],
621
+ _whereNullClauses: [],
622
+ _whereNotNullClauses: [],
623
+ _orderBy: null,
624
+ _limit: null,
625
+ _offset: null,
626
+ _insertData: null,
627
+ _updateData: null,
628
+ _deleted: false,
629
+ where(column, operatorOrValue, value) {
630
+ if (value !== void 0) {
631
+ mockQb._whereClauses = [...mockQb._whereClauses, { column, operator: operatorOrValue, value }];
632
+ } else {
633
+ mockQb._whereClauses = [...mockQb._whereClauses, { column, value: operatorOrValue }];
634
+ }
635
+ return mockQb;
636
+ },
637
+ whereNot(column, value) {
638
+ mockQb._whereNotClauses = [...mockQb._whereNotClauses, { column, value }];
639
+ return mockQb;
640
+ },
641
+ whereIn(column, values) {
642
+ mockQb._whereInClauses = [...mockQb._whereInClauses, { column, values }];
643
+ return mockQb;
644
+ },
645
+ whereNotIn(column, values) {
646
+ mockQb._whereNotInClauses = [...mockQb._whereNotInClauses, { column, values }];
647
+ return mockQb;
648
+ },
649
+ whereNull(column) {
650
+ mockQb._whereNullClauses = [...mockQb._whereNullClauses, column];
651
+ return mockQb;
652
+ },
653
+ whereNotNull(column) {
654
+ mockQb._whereNotNullClauses = [...mockQb._whereNotNullClauses, column];
655
+ return mockQb;
656
+ },
657
+ whereBetween() {
658
+ return mockQb;
659
+ },
660
+ orderBy(column, direction = "asc") {
661
+ mockQb._orderBy = { column, direction };
662
+ return mockQb;
663
+ },
664
+ limit(n) {
665
+ mockQb._limit = n;
666
+ return mockQb;
667
+ },
668
+ offset(n) {
669
+ mockQb._offset = n;
670
+ return mockQb;
671
+ },
672
+ select() {
673
+ return mockQb;
674
+ },
675
+ join() {
676
+ return mockQb;
677
+ },
678
+ leftJoin() {
679
+ return mockQb;
680
+ },
681
+ groupBy() {
682
+ return mockQb;
683
+ },
684
+ andWhere() {
685
+ return mockQb;
686
+ },
687
+ orWhere() {
688
+ return mockQb;
689
+ },
690
+ returning() {
691
+ return mockQb;
692
+ },
693
+ clone() {
694
+ return createMockQueryBuilder(mockQb._data);
695
+ },
696
+ count() {
697
+ return {
698
+ first: vi.fn().mockResolvedValue({ count: mockQb._data.length })
699
+ };
700
+ },
701
+ async first() {
702
+ let result = applyFilters(mockQb._data, mockQb);
703
+ if (mockQb._orderBy) {
704
+ const { column, direction } = mockQb._orderBy;
705
+ result = [...result].sort((a, b) => {
706
+ const aVal = String(a[column] ?? "");
707
+ const bVal = String(b[column] ?? "");
708
+ return direction === "asc" ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
709
+ });
710
+ }
711
+ return result[0] ?? void 0;
712
+ },
713
+ async insert(data2) {
714
+ mockQb._insertData = data2;
715
+ return [1];
716
+ },
717
+ async update(data2) {
718
+ mockQb._updateData = data2;
719
+ return 1;
720
+ },
721
+ async delete() {
722
+ mockQb._deleted = true;
723
+ return 1;
724
+ },
725
+ then(resolve) {
726
+ const filtered = applyFilters(mockQb._data, mockQb);
727
+ let result = filtered;
728
+ if (mockQb._orderBy) {
729
+ const { column, direction } = mockQb._orderBy;
730
+ result = [...result].sort((a, b) => {
731
+ const aVal = String(a[column] ?? "");
732
+ const bVal = String(b[column] ?? "");
733
+ return direction === "asc" ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
734
+ });
735
+ }
736
+ if (mockQb._offset !== null) {
737
+ result = result.slice(mockQb._offset);
738
+ }
739
+ if (mockQb._limit !== null) {
740
+ result = result.slice(0, mockQb._limit);
741
+ }
742
+ resolve(result);
743
+ }
744
+ };
745
+ return mockQb;
746
+ }
747
+ function applyFilters(data, qb) {
748
+ let result = [...data];
749
+ for (const clause of qb._whereClauses) {
750
+ if (clause.operator) {
751
+ result = result.filter((item) => {
752
+ const itemVal = item[clause.column];
753
+ switch (clause.operator) {
754
+ case ">":
755
+ return itemVal > clause.value;
756
+ case ">=":
757
+ return itemVal >= clause.value;
758
+ case "<":
759
+ return itemVal < clause.value;
760
+ case "<=":
761
+ return itemVal <= clause.value;
762
+ case "like": {
763
+ const pattern = String(clause.value);
764
+ const str = String(itemVal ?? "");
765
+ if (pattern.startsWith("%") && pattern.endsWith("%"))
766
+ return str.toLowerCase().includes(pattern.slice(1, -1).toLowerCase());
767
+ if (pattern.startsWith("%"))
768
+ return str.toLowerCase().endsWith(pattern.slice(1).toLowerCase());
769
+ if (pattern.endsWith("%"))
770
+ return str.toLowerCase().startsWith(pattern.slice(0, -1).toLowerCase());
771
+ return str === pattern;
772
+ }
773
+ default:
774
+ return itemVal === clause.value;
775
+ }
776
+ });
777
+ } else {
778
+ result = result.filter((item) => item[clause.column] === clause.value);
779
+ }
780
+ }
781
+ for (const clause of qb._whereNotClauses) {
782
+ result = result.filter((item) => item[clause.column] !== clause.value);
783
+ }
784
+ for (const clause of qb._whereInClauses) {
785
+ result = result.filter((item) => clause.values.includes(item[clause.column]));
786
+ }
787
+ for (const clause of qb._whereNotInClauses) {
788
+ result = result.filter((item) => !clause.values.includes(item[clause.column]));
789
+ }
790
+ for (const column of qb._whereNullClauses) {
791
+ result = result.filter((item) => item[column] === null || item[column] === void 0);
792
+ }
793
+ for (const column of qb._whereNotNullClauses) {
794
+ result = result.filter((item) => item[column] !== null && item[column] !== void 0);
795
+ }
796
+ return result;
797
+ }
798
+ function createMockDb(tableData = {}) {
799
+ const mockDb = vi.fn((table) => {
800
+ return createMockQueryBuilder(tableData[table] ?? []);
801
+ });
802
+ mockDb["schema"] = {
803
+ hasTable: vi.fn().mockResolvedValue(false),
804
+ createTable: vi.fn().mockResolvedValue(void 0)
805
+ };
806
+ mockDb["raw"] = vi.fn().mockReturnValue({
807
+ then: (resolve) => resolve([])
808
+ });
809
+ mockDb["transaction"] = vi.fn().mockImplementation(async (callback) => {
810
+ const trxMock = vi.fn((table) => {
811
+ return createMockQueryBuilder(tableData[table] ?? []);
812
+ });
813
+ await callback(trxMock);
814
+ });
815
+ return mockDb;
816
+ }
817
+ function createMockDbContext(tableData = {}) {
818
+ const mockKnex = createMockDb(tableData);
819
+ return {
820
+ knex: mockKnex,
821
+ adapter: {
822
+ findMany: vi.fn().mockResolvedValue({ items: [], total: 0, page: 1, limit: 20, totalPages: 0, hasNext: false }),
823
+ findOne: vi.fn().mockResolvedValue(null),
824
+ findById: vi.fn().mockResolvedValue(null),
825
+ count: vi.fn().mockResolvedValue(0),
826
+ insert: vi.fn().mockResolvedValue({}),
827
+ update: vi.fn().mockResolvedValue({}),
828
+ delete: vi.fn().mockResolvedValue(true),
829
+ transaction: vi.fn().mockImplementation(async (fn) => fn({}))
830
+ },
831
+ schema: {
832
+ hasTable: vi.fn().mockResolvedValue(false),
833
+ hasColumn: vi.fn().mockResolvedValue(false),
834
+ getColumns: vi.fn().mockResolvedValue([]),
835
+ createTable: vi.fn().mockResolvedValue(void 0),
836
+ alterTable: vi.fn().mockResolvedValue(void 0),
837
+ dropTable: vi.fn().mockResolvedValue(void 0),
838
+ addTimestamps: vi.fn().mockResolvedValue(void 0),
839
+ addAuditFields: vi.fn().mockResolvedValue(void 0),
840
+ addColumnIfMissing: vi.fn().mockResolvedValue(false),
841
+ raw: {}
842
+ },
843
+ t: (name) => name,
844
+ addTimestamps: vi.fn(),
845
+ addAuditFieldsIfMissing: vi.fn(),
846
+ addSoftDeleteFieldIfMissing: vi.fn(),
847
+ addConfigDefaultField: vi.fn(),
848
+ addColumnIfMissing: vi.fn(),
849
+ nowTimestamp: () => (/* @__PURE__ */ new Date()).toISOString(),
850
+ formatTimestamp: (_db, date) => (date ?? /* @__PURE__ */ new Date()).toISOString(),
851
+ applySearchFilter,
852
+ getPagination,
853
+ buildPaginatedResult,
854
+ getSearchableFields
855
+ };
856
+ }
857
+ function createMockContext(overrides = {}) {
858
+ let idCounter = 0;
859
+ const { pluginCode, ...rest } = overrides;
860
+ const t = pluginCode ? (name) => `${pluginCode}_${name}` : (name) => name;
861
+ const services = {};
862
+ const tempAdapter = createInMemoryAdapter();
863
+ const adapters = {
864
+ temp: { data: tempAdapter }
865
+ };
866
+ let processedOverrides = { ...rest };
867
+ if (rest.db && typeof rest.db === "function") {
868
+ const knexMock = rest.db;
869
+ processedOverrides = {
870
+ ...rest,
871
+ db: {
872
+ knex: knexMock,
873
+ adapter: {
874
+ findMany: vi.fn().mockResolvedValue({ items: [], total: 0, page: 1, limit: 20, totalPages: 0, hasNext: false }),
875
+ findOne: vi.fn().mockResolvedValue(null),
876
+ findById: vi.fn().mockResolvedValue(null),
877
+ count: vi.fn().mockResolvedValue(0),
878
+ insert: vi.fn().mockResolvedValue({}),
879
+ update: vi.fn().mockResolvedValue({}),
880
+ delete: vi.fn().mockResolvedValue(true),
881
+ transaction: vi.fn().mockImplementation(async (fn) => fn({}))
882
+ },
883
+ schema: {
884
+ hasTable: vi.fn().mockResolvedValue(false),
885
+ hasColumn: vi.fn().mockResolvedValue(false),
886
+ getColumns: vi.fn().mockResolvedValue([]),
887
+ createTable: vi.fn().mockResolvedValue(void 0),
888
+ alterTable: vi.fn().mockResolvedValue(void 0),
889
+ dropTable: vi.fn().mockResolvedValue(void 0),
890
+ addTimestamps: vi.fn().mockResolvedValue(void 0),
891
+ addAuditFields: vi.fn().mockResolvedValue(void 0),
892
+ addColumnIfMissing: vi.fn().mockResolvedValue(false),
893
+ raw: {}
894
+ },
895
+ t,
896
+ addTimestamps: vi.fn(),
897
+ addAuditFieldsIfMissing: vi.fn(),
898
+ addSoftDeleteFieldIfMissing: vi.fn(),
899
+ addConfigDefaultField: vi.fn(),
900
+ addColumnIfMissing: vi.fn(),
901
+ nowTimestamp: () => (/* @__PURE__ */ new Date()).toISOString(),
902
+ formatTimestamp: (_db, date) => (date ?? /* @__PURE__ */ new Date()).toISOString(),
903
+ applySearchFilter,
904
+ getPagination,
905
+ buildPaginatedResult,
906
+ getSearchableFields,
907
+ getKnex: () => knexMock,
908
+ // Legacy: ctx.db.raw → now ctx.db.knex.raw
909
+ raw: knexMock.raw
910
+ }
911
+ };
912
+ }
913
+ const mockKnex = createMockDb();
914
+ const mockLogger = {
915
+ info: vi.fn(),
916
+ warn: vi.fn(),
917
+ error: vi.fn(),
918
+ debug: vi.fn(),
919
+ trace: vi.fn(),
920
+ fatal: vi.fn(),
921
+ child: vi.fn().mockReturnThis()
922
+ };
923
+ const mockSchemaAdapter = {
924
+ hasTable: vi.fn().mockResolvedValue(false),
925
+ hasColumn: vi.fn().mockResolvedValue(false),
926
+ getColumns: vi.fn().mockResolvedValue([]),
927
+ createTable: vi.fn().mockResolvedValue(void 0),
928
+ alterTable: vi.fn().mockResolvedValue(void 0),
929
+ dropTable: vi.fn().mockResolvedValue(void 0),
930
+ addTimestamps: vi.fn().mockResolvedValue(void 0),
931
+ addAuditFields: vi.fn().mockResolvedValue(void 0),
932
+ addColumnIfMissing: vi.fn().mockResolvedValue(false),
933
+ raw: {}
934
+ };
935
+ const mockDbAdapter = {
936
+ findMany: vi.fn().mockResolvedValue({ items: [], total: 0, page: 1, limit: 20, totalPages: 0, hasNext: false }),
937
+ findOne: vi.fn().mockResolvedValue(null),
938
+ findById: vi.fn().mockResolvedValue(null),
939
+ count: vi.fn().mockResolvedValue(0),
940
+ insert: vi.fn().mockResolvedValue({}),
941
+ update: vi.fn().mockResolvedValue({}),
942
+ delete: vi.fn().mockResolvedValue(true),
943
+ transaction: vi.fn().mockImplementation(async (fn) => fn({}))
944
+ };
945
+ return {
946
+ // =========================================================================
947
+ // Core Context: logger, errors, crypto, socket, events, middleware
948
+ // =========================================================================
949
+ core: {
950
+ logger: mockLogger,
951
+ errors: {
952
+ AppError,
953
+ NotFoundError,
954
+ UnauthorizedError,
955
+ ForbiddenError,
956
+ ConflictError,
957
+ ValidationError,
958
+ codes: ErrorCodes
959
+ },
960
+ crypto: {
961
+ hashPassword: vi.fn().mockResolvedValue("$2b$12$mockhash"),
962
+ verifyPassword: vi.fn().mockResolvedValue(true),
963
+ DUMMY_HASH: "$2b$12$dummyhashfordummypasswordcheck"
964
+ },
965
+ socket: {
966
+ getIO: vi.fn().mockReturnValue({}),
967
+ isInitialized: vi.fn().mockReturnValue(false),
968
+ isUserConnected: vi.fn().mockReturnValue(false),
969
+ getUserSocketCount: vi.fn().mockReturnValue(0),
970
+ getConnectedUsers: vi.fn().mockReturnValue([])
971
+ },
972
+ sse: {
973
+ sendEvent: vi.fn(),
974
+ close: vi.fn()
975
+ },
976
+ abilities: {},
977
+ events: {
978
+ emit: vi.fn(),
979
+ emitEvent: vi.fn(),
980
+ emitAsync: vi.fn().mockResolvedValue(void 0),
981
+ on: vi.fn(),
982
+ off: vi.fn()
983
+ },
984
+ middleware: {},
985
+ createRouter: vi.fn(),
986
+ generateId: () => `test-id-${++idCounter}`,
987
+ generateIdByType: (type) => {
988
+ if (type === "auto" || type === "custom" || type === "pattern") return void 0;
989
+ return `test-id-${++idCounter}`;
990
+ },
991
+ getLibPath: () => "/tmp",
992
+ getProjectPath: () => "/tmp",
993
+ cache: (() => {
994
+ const caches = /* @__PURE__ */ new Map();
995
+ return {
996
+ create: vi.fn().mockImplementation((name, opts) => {
997
+ const cache = new ManagedCacheImpl(name, opts);
998
+ caches.set(name, cache);
999
+ return cache;
1000
+ }),
1001
+ getOrCreate: vi.fn().mockImplementation((name, opts) => {
1002
+ if (caches.has(name)) return caches.get(name);
1003
+ const cache = new ManagedCacheImpl(name, opts);
1004
+ caches.set(name, cache);
1005
+ return cache;
1006
+ }),
1007
+ get: vi.fn().mockImplementation((name) => caches.get(name)),
1008
+ clearAll: vi.fn(),
1009
+ stats: vi.fn().mockReturnValue({}),
1010
+ destroy: vi.fn()
1011
+ };
1012
+ })(),
1013
+ safeJsonParse: (jsonString, fallback) => {
1014
+ try {
1015
+ return JSON.parse(jsonString);
1016
+ } catch {
1017
+ return fallback;
1018
+ }
1019
+ }
1020
+ },
1021
+ // =========================================================================
1022
+ // Database Context: knex, adapter, schema, helper functions
1023
+ // =========================================================================
1024
+ db: {
1025
+ knex: mockKnex,
1026
+ adapter: mockDbAdapter,
1027
+ schema: mockSchemaAdapter,
1028
+ t,
1029
+ addTimestamps: vi.fn(),
1030
+ addAuditFieldsIfMissing: vi.fn(),
1031
+ addSoftDeleteFieldIfMissing: vi.fn(),
1032
+ addConfigDefaultField: vi.fn(),
1033
+ addColumnIfMissing: vi.fn(),
1034
+ nowTimestamp: () => (/* @__PURE__ */ new Date()).toISOString(),
1035
+ formatTimestamp: (_db, date) => (date ?? /* @__PURE__ */ new Date()).toISOString(),
1036
+ applySearchFilter,
1037
+ getPagination,
1038
+ buildPaginatedResult,
1039
+ getSearchableFields,
1040
+ getKnex: () => mockKnex,
1041
+ // Legacy: ctx.db.raw → now ctx.db.knex.raw
1042
+ raw: mockKnex.raw
1043
+ },
1044
+ // =========================================================================
1045
+ // Runtime Context: entity service/controller/router factories
1046
+ // =========================================================================
1047
+ runtime: {
1048
+ createEntityService: vi.fn(),
1049
+ createEntityController: vi.fn(),
1050
+ createEntityRouter: vi.fn()
1051
+ },
1052
+ // =========================================================================
1053
+ // Config Context: env vars and resolved NexusConfig
1054
+ // =========================================================================
1055
+ config: {},
1056
+ // =========================================================================
1057
+ // Engine Context: module/plugin introspection
1058
+ // =========================================================================
1059
+ engine: {
1060
+ getModules: vi.fn().mockReturnValue([]),
1061
+ getPlugins: vi.fn().mockReturnValue([]),
1062
+ getModuleSubjects: vi.fn().mockReturnValue([]),
1063
+ getRegisteredSubjects: vi.fn().mockReturnValue([])
1064
+ },
1065
+ // =========================================================================
1066
+ // Services Context: inter-module service registry
1067
+ // =========================================================================
1068
+ services: {
1069
+ register(name, service) {
1070
+ services[name] = service;
1071
+ },
1072
+ get(serviceName) {
1073
+ const service = services[serviceName];
1074
+ if (!service) {
1075
+ throw new Error(`Service "${serviceName}" not initialized.`);
1076
+ }
1077
+ return service;
1078
+ },
1079
+ getOptional(serviceName) {
1080
+ return services[serviceName];
1081
+ },
1082
+ has(serviceName) {
1083
+ return !!services[serviceName];
1084
+ }
1085
+ },
1086
+ // =========================================================================
1087
+ // Adapters Context: external data source adapters
1088
+ // =========================================================================
1089
+ adapters: {
1090
+ register(name, dataAdapter, schemaAdapter) {
1091
+ adapters[name] = { data: dataAdapter, schema: schemaAdapter };
1092
+ },
1093
+ get(name) {
1094
+ const pair = adapters[name];
1095
+ if (!pair) {
1096
+ throw new Error(`Adapter "${name}" not found`);
1097
+ }
1098
+ return pair.data;
1099
+ },
1100
+ getSchema(name) {
1101
+ const pair = adapters[name];
1102
+ return pair?.schema;
1103
+ },
1104
+ has(name) {
1105
+ return name in adapters;
1106
+ }
1107
+ },
1108
+ // =========================================================================
1109
+ // Root-level shortcut
1110
+ // =========================================================================
1111
+ createRouter: vi.fn(),
1112
+ // Semantic events API (NEX-161)
1113
+ events: {
1114
+ notify: vi.fn(),
1115
+ query: vi.fn().mockResolvedValue([]),
1116
+ command: vi.fn().mockResolvedValue(void 0),
1117
+ on: vi.fn(),
1118
+ off: vi.fn(),
1119
+ once: vi.fn(),
1120
+ onAny: vi.fn()
1121
+ },
1122
+ // =========================================================================
1123
+ // Legacy API compatibility - deprecated, use new API instead
1124
+ // =========================================================================
1125
+ // Legacy: ctx.logger → now ctx.core.logger
1126
+ logger: mockLogger,
1127
+ // Legacy: ctx.errors → now ctx.core.errors
1128
+ errors: {
1129
+ AppError,
1130
+ NotFoundError,
1131
+ UnauthorizedError,
1132
+ ForbiddenError,
1133
+ ConflictError,
1134
+ ValidationError,
1135
+ codes: ErrorCodes
1136
+ },
1137
+ // Legacy: ctx.registerService → now ctx.services.register
1138
+ registerService(name, service) {
1139
+ services[name] = service;
1140
+ },
1141
+ getService(serviceName) {
1142
+ const service = services[serviceName];
1143
+ if (!service) {
1144
+ throw new Error(`Service "${serviceName}" not initialized.`);
1145
+ }
1146
+ return service;
1147
+ },
1148
+ getOptionalService(serviceName) {
1149
+ return services[serviceName];
1150
+ },
1151
+ hasService(serviceName) {
1152
+ return !!services[serviceName];
1153
+ },
1154
+ registerAdapter(name, dataAdapter, schemaAdapter) {
1155
+ adapters[name] = { data: dataAdapter, schema: schemaAdapter };
1156
+ },
1157
+ getAdapter(name) {
1158
+ const pair = adapters[name];
1159
+ if (!pair) {
1160
+ throw new Error(`Adapter "${name}" not found`);
1161
+ }
1162
+ return pair.data;
1163
+ },
1164
+ hasAdapter(name) {
1165
+ return name in adapters;
1166
+ },
1167
+ ...processedOverrides
1168
+ };
1169
+ }
1170
+ function createCollectionDefinition(overrides = {}) {
1171
+ return {
1172
+ type: "collection",
1173
+ table: "test_items",
1174
+ label: "Test Items",
1175
+ labelField: "name",
1176
+ timestamps: true,
1177
+ fields: {
1178
+ id: {
1179
+ label: "ID",
1180
+ input: "text",
1181
+ hidden: true,
1182
+ db: { type: "string", size: 26, nullable: false }
1183
+ },
1184
+ name: {
1185
+ label: "Name",
1186
+ input: "text",
1187
+ db: { type: "string", size: 100, nullable: false }
1188
+ }
1189
+ },
1190
+ ...overrides
1191
+ };
1192
+ }
1193
+ function createSingleDefinition(overrides = {}) {
1194
+ return {
1195
+ type: "single",
1196
+ key: "test_settings",
1197
+ label: "Test Settings",
1198
+ defaults: { theme: "light", language: "en" },
1199
+ fields: {
1200
+ theme: {
1201
+ label: "Theme",
1202
+ input: "select",
1203
+ db: { type: "string", size: 20, nullable: false }
1204
+ },
1205
+ language: {
1206
+ label: "Language",
1207
+ input: "select",
1208
+ db: { type: "string", size: 10, nullable: false }
1209
+ }
1210
+ },
1211
+ ...overrides
1212
+ };
1213
+ }
1214
+ function createReferenceDefinition(overrides = {}) {
1215
+ return {
1216
+ type: "collection",
1217
+ table: "ref_countries",
1218
+ label: "Countries",
1219
+ labelField: "name",
1220
+ seed: [
1221
+ { id: "ES", code: "ES", name: "Spain" },
1222
+ { id: "US", code: "US", name: "United States" },
1223
+ { id: "MX", code: "MX", name: "Mexico" }
1224
+ ],
1225
+ fields: {
1226
+ id: {
1227
+ label: "ID",
1228
+ input: "text",
1229
+ hidden: true,
1230
+ db: { type: "string", size: 10, nullable: false }
1231
+ },
1232
+ code: {
1233
+ label: "Code",
1234
+ input: "text",
1235
+ db: { type: "string", size: 10, nullable: false }
1236
+ },
1237
+ name: {
1238
+ label: "Name",
1239
+ input: "text",
1240
+ db: { type: "string", size: 100, nullable: false }
1241
+ }
1242
+ },
1243
+ ...overrides
1244
+ };
1245
+ }
1246
+ function createEventDefinition(overrides = {}) {
1247
+ return {
1248
+ type: "event",
1249
+ table: "evt_audit_logs",
1250
+ label: "Audit Logs",
1251
+ labelField: "action",
1252
+ immutable: true,
1253
+ defaultSort: { field: "created_at", order: "desc" },
1254
+ retention: { days: 90, maxRows: 1e4 },
1255
+ fields: {
1256
+ id: {
1257
+ label: "ID",
1258
+ input: "text",
1259
+ hidden: true,
1260
+ db: { type: "string", size: 26, nullable: false }
1261
+ },
1262
+ action: {
1263
+ label: "Action",
1264
+ input: "text",
1265
+ db: { type: "string", size: 50, nullable: false }
1266
+ },
1267
+ user_id: {
1268
+ label: "User",
1269
+ input: "text",
1270
+ db: { type: "string", size: 26, nullable: true }
1271
+ },
1272
+ created_at: {
1273
+ label: "Created At",
1274
+ input: "datetime",
1275
+ db: { type: "datetime", nullable: false }
1276
+ }
1277
+ },
1278
+ ...overrides
1279
+ };
1280
+ }
1281
+ function createConfigDefinition(overrides = {}) {
1282
+ return {
1283
+ type: "config",
1284
+ key: "cfg_module_settings",
1285
+ label: "Module Settings",
1286
+ scopeField: "module_name",
1287
+ defaults: { enabled: true, maxItems: 100 },
1288
+ fields: {
1289
+ module_name: {
1290
+ label: "Module",
1291
+ input: "text",
1292
+ db: { type: "string", size: 50, nullable: false }
1293
+ },
1294
+ enabled: {
1295
+ label: "Enabled",
1296
+ input: "checkbox",
1297
+ db: { type: "boolean", nullable: false }
1298
+ },
1299
+ maxItems: {
1300
+ label: "Max Items",
1301
+ input: "number",
1302
+ db: { type: "integer", nullable: false }
1303
+ }
1304
+ },
1305
+ ...overrides
1306
+ };
1307
+ }
1308
+ function createTempDefinition(overrides = {}) {
1309
+ return {
1310
+ type: "temp",
1311
+ table: "tmp_sessions",
1312
+ label: "Sessions",
1313
+ labelField: "token",
1314
+ retention: { seconds: 3600, expiresField: "expires_at" },
1315
+ // 1 hour
1316
+ fields: {
1317
+ id: {
1318
+ label: "ID",
1319
+ input: "text",
1320
+ hidden: true,
1321
+ db: { type: "string", size: 26, nullable: false }
1322
+ },
1323
+ token: {
1324
+ label: "Token",
1325
+ input: "text",
1326
+ db: { type: "string", size: 256, nullable: false }
1327
+ },
1328
+ expires_at: {
1329
+ label: "Expires At",
1330
+ input: "datetime",
1331
+ db: { type: "datetime", nullable: false }
1332
+ }
1333
+ },
1334
+ ...overrides
1335
+ };
1336
+ }
1337
+ function createViewDefinition(overrides = {}) {
1338
+ return {
1339
+ type: "view",
1340
+ table: "vw_user_stats",
1341
+ label: "User Stats",
1342
+ labelField: "user_id",
1343
+ query: "SELECT user_id, COUNT(*) as count FROM posts GROUP BY user_id",
1344
+ fields: {
1345
+ user_id: {
1346
+ label: "User",
1347
+ input: "text",
1348
+ db: { type: "string", size: 26, nullable: false }
1349
+ },
1350
+ count: {
1351
+ label: "Count",
1352
+ input: "number",
1353
+ db: { type: "integer", nullable: false }
1354
+ }
1355
+ },
1356
+ ...overrides
1357
+ };
1358
+ }
1359
+ function createExternalDefinition(overrides = {}) {
1360
+ return {
1361
+ type: "external",
1362
+ adapter: "test-api",
1363
+ table: "external_items",
1364
+ // Resource ID in external system
1365
+ label: "External Items",
1366
+ labelField: "title",
1367
+ cache: { ttl: 300 },
1368
+ fields: {
1369
+ id: {
1370
+ label: "ID",
1371
+ input: "text",
1372
+ hidden: true,
1373
+ db: { type: "string", size: 26, nullable: false }
1374
+ },
1375
+ title: {
1376
+ label: "Title",
1377
+ input: "text",
1378
+ db: { type: "string", size: 200, nullable: false }
1379
+ }
1380
+ },
1381
+ ...overrides
1382
+ };
1383
+ }
1384
+ function createVirtualDefinition(overrides = {}) {
1385
+ return createComputedDefinition({
1386
+ sources: ["source_a", "source_b"],
1387
+ resolver: (sources) => {
1388
+ const combined = [
1389
+ ...sources["source_a"] ?? [],
1390
+ ...sources["source_b"] ?? []
1391
+ ];
1392
+ return combined;
1393
+ },
1394
+ compute: void 0,
1395
+ ...overrides
1396
+ });
1397
+ }
1398
+ function createComputedDefinition(overrides = {}) {
1399
+ return {
1400
+ type: "computed",
1401
+ label: "Computed Stats",
1402
+ compute: vi.fn().mockResolvedValue([
1403
+ { id: "1", metric: "users", value: 100 },
1404
+ { id: "2", metric: "posts", value: 500 }
1405
+ ]),
1406
+ cache: { ttl: 60 },
1407
+ fields: {
1408
+ id: {
1409
+ label: "ID",
1410
+ input: "text",
1411
+ hidden: true,
1412
+ db: { type: "string", size: 26, nullable: false }
1413
+ },
1414
+ metric: {
1415
+ label: "Metric",
1416
+ input: "text",
1417
+ db: { type: "string", size: 50, nullable: false }
1418
+ },
1419
+ value: {
1420
+ label: "Value",
1421
+ input: "number",
1422
+ db: { type: "integer", nullable: false }
1423
+ }
1424
+ },
1425
+ ...overrides
1426
+ };
1427
+ }
1428
+ function createMockDatabaseAdapter(overrides = {}) {
1429
+ return {
1430
+ findMany: vi.fn().mockResolvedValue({
1431
+ items: [
1432
+ { id: "ext-1", title: "External Item 1" },
1433
+ { id: "ext-2", title: "External Item 2" }
1434
+ ],
1435
+ total: 2,
1436
+ page: 1,
1437
+ limit: 20,
1438
+ totalPages: 1,
1439
+ hasNext: false
1440
+ }),
1441
+ findOne: vi.fn().mockResolvedValue(null),
1442
+ findById: vi.fn().mockResolvedValue({ id: "ext-1", title: "External Item 1" }),
1443
+ count: vi.fn().mockResolvedValue(2),
1444
+ insert: vi.fn().mockResolvedValue({ id: "ext-new", title: "Created" }),
1445
+ update: vi.fn().mockResolvedValue({ id: "ext-1", title: "Updated" }),
1446
+ delete: vi.fn().mockResolvedValue(true),
1447
+ transaction: vi.fn().mockImplementation(async (fn) => fn({})),
1448
+ raw: vi.fn().mockResolvedValue([]),
1449
+ ...overrides
1450
+ };
1451
+ }
1452
+ function createTreeDefinition(overrides = {}) {
1453
+ return {
1454
+ type: "tree",
1455
+ table: "tree_categories",
1456
+ label: "Categories",
1457
+ labelField: "name",
1458
+ timestamps: true,
1459
+ seed: [
1460
+ { id: "root", name: "Root Category", parent_id: null }
1461
+ ],
1462
+ fields: {
1463
+ id: {
1464
+ label: "ID",
1465
+ input: "text",
1466
+ hidden: true,
1467
+ db: { type: "string", size: 26, nullable: false }
1468
+ },
1469
+ name: {
1470
+ label: "Name",
1471
+ input: "text",
1472
+ db: { type: "string", size: 100, nullable: false }
1473
+ }
1474
+ // parent_id is auto-injected by TreeService
1475
+ },
1476
+ ...overrides
1477
+ };
1478
+ }
1479
+ function createDagDefinition(overrides = {}) {
1480
+ return {
1481
+ type: "dag",
1482
+ table: "dag_tags",
1483
+ label: "Tags",
1484
+ labelField: "name",
1485
+ timestamps: true,
1486
+ seed: [
1487
+ { id: "root", name: "Root Tag" }
1488
+ ],
1489
+ fields: {
1490
+ id: {
1491
+ label: "ID",
1492
+ input: "text",
1493
+ hidden: true,
1494
+ db: { type: "string", size: 26, nullable: false }
1495
+ },
1496
+ name: {
1497
+ label: "Name",
1498
+ input: "text",
1499
+ db: { type: "string", size: 100, nullable: false }
1500
+ }
1501
+ },
1502
+ ...overrides
1503
+ };
1504
+ }
1505
+ function createActionDefinition(overrides = {}) {
1506
+ return {
1507
+ key: "test_action",
1508
+ label: "Test Action",
1509
+ scope: "row",
1510
+ method: "POST",
1511
+ handler: async () => ({ success: true }),
1512
+ ...overrides
1513
+ };
1514
+ }
1515
+
1516
+ // src/testing/core-tables.ts
1517
+ async function createCoreTables(db) {
1518
+ if (!await db.schema.hasTable("users")) {
1519
+ await db.schema.createTable("users", (t) => {
1520
+ t.string("id", 26).primary();
1521
+ t.string("name", 100).notNullable();
1522
+ t.string("email", 255).notNullable();
1523
+ t.string("role", 20).notNullable().defaultTo("user");
1524
+ t.timestamps(true, true);
1525
+ });
1526
+ }
1527
+ if (!await db.schema.hasTable("roles")) {
1528
+ await db.schema.createTable("roles", (t) => {
1529
+ t.string("id", 26).primary();
1530
+ t.string("name", 50).notNullable();
1531
+ });
1532
+ }
1533
+ }
1534
+
1535
+ // src/core/errors/app-error.ts
1536
+ var AppError2 = class extends Error {
1537
+ statusCode;
1538
+ code;
1539
+ interpolation;
1540
+ details;
1541
+ constructor(params, statusCode = 400, details) {
1542
+ if (typeof params === "string") {
1543
+ super(params);
1544
+ this.code = ErrorCodes.SYSTEM_INTERNAL_ERROR;
1545
+ } else {
1546
+ super(params.message || params.code);
1547
+ this.code = params.code;
1548
+ this.interpolation = params.interpolation;
1549
+ }
1550
+ this.statusCode = statusCode;
1551
+ this.details = details;
1552
+ this.name = "AppError";
1553
+ Error.captureStackTrace(this, this.constructor);
1554
+ }
1555
+ };
1556
+ var NotFoundError2 = class extends AppError2 {
1557
+ constructor(resource = "Resource") {
1558
+ super({
1559
+ code: ErrorCodes.RESOURCE_NOT_FOUND,
1560
+ message: `${resource} not found`,
1561
+ interpolation: { resource }
1562
+ }, 404);
1563
+ this.name = "NotFoundError";
1564
+ }
1565
+ };
1566
+ var UnauthorizedError2 = class extends AppError2 {
1567
+ constructor(codeOrMessage = ErrorCodes.AUTH_TOKEN_REQUIRED, message) {
1568
+ const isCode = Object.values(ErrorCodes).includes(codeOrMessage);
1569
+ if (isCode) {
1570
+ super({
1571
+ code: codeOrMessage,
1572
+ message: message || "Unauthorized"
1573
+ }, 401);
1574
+ } else {
1575
+ super({
1576
+ code: ErrorCodes.AUTH_TOKEN_REQUIRED,
1577
+ message: codeOrMessage
1578
+ }, 401);
1579
+ }
1580
+ this.name = "UnauthorizedError";
1581
+ }
1582
+ };
1583
+ var ForbiddenError2 = class extends AppError2 {
1584
+ constructor(codeOrMessage = ErrorCodes.PERMISSION_DENIED, message) {
1585
+ const isCode = Object.values(ErrorCodes).includes(codeOrMessage);
1586
+ if (isCode) {
1587
+ super({
1588
+ code: codeOrMessage,
1589
+ message: message || "Access denied"
1590
+ }, 403);
1591
+ } else {
1592
+ super({
1593
+ code: ErrorCodes.PERMISSION_DENIED,
1594
+ message: codeOrMessage
1595
+ }, 403);
1596
+ }
1597
+ this.name = "ForbiddenError";
1598
+ }
1599
+ };
1600
+ var ConflictError2 = class extends AppError2 {
1601
+ constructor(codeOrMessage = ErrorCodes.RESOURCE_CONFLICT, message) {
1602
+ const isCode = Object.values(ErrorCodes).includes(codeOrMessage);
1603
+ if (isCode) {
1604
+ super({
1605
+ code: codeOrMessage,
1606
+ message: message || "Conflict"
1607
+ }, 409);
1608
+ } else {
1609
+ super({
1610
+ code: ErrorCodes.RESOURCE_CONFLICT,
1611
+ message: codeOrMessage
1612
+ }, 409);
1613
+ }
1614
+ this.name = "ConflictError";
1615
+ }
1616
+ };
1617
+ var ValidationError2 = class extends AppError2 {
1618
+ details;
1619
+ constructor(messageOrCode = ErrorCodes.VALIDATION_ERROR, details = []) {
1620
+ const isCode = Object.values(ErrorCodes).includes(messageOrCode);
1621
+ if (isCode) {
1622
+ super({
1623
+ code: messageOrCode,
1624
+ message: "Validation error"
1625
+ }, 400);
1626
+ } else {
1627
+ super({
1628
+ code: ErrorCodes.VALIDATION_ERROR,
1629
+ message: messageOrCode
1630
+ }, 400);
1631
+ }
1632
+ this.name = "ValidationError";
1633
+ this.details = details;
1634
+ }
1635
+ };
1636
+
1637
+ // src/testing/test-db.ts
1638
+ import knex from "knex";
1639
+ function createTestDb() {
1640
+ return knex({
1641
+ client: "better-sqlite3",
1642
+ connection: ":memory:",
1643
+ useNullAsDefault: true
1644
+ });
1645
+ }
1646
+ export {
1647
+ AppError2 as AppError,
1648
+ ConflictError2 as ConflictError,
1649
+ ErrorCodes,
1650
+ ForbiddenError2 as ForbiddenError,
1651
+ NotFoundError2 as NotFoundError,
1652
+ UnauthorizedError2 as UnauthorizedError,
1653
+ ValidationError2 as ValidationError,
1654
+ createActionDefinition,
1655
+ createCollectionDefinition,
1656
+ createComputedDefinition,
1657
+ createConfigDefinition,
1658
+ createCoreTables,
1659
+ createDagDefinition,
1660
+ createEventDefinition,
1661
+ createExternalDefinition,
1662
+ createMockContext,
1663
+ createMockDatabaseAdapter,
1664
+ createMockDb,
1665
+ createMockDbContext,
1666
+ createMockQueryBuilder,
1667
+ createReferenceDefinition,
1668
+ createSingleDefinition,
1669
+ createTempDefinition,
1670
+ createTestDb,
1671
+ createTreeDefinition,
1672
+ createViewDefinition,
1673
+ createVirtualDefinition
1674
+ };
1675
+ //# sourceMappingURL=index.js.map