@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.
- package/dist/app-error-CKbYJQ9V.d.ts +136 -0
- package/dist/cli.js +521 -282
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -135
- package/dist/index.js +632 -147
- package/dist/index.js.map +1 -1
- package/dist/main.js +632 -147
- package/dist/main.js.map +1 -1
- package/dist/migration-helpers/index.d.ts +63 -0
- package/dist/migration-helpers/index.js +12116 -0
- package/dist/migration-helpers/index.js.map +1 -0
- package/dist/testing/index.d.ts +81 -0
- package/dist/testing/index.js +1675 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +17 -3
|
@@ -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
|