@anteros/core 0.0.24 → 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/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
@@ -689,6 +689,9 @@ function buildPipeline(p: FindOptions, options?: {
689
689
  col: Collection
690
690
  }) {
691
691
 
692
+ // Never mutate the caller's params (buildPipeline fills defaults like $limit)
693
+ p = { ...p }
694
+
692
695
  let pipeline = []
693
696
  // $Match
694
697
  if (p?.$match) {
@@ -0,0 +1,212 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
5
+
6
+ const LEVEL_ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };
7
+
8
+ /** ANSI colors (no dependency on @colors/colors import order) */
9
+ const CONSOLE_COLORS: Record<LogLevel, string> = {
10
+ debug: '\x1b[90m', // gray
11
+ info: '\x1b[36m', // cyan
12
+ warn: '\x1b[33m', // yellow
13
+ error: '\x1b[31m', // red
14
+ };
15
+ const RESET = '\x1b[0m';
16
+
17
+ export type LoggerOptions = {
18
+ /** Minimum level to emit (default: 'info') */
19
+ level?: LogLevel;
20
+ /** Also write to the console (default: true) */
21
+ console?: boolean;
22
+ /** File logging: `true` → `<dir>/anteros.log`, `string` → custom path, `false` → disabled (default: true) */
23
+ file?: boolean | string;
24
+ /** Directory used when `file` is `true` (default: '.logs') */
25
+ dir?: string;
26
+ /** Rotate when the file exceeds this size in bytes (default: 10MB) */
27
+ maxSize?: number;
28
+ /** Keep this many rotated files (default: 5) */
29
+ maxFiles?: number;
30
+ };
31
+
32
+ const DEFAULT_OPTIONS: Required<Pick<LoggerOptions, 'level' | 'console' | 'dir' | 'maxSize' | 'maxFiles'>> & { file: boolean | string } = {
33
+ level: 'info',
34
+ console: true,
35
+ file: true,
36
+ dir: '.logs',
37
+ maxSize: 10 * 1024 * 1024, // 10MB
38
+ maxFiles: 5,
39
+ };
40
+
41
+ function safeStringify(value: unknown): string {
42
+ if (value === undefined) return '';
43
+ try {
44
+ const seen = new WeakSet();
45
+ return JSON.stringify(value, (_key, v) => {
46
+ if (typeof v === 'bigint') return String(v);
47
+ if (v && typeof v === 'object') {
48
+ if (seen.has(v)) return '[Circular]';
49
+ seen.add(v);
50
+ }
51
+ return v;
52
+ });
53
+ } catch {
54
+ return String(value);
55
+ }
56
+ }
57
+
58
+ class Logger {
59
+ #options: LoggerOptions & typeof DEFAULT_OPTIONS;
60
+ #filePath: string = '';
61
+ #size = 0;
62
+ #pending: Promise<void>[] = [];
63
+ #queue: Promise<void> = Promise.resolve();
64
+
65
+ constructor(options: LoggerOptions = {}) {
66
+ this.#options = { ...DEFAULT_OPTIONS, ...options };
67
+ }
68
+
69
+ /** Serialize file operations (appends + renames) — preserves order, no races */
70
+ #enqueue(fn: () => Promise<void>) {
71
+ this.#queue = this.#queue.then(fn).catch(() => {});
72
+ this.#pending.push(this.#queue);
73
+ }
74
+
75
+ /** (Re)configure — each call is independent: unspecified options reset to defaults */
76
+ configure(options: LoggerOptions = {}) {
77
+ this.#options = { ...DEFAULT_OPTIONS, ...options };
78
+ this.#openFile();
79
+ }
80
+
81
+ #resolvePath(): string {
82
+ const file = this.#options.file;
83
+ if (typeof file === 'string') return file;
84
+ const dir = this.#options.dir;
85
+ // Keep relative dirs explicit (`./logs`), but never double-prefix
86
+ // dot-dirs (`.logs`, `./logs`, `../logs`) or absolute paths
87
+ const prefix = /^(\/|\.|[A-Za-z]:)/.test(dir) ? dir : `./${dir}`;
88
+ return `${prefix}/anteros.log`;
89
+ }
90
+
91
+ #openFile() {
92
+ if (!this.#options.file) {
93
+ this.#filePath = '';
94
+ this.#size = 0;
95
+ return;
96
+ }
97
+ this.#filePath = this.#resolvePath();
98
+ fs.mkdirSync(path.dirname(this.#filePath), { recursive: true });
99
+ try {
100
+ this.#size = fs.statSync(this.#filePath).size;
101
+ } catch {
102
+ this.#size = 0;
103
+ }
104
+ }
105
+
106
+ #rotate(filePath: string) {
107
+ const { maxFiles } = this.#options;
108
+ this.#enqueue(async () => {
109
+ const dir = path.dirname(filePath);
110
+ const ext = path.extname(filePath);
111
+ const base = path.basename(filePath, ext);
112
+ const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113
+ const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
114
+
115
+ // Dated rotation name: anteros-2026-07-08-1.log, -2, … (index resets per day)
116
+ let n = 1;
117
+ try {
118
+ const files = await fs.promises.readdir(dir);
119
+ const re = new RegExp(`^${esc(base)}-${today}-(\\d+)${esc(ext)}$`);
120
+ for (const f of files) {
121
+ const m = f.match(re);
122
+ if (m) n = Math.max(n, parseInt(m[1]!, 10) + 1);
123
+ }
124
+ } catch {}
125
+ await fs.promises.rename(filePath, path.join(dir, `${base}-${today}-${n}${ext}`)).catch(() => {});
126
+
127
+ // Prune: keep only the maxFiles most recent rotated files
128
+ try {
129
+ const re = new RegExp(`^${esc(base)}-\\d{4}-\\d{2}-\\d{2}-\\d+${esc(ext)}$`);
130
+ const files = (await fs.promises.readdir(dir)).filter((f) => re.test(f));
131
+ const withTime = await Promise.all(
132
+ files.map(async (f) => ({ f, t: (await fs.promises.stat(path.join(dir, f))).mtimeMs }))
133
+ );
134
+ withTime.sort((a, b) => b.t - a.t);
135
+ for (const { f } of withTime.slice(maxFiles)) {
136
+ await fs.promises.unlink(path.join(dir, f)).catch(() => {});
137
+ }
138
+ } catch {}
139
+ });
140
+ }
141
+
142
+ #write(level: LogLevel, msg: string, meta?: unknown, opts: { console?: boolean } = {}) {
143
+ if (LEVEL_ORDER[level] < LEVEL_ORDER[this.#options.level]) return;
144
+
145
+ const tsLocal = new Date().toLocaleTimeString('en-GB', { hour12: false });
146
+ const ts = Date.now() / 1000;
147
+ const pad = level.toUpperCase().padEnd(5);
148
+ const isPlain = (v: any): boolean =>
149
+ v !== null && typeof v === 'object' && !Array.isArray(v)
150
+ && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
151
+
152
+ // JSONL payload (file): Caddy-style envelope + user meta (user keys win)
153
+ const payload: Record<string, unknown> = { ts, level, msg };
154
+ if (isPlain(meta)) {
155
+ Object.assign(payload, meta);
156
+ } else if (meta !== undefined && meta !== null) {
157
+ payload.value = meta;
158
+ }
159
+
160
+ // Console (human-readable, colorized) — skipped when opts.console === false
161
+ if (this.#options.console && opts.console !== false) {
162
+ const metaStr = safeStringify(meta);
163
+ const color = CONSOLE_COLORS[level];
164
+ const line = `[${tsLocal}] ${color}${pad}${RESET} ${msg}${metaStr ? ` ${metaStr}` : ''}`;
165
+ if (level === 'error') console.error(line);
166
+ else if (level === 'warn') console.warn(line);
167
+ else console.log(line);
168
+ }
169
+
170
+ // File: pure JSONL — one JSON object per line
171
+ if (!this.#filePath) return;
172
+ const line = safeStringify(payload) + '\n';
173
+ const bytes = Buffer.byteLength(line);
174
+ const filePath = this.#filePath;
175
+ if (this.#size + bytes > this.#options.maxSize) {
176
+ this.#size = 0;
177
+ this.#rotate(filePath);
178
+ }
179
+ this.#size += bytes;
180
+ this.#enqueue(() => fs.promises.appendFile(filePath, line));
181
+ }
182
+
183
+ debug(msg: string, meta?: unknown) { this.#write('debug', msg, meta); }
184
+ info(msg: string, meta?: unknown) { this.#write('info', msg, meta); }
185
+ warn(msg: string, meta?: unknown) { this.#write('warn', msg, meta); }
186
+ error(msg: string, meta?: unknown) { this.#write('error', msg, meta); }
187
+
188
+ /** Write to the file only (no console output) — for events that would clutter the terminal */
189
+ file(msg: string, meta?: unknown) { this.#write('info', msg, meta, { console: false }); }
190
+
191
+ /** Current log file path (empty when file logging is disabled) */
192
+ get filePath(): string { return this.#filePath; }
193
+
194
+ /** Wait until every pending write is on disk */
195
+ async flush(): Promise<void> {
196
+ while (this.#pending.length) {
197
+ const batch = this.#pending;
198
+ this.#pending = [];
199
+ await Promise.all(batch);
200
+ }
201
+ }
202
+
203
+ /** Fire-and-forget flush (used at shutdown) */
204
+ close() {
205
+ void this.flush();
206
+ }
207
+ }
208
+
209
+ /** Singleton — console + file. Configured at boot from `cfg.server.logging`. */
210
+ export const logger = new Logger({ console: true, file: false });
211
+
212
+ export default logger;