@anteros/core 0.0.25 → 0.0.26
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/database/db.cache.ts +197 -0
- package/database/decorator.ts +32 -0
- package/database/mongodbadapter.ts +244 -32
- package/index.ts +2 -0
- package/package.json +1 -1
- package/server/api.ts +1 -1
- package/server/boot.ts +65 -2
- package/server/hono.ts +63 -0
- package/server/mcp.ts +34 -12
- package/server/routes.ts +10 -0
- package/tests/api.test.ts +217 -3
- package/tests/db-cache.test.ts +214 -0
- package/tests/logger.test.ts +123 -0
- package/types/collection.d.ts +6 -0
- package/types/config.d.ts +37 -0
- package/types/mongo.d.ts +10 -0
- package/types/rest.d.ts +37 -0
- package/utils/cache.ts +2 -2
- package/utils/func.ts +3 -0
- package/utils/logger.ts +212 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { ObjectId } from "mongodb";
|
|
3
|
+
import { useRest } from "../database/rest";
|
|
4
|
+
import { formatConfig, cfg } from "../server/config";
|
|
5
|
+
import { syncTenants } from "../database/tenant";
|
|
6
|
+
import { syncCollections } from "../database/collection";
|
|
7
|
+
|
|
8
|
+
const TEST_TENANT = "cache-test";
|
|
9
|
+
const TEST_DB = "mongodb://localhost:27017/_DB_CACHE_TEST";
|
|
10
|
+
|
|
11
|
+
let rest: InstanceType<typeof useRest>;
|
|
12
|
+
|
|
13
|
+
beforeAll(async () => {
|
|
14
|
+
formatConfig({
|
|
15
|
+
server: { port: 4000, cache: { driver: "memory", enabled: true } },
|
|
16
|
+
tenants: [
|
|
17
|
+
{ id: TEST_TENANT, dir: "src", database: { uri: TEST_DB } },
|
|
18
|
+
{ id: "cache-test-2", dir: "src", database: { uri: TEST_DB } },
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
await syncTenants();
|
|
22
|
+
await syncCollections();
|
|
23
|
+
(cfg as any).collections = [
|
|
24
|
+
{ _tenant_: TEST_TENANT, slug: "items", fields: [{ name: "title", type: "string" }, { name: "count", type: "number" }], api: { access: { "*": true } } },
|
|
25
|
+
{ _tenant_: TEST_TENANT, slug: "orders", fields: [{ name: "code", type: "string" }, { name: "author", type: "relationship", relation: { to: "clients" } }], api: { access: { "*": true } } },
|
|
26
|
+
{ _tenant_: TEST_TENANT, slug: "clients", fields: [{ name: "name", type: "string" }], api: { access: { "*": true } } },
|
|
27
|
+
];
|
|
28
|
+
rest = new useRest({ internal: false, tenant_id: TEST_TENANT });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterAll(async () => {
|
|
32
|
+
try { await rest.db.collection("items").drop(); } catch {}
|
|
33
|
+
try { await rest.db.collection("orders").drop(); } catch {}
|
|
34
|
+
try { await rest.db.collection("clients").drop(); } catch {}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("DB query cache (useCache)", () => {
|
|
38
|
+
it("serves from cache and invalidates on framework writes", async () => {
|
|
39
|
+
const marker = `dc-${crypto.randomUUID()}`;
|
|
40
|
+
await rest.insertMany("items", [{ title: marker }, { title: marker }]);
|
|
41
|
+
const params = { $match: { title: marker } };
|
|
42
|
+
|
|
43
|
+
const r1 = await rest.find("items", params, { useCache: true });
|
|
44
|
+
expect(r1.length).toBe(2);
|
|
45
|
+
|
|
46
|
+
// direct DB write (bypasses the framework) → cache is still served (stale)
|
|
47
|
+
await rest.db.collection("items").insertOne({ title: marker });
|
|
48
|
+
const r2 = await rest.find("items", params, { useCache: true });
|
|
49
|
+
expect(r2.length).toBe(2); // from cache
|
|
50
|
+
|
|
51
|
+
// framework insert → invalidates → refetches (now 4 docs: 2 + direct + framework)
|
|
52
|
+
await rest.insertOne("items", { title: marker });
|
|
53
|
+
const r3 = await rest.find("items", params, { useCache: true });
|
|
54
|
+
expect(r3.length).toBe(4); // refetched
|
|
55
|
+
|
|
56
|
+
// framework delete → invalidates
|
|
57
|
+
await rest.deleteMany("items", [r3[0]!._id]);
|
|
58
|
+
const r4 = await rest.find("items", params, { useCache: true });
|
|
59
|
+
expect(r4.length).toBe(3);
|
|
60
|
+
|
|
61
|
+
await rest.db.collection("items").deleteMany({ title: marker });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("different params produce different cache entries", async () => {
|
|
65
|
+
const marker = `dp-${crypto.randomUUID()}`;
|
|
66
|
+
await rest.insertMany("items", [{ title: marker, count: 1 }, { title: marker, count: 2 }]);
|
|
67
|
+
await rest.find("items", { $match: { title: marker, count: 1 } }, { useCache: true });
|
|
68
|
+
const r = await rest.find("items", { $match: { title: marker, count: 2 } }, { useCache: true });
|
|
69
|
+
expect(r.length).toBe(1);
|
|
70
|
+
expect(r[0]!.count).toBe(2);
|
|
71
|
+
await rest.db.collection("items").deleteMany({ title: marker });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("relations: modifying the included collection invalidates the parent cache", async () => {
|
|
75
|
+
const client = await rest.insertOne("clients", { name: "Old" });
|
|
76
|
+
const order = await rest.insertOne("orders", { code: "O1", author: client._id });
|
|
77
|
+
|
|
78
|
+
const params = { $match: { _id: order._id }, $include: ["author"] };
|
|
79
|
+
const r1: any = await rest.find("orders", params, { useCache: true });
|
|
80
|
+
expect(r1[0]!.author.name).toBe("Old");
|
|
81
|
+
|
|
82
|
+
// direct DB write on clients (bypass) → orders cache still stale
|
|
83
|
+
await rest.db.collection("clients").updateOne({ _id: new ObjectId(client._id) }, { $set: { name: "Direct" } });
|
|
84
|
+
const r2: any = await rest.find("orders", params, { useCache: true });
|
|
85
|
+
expect(r2[0]!.author.name).toBe("Old"); // from cache
|
|
86
|
+
|
|
87
|
+
// framework write on clients → invalidates orders cache too (relation)
|
|
88
|
+
await rest.updateOne("clients", client._id, { $set: { name: "New" } });
|
|
89
|
+
const r3: any = await rest.find("orders", params, { useCache: true });
|
|
90
|
+
expect(r3[0]!.author.name).toBe("New"); // refetched
|
|
91
|
+
|
|
92
|
+
await rest.db.collection("orders").deleteMany({ _id: order._id });
|
|
93
|
+
await rest.db.collection("clients").deleteMany({ _id: client._id });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("collection cache.ttl default applies without the option", async () => {
|
|
97
|
+
(cfg.collections as any)[0]!.cache = { ttl: "1m" };
|
|
98
|
+
const marker = `cttl-${crypto.randomUUID()}`;
|
|
99
|
+
await rest.insertOne("items", { title: marker });
|
|
100
|
+
const r1 = await rest.find("items", { $match: { title: marker } }, { useCache: true });
|
|
101
|
+
expect(r1.length).toBe(1);
|
|
102
|
+
delete (cfg.collections as any)[0]!.cache;
|
|
103
|
+
await rest.db.collection("items").deleteMany({ title: marker });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("targeted invalidation: an id NOT in the cached result keeps the cache", async () => {
|
|
107
|
+
const marker = `ti-out-${crypto.randomUUID()}`;
|
|
108
|
+
const feb1 = await rest.insertOne("items", { title: marker, month: "feb" });
|
|
109
|
+
const feb2 = await rest.insertOne("items", { title: marker, month: "feb" });
|
|
110
|
+
const jan = await rest.insertOne("items", { title: marker, month: "jan" });
|
|
111
|
+
|
|
112
|
+
const params = { $match: { title: marker, month: "feb" } };
|
|
113
|
+
const r1 = await rest.find("items", params, { useCache: true });
|
|
114
|
+
expect(r1.length).toBe(2); // cached (ids: feb1, feb2)
|
|
115
|
+
|
|
116
|
+
// update a JANUARY doc (id not in the cached result) → no invalidation
|
|
117
|
+
await rest.updateOne("items", jan._id, { $set: { month: "feb" } });
|
|
118
|
+
const r2 = await rest.find("items", params, { useCache: true });
|
|
119
|
+
expect(r2.length).toBe(2); // served from cache — jan's doc NOT in the result
|
|
120
|
+
|
|
121
|
+
await rest.db.collection("items").deleteMany({ title: marker });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("targeted invalidation: an id IN the cached result invalidates it", async () => {
|
|
125
|
+
const marker = `ti-in-${crypto.randomUUID()}`;
|
|
126
|
+
const a = await rest.insertOne("items", { title: marker, count: 1 });
|
|
127
|
+
const b = await rest.insertOne("items", { title: marker, count: 2 });
|
|
128
|
+
|
|
129
|
+
const params = { $match: { title: marker } };
|
|
130
|
+
const r1: any = await rest.find("items", params, { useCache: true });
|
|
131
|
+
expect(r1.find((d: any) => d._id === a._id)!.count).toBe(1);
|
|
132
|
+
|
|
133
|
+
// update a doc that IS in the cached result → targeted invalidation → refetch
|
|
134
|
+
await rest.updateOne("items", a._id, { $set: { count: 99 } });
|
|
135
|
+
const r2: any = await rest.find("items", params, { useCache: true });
|
|
136
|
+
expect(r2.find((d: any) => d._id === a._id)!.count).toBe(99);
|
|
137
|
+
expect(r2.length).toBe(2);
|
|
138
|
+
|
|
139
|
+
// delete a doc in the cache → invalidated → refetch
|
|
140
|
+
await rest.deleteOne("items", b._id);
|
|
141
|
+
const r3 = await rest.find("items", params, { useCache: true });
|
|
142
|
+
expect(r3.length).toBe(1);
|
|
143
|
+
|
|
144
|
+
await rest.db.collection("items").deleteMany({ title: marker });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("targeted invalidation works with native ObjectId ids", async () => {
|
|
148
|
+
const marker = `ti-oid-${crypto.randomUUID()}`;
|
|
149
|
+
const a = await rest.insertOne("items", { title: marker, count: 1 });
|
|
150
|
+
|
|
151
|
+
const params = { $match: { title: marker } };
|
|
152
|
+
const r1: any = await rest.find("items", params, { useCache: true });
|
|
153
|
+
expect(r1[0]!.count).toBe(1);
|
|
154
|
+
|
|
155
|
+
// update avec un ObjectId NATIF (pas un string) — la normalisation String(id) doit matcher l'index
|
|
156
|
+
await rest.updateOne("items", new ObjectId(a._id), { $set: { count: 77 } });
|
|
157
|
+
const r2: any = await rest.find("items", params, { useCache: true });
|
|
158
|
+
expect(r2[0]!.count).toBe(77); // invalidé + refetch
|
|
159
|
+
|
|
160
|
+
await rest.db.collection("items").deleteMany({ title: marker });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("maxTags configurable caps the tagged ids", async () => {
|
|
164
|
+
const cacheConf = (cfg.server as any).cache;
|
|
165
|
+
(cfg.server as any).cache = { ...cacheConf, maxTags: 2 };
|
|
166
|
+
const marker = `mt-${crypto.randomUUID()}`;
|
|
167
|
+
const a = await rest.insertOne("items", { title: marker, count: 1 });
|
|
168
|
+
const b = await rest.insertOne("items", { title: marker, count: 2 });
|
|
169
|
+
const c = await rest.insertOne("items", { title: marker, count: 3 });
|
|
170
|
+
|
|
171
|
+
const params = { $match: { title: marker }, $sort: { count: 1 } }; // ordre garanti: a(1), b(2), c(3)
|
|
172
|
+
await rest.find("items", params, { useCache: true }); // cache peuplé (3 docs)
|
|
173
|
+
|
|
174
|
+
// le 3e id (c) n'est PAS taggé (maxTags: 2) → pas d'invalidation ciblée
|
|
175
|
+
await rest.updateOne("items", c._id, { $set: { count: 33 } });
|
|
176
|
+
const r1: any = await rest.find("items", params, { useCache: true });
|
|
177
|
+
expect(r1.find((d: any) => d._id === c._id)!.count).toBe(3); // cache conservé
|
|
178
|
+
|
|
179
|
+
// le 1er id (a) est taggé → invalidation ciblée
|
|
180
|
+
await rest.updateOne("items", a._id, { $set: { count: 11 } });
|
|
181
|
+
const r2: any = await rest.find("items", params, { useCache: true });
|
|
182
|
+
expect(r2.find((d: any) => d._id === a._id)!.count).toBe(11); // refetch
|
|
183
|
+
|
|
184
|
+
(cfg.server as any).cache = cacheConf; // restore
|
|
185
|
+
await rest.db.collection("items").deleteMany({ title: marker });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("rest.cache is tenant-scoped", async () => {
|
|
189
|
+
// write from tenant A
|
|
190
|
+
await rest.cache.set("price:42", { total: 99 }, "5m");
|
|
191
|
+
expect(await rest.cache.get("price:42")).toEqual({ total: 99 });
|
|
192
|
+
expect(await rest.cache.has("price:42")).toBe(true);
|
|
193
|
+
|
|
194
|
+
// tenant B does NOT see tenant A's keys
|
|
195
|
+
const restB = new useRest({ tenant_id: "cache-test-2" });
|
|
196
|
+
expect(await restB.cache.get("price:42")).toBeUndefined();
|
|
197
|
+
await restB.cache.set("price:42", { total: 0 });
|
|
198
|
+
expect(await restB.cache.get("price:42")).toEqual({ total: 0 });
|
|
199
|
+
// tenant A unchanged
|
|
200
|
+
expect(await rest.cache.get("price:42")).toEqual({ total: 99 });
|
|
201
|
+
|
|
202
|
+
// getOrSet + delete
|
|
203
|
+
let calls = 0;
|
|
204
|
+
const v = await rest.cache.getOrSet("stats", async () => { calls++; return { n: 1 }; });
|
|
205
|
+
expect(v).toEqual({ n: 1 });
|
|
206
|
+
const v2 = await rest.cache.getOrSet("stats", async () => { calls++; return { n: 2 }; });
|
|
207
|
+
expect(v2).toEqual({ n: 1 });
|
|
208
|
+
expect(calls).toBe(1);
|
|
209
|
+
|
|
210
|
+
await rest.cache.delete("price:42");
|
|
211
|
+
await rest.cache.delete("stats");
|
|
212
|
+
expect(await rest.cache.get("price:42")).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { logger } from "../utils/logger";
|
|
5
|
+
|
|
6
|
+
const DIR = path.join(import.meta.dir, ".tmp-logger");
|
|
7
|
+
|
|
8
|
+
/** Read the log file and parse every line as JSONL */
|
|
9
|
+
function readLogLines(rel: string): any[] {
|
|
10
|
+
return fs.readFileSync(path.join(DIR, rel), "utf-8")
|
|
11
|
+
.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function flush() {
|
|
15
|
+
await logger.flush(); // wait for all pending writes
|
|
16
|
+
await Bun.sleep(10);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
try { fs.rmSync(DIR, { recursive: true, force: true }); } catch {}
|
|
21
|
+
logger.configure({ console: false, file: false });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("logger", () => {
|
|
25
|
+
it("writes JSONL lines with the Caddy envelope", async () => {
|
|
26
|
+
logger.configure({ dir: path.join(DIR, "basic"), file: true, console: false, level: "debug" });
|
|
27
|
+
logger.debug("dbg", { a: 1 });
|
|
28
|
+
logger.info("hello", { port: 4000 });
|
|
29
|
+
logger.warn("careful", { code: "X" });
|
|
30
|
+
logger.error("boom");
|
|
31
|
+
await flush();
|
|
32
|
+
const lines = readLogLines("basic/anteros.log");
|
|
33
|
+
expect(lines.length).toBe(4);
|
|
34
|
+
const hello = lines.find((l) => l.msg === "hello")!;
|
|
35
|
+
expect(hello.level).toBe("info");
|
|
36
|
+
expect(hello.port).toBe(4000);
|
|
37
|
+
expect(typeof hello.ts).toBe("number");
|
|
38
|
+
expect(lines.find((l) => l.msg === "boom")!.level).toBe("error");
|
|
39
|
+
expect(lines.find((l) => l.msg === "dbg")!.level).toBe("debug");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("respects the level filter", async () => {
|
|
43
|
+
logger.configure({ dir: path.join(DIR, "filter"), file: true, console: false, level: "warn" });
|
|
44
|
+
logger.info("hidden-info");
|
|
45
|
+
logger.debug("hidden-debug");
|
|
46
|
+
logger.warn("shown-warn");
|
|
47
|
+
logger.error("shown-error");
|
|
48
|
+
await flush();
|
|
49
|
+
const lines = readLogLines("filter/anteros.log");
|
|
50
|
+
expect(lines.some((l) => l.msg === "hidden-info")).toBe(false);
|
|
51
|
+
expect(lines.some((l) => l.msg === "hidden-debug")).toBe(false);
|
|
52
|
+
expect(lines.some((l) => l.msg === "shown-warn")).toBe(true);
|
|
53
|
+
expect(lines.some((l) => l.msg === "shown-error")).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("supports a custom file path", async () => {
|
|
57
|
+
const custom = path.join(DIR, "custom", "my-app.log");
|
|
58
|
+
logger.configure({ file: custom, console: false });
|
|
59
|
+
logger.info("custom path", { ok: true });
|
|
60
|
+
await flush();
|
|
61
|
+
const lines = fs.readFileSync(custom, "utf-8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
62
|
+
expect(lines[0]!.msg).toBe("custom path");
|
|
63
|
+
expect(lines[0]!.ok).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("rotates the file when maxSize is exceeded", async () => {
|
|
67
|
+
const dir = path.join(DIR, "rotate");
|
|
68
|
+
logger.configure({ dir, file: true, console: false, maxSize: 200, maxFiles: 3 });
|
|
69
|
+
for (let i = 0; i < 20; i++) logger.info(`line ${i} with some padding ${'x'.repeat(30)}`);
|
|
70
|
+
await flush();
|
|
71
|
+
const rotated = fs.readdirSync(dir).filter((f) => f !== "anteros.log");
|
|
72
|
+
// Dated naming: anteros-YYYY-MM-DD-N.log
|
|
73
|
+
const dated = /^anteros-\d{4}-\d{2}-\d{2}-\d+\.log$/;
|
|
74
|
+
expect(rotated.length).toBeGreaterThan(0);
|
|
75
|
+
expect(rotated.every((f) => dated.test(f))).toBe(true);
|
|
76
|
+
// maxFiles: 3 → never more than 3 rotated files kept
|
|
77
|
+
expect(rotated.length).toBeLessThanOrEqual(3);
|
|
78
|
+
// the live file is never removed
|
|
79
|
+
expect(fs.existsSync(path.join(dir, "anteros.log"))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("handles circular / non-JSON meta safely", async () => {
|
|
83
|
+
logger.configure({ dir: path.join(DIR, "safe"), file: true, console: false });
|
|
84
|
+
const circular: any = { name: "x" };
|
|
85
|
+
circular.self = circular;
|
|
86
|
+
logger.info("circular meta", circular);
|
|
87
|
+
logger.info("bigint meta", { n: 123n });
|
|
88
|
+
await flush();
|
|
89
|
+
const lines = readLogLines("safe/anteros.log");
|
|
90
|
+
expect(lines.some((l) => l.msg === "circular meta")).toBe(true);
|
|
91
|
+
const raw = fs.readFileSync(path.join(DIR, "safe", "anteros.log"), "utf-8");
|
|
92
|
+
expect(raw).toContain("[Circular]");
|
|
93
|
+
expect(lines.find((l) => l.msg === "bigint meta")!.n).toBe("123");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("keeps user-provided envelope keys", async () => {
|
|
97
|
+
logger.configure({ dir: path.join(DIR, "ts"), file: true, console: false });
|
|
98
|
+
logger.info("custom ts", { ts: 123.5, customField: 1 });
|
|
99
|
+
await flush();
|
|
100
|
+
const lines = readLogLines("ts/anteros.log");
|
|
101
|
+
expect(lines[0]!.ts).toBe(123.5);
|
|
102
|
+
expect(lines[0]!.customField).toBe(1);
|
|
103
|
+
expect(lines[0]!.msg).toBe("custom ts");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("logger.file writes to the file but never to the console", async () => {
|
|
107
|
+
const out: string[] = [];
|
|
108
|
+
const orig = console.log;
|
|
109
|
+
console.log = (...a: any[]) => { out.push(a.join(" ")); };
|
|
110
|
+
|
|
111
|
+
logger.configure({ dir: path.join(DIR, "fileonly"), file: true, console: true });
|
|
112
|
+
logger.file("Server started", { pid: 1 });
|
|
113
|
+
logger.info("visible info", { a: 1 });
|
|
114
|
+
await flush();
|
|
115
|
+
console.log = orig;
|
|
116
|
+
|
|
117
|
+
const lines = readLogLines("fileonly/anteros.log");
|
|
118
|
+
expect(lines.some((l) => l.msg === "Server started")).toBe(true);
|
|
119
|
+
expect(lines.some((l) => l.msg === "visible info")).toBe(true);
|
|
120
|
+
expect(out.some((l) => l.includes("visible info"))).toBe(true);
|
|
121
|
+
expect(out.some((l) => l.includes("Server started"))).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
});
|
package/types/collection.d.ts
CHANGED
|
@@ -79,6 +79,12 @@ export type Collection = {
|
|
|
79
79
|
actions?: {
|
|
80
80
|
[key: string]: CollectionAction;
|
|
81
81
|
};
|
|
82
|
+
/** Per-collection query caching — default TTL for `useCache` queries on this collection */
|
|
83
|
+
cache?: {
|
|
84
|
+
enabled?: boolean;
|
|
85
|
+
/** Default TTL for cached queries — human string ('5m', '2h') or ms */
|
|
86
|
+
ttl?: string | number;
|
|
87
|
+
};
|
|
82
88
|
/**
|
|
83
89
|
* The tenant id of the collection
|
|
84
90
|
* @type {string}
|
package/types/config.d.ts
CHANGED
|
@@ -58,6 +58,43 @@ export type ServerConfig = {
|
|
|
58
58
|
max?: number;
|
|
59
59
|
};
|
|
60
60
|
};
|
|
61
|
+
logging?: {
|
|
62
|
+
/** Minimum level emitted (default: 'info') */
|
|
63
|
+
level?: 'debug' | 'info' | 'warn' | 'error';
|
|
64
|
+
/** Also write to the console (default: true) */
|
|
65
|
+
console?: boolean;
|
|
66
|
+
/** File logging: true → <dir>/anteros.log, string → custom path, false → disabled (default: true) */
|
|
67
|
+
file?: boolean | string;
|
|
68
|
+
/** Directory used when `file` is true (default: '.logs') */
|
|
69
|
+
dir?: string;
|
|
70
|
+
/** Rotate when the log file exceeds this size in bytes (default: 10MB) */
|
|
71
|
+
maxSize?: number;
|
|
72
|
+
/** Keep this many rotated files (default: 5) */
|
|
73
|
+
maxFiles?: number;
|
|
74
|
+
/** Log database operations slower than this many ms (default: 200) */
|
|
75
|
+
slowQueryMs?: number;
|
|
76
|
+
};
|
|
77
|
+
/** DB query caching (find + useCache) — driver + defaults for the query cache */
|
|
78
|
+
cache?: {
|
|
79
|
+
enabled?: boolean;
|
|
80
|
+
/** Driver: 'memory' (default), 'filesystem' (.cache/ folder), 'redis' */
|
|
81
|
+
driver?: 'memory' | 'filesystem' | 'redis';
|
|
82
|
+
/** Filesystem driver directory (default: ./.cache) */
|
|
83
|
+
directory?: string;
|
|
84
|
+
/** Redis connection (driver: 'redis') */
|
|
85
|
+
redis?: {
|
|
86
|
+
url?: string;
|
|
87
|
+
host?: string;
|
|
88
|
+
port?: number;
|
|
89
|
+
password?: string;
|
|
90
|
+
};
|
|
91
|
+
/** Filesystem prune interval (default: '1h') */
|
|
92
|
+
pruneInterval?: string;
|
|
93
|
+
/** Default TTL for cached queries — human string ('5m') or ms (default: '5m') */
|
|
94
|
+
ttl?: string | number;
|
|
95
|
+
/** Max `_id` tags per cached entry — large results are tagged up to this count (default: 50000; each tag costs ~0.18µs per cache hit) */
|
|
96
|
+
maxTags?: number;
|
|
97
|
+
};
|
|
61
98
|
}
|
|
62
99
|
tenants: Tenant[];
|
|
63
100
|
}
|
package/types/mongo.d.ts
CHANGED
|
@@ -50,6 +50,16 @@ export type findOneOptions = {
|
|
|
50
50
|
$project?: Record<string, unknown>;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** Third parameter of `find`/`findOne` — query options */
|
|
54
|
+
export type FindCallOptions = {
|
|
55
|
+
/** Serve from the DB query cache — cache-first, populated on miss, invalidated on any write */
|
|
56
|
+
useCache?: boolean;
|
|
57
|
+
/** TTL override for this query — human string ('5m', '2h') or ms (default: collection/server config, else '5m') */
|
|
58
|
+
ttl?: string | number;
|
|
59
|
+
/** Clean undefined/null/empty values from the result (SDK parity) */
|
|
60
|
+
cleanDeep?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
export type LookupOptions = {
|
|
54
64
|
from: string;
|
|
55
65
|
localField: string;
|
package/types/rest.d.ts
CHANGED
|
@@ -5,6 +5,43 @@ export type RestActions =
|
|
|
5
5
|
|
|
6
6
|
export type BulkUpdateOperation = { updateOne: UpdateOneModel } | { updateMany: UpdateManyModel };
|
|
7
7
|
|
|
8
|
+
/** Options for the streaming cursor methods (findCursor / aggregateCursor / findStream / aggregateStream) */
|
|
9
|
+
export type CursorOptions = {
|
|
10
|
+
/** Number of documents fetched per batch from the server */
|
|
11
|
+
batchSize?: number;
|
|
12
|
+
/** Max execution time in ms — the query aborts with a timeout error if it runs longer */
|
|
13
|
+
maxTimeMS?: number;
|
|
14
|
+
/** Comment attached to the query — shows up in MongoDB profiling / slow query logs */
|
|
15
|
+
comment?: string;
|
|
16
|
+
/** Index hint — force a specific index: index name (`'email_1'`) or key pattern (`{ email: 1 }`) */
|
|
17
|
+
hint?: string | Record<string, 1 | -1>;
|
|
18
|
+
/** Compute the total count of matching docs in the background — each yielded item becomes `{ count, doc }` */
|
|
19
|
+
withCount?: boolean;
|
|
20
|
+
/** Abort the stream when the signal fires — iteration stops and the cursor is closed cleanly */
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
/** Transform each document on the fly (lazy — no intermediate array). Applied before `withCount` wraps the value */
|
|
23
|
+
map?: (doc: any) => any;
|
|
24
|
+
/** Page mode — when set, `findStream` yields `{ count, docs, hasNext() }` batches of this size instead of single docs */
|
|
25
|
+
pageSize?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Tenant-scoped cache API — `rest.cache.*` (namespace `app:{tenant_id}`) */
|
|
29
|
+
export type TenantCache = {
|
|
30
|
+
set(key: string, value: any, ttl?: string | number): Promise<boolean>;
|
|
31
|
+
setForever(key: string, value: any): Promise<boolean>;
|
|
32
|
+
get<T = any>(key: string): Promise<T | undefined>;
|
|
33
|
+
getOrSet<T = any>(key: string, factory: () => Promise<T> | T, ttl?: string | number): Promise<T>;
|
|
34
|
+
getOrSetForever<T = any>(key: string, factory: () => Promise<T> | T): Promise<T>;
|
|
35
|
+
has(key: string): Promise<boolean>;
|
|
36
|
+
missing(key: string): Promise<boolean>;
|
|
37
|
+
delete(key: string): Promise<boolean>;
|
|
38
|
+
deleteMany(keys: string[]): Promise<boolean>;
|
|
39
|
+
pull<T = any>(key: string): Promise<T | undefined>;
|
|
40
|
+
expire(key: string, ttl: string | number): Promise<boolean>;
|
|
41
|
+
clear(): Promise<void>;
|
|
42
|
+
namespace(name: string): any;
|
|
43
|
+
};
|
|
44
|
+
|
|
8
45
|
export type RestOptions = {
|
|
9
46
|
internal?: boolean;
|
|
10
47
|
tenant_id?: string;
|
package/utils/cache.ts
CHANGED
|
@@ -33,8 +33,8 @@ class Cache {
|
|
|
33
33
|
|
|
34
34
|
if (driver == 'filesystem') {
|
|
35
35
|
store = store.useL2Layer(fileDriver({
|
|
36
|
-
directory: path.join(process.cwd(), '.cache'),
|
|
37
|
-
pruneInterval: '1h'
|
|
36
|
+
directory: options?.filesystem?.directory ?? path.join(process.cwd(), '.cache'),
|
|
37
|
+
pruneInterval: options?.pruneInterval ?? '1h'
|
|
38
38
|
}))
|
|
39
39
|
}
|
|
40
40
|
|
package/utils/func.ts
CHANGED