@anteros/core 0.0.31 → 0.0.33

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.
@@ -8,6 +8,22 @@ import type { Field } from "../types/field"
8
8
  import { getTenant } from "./tenant"
9
9
  import { buildSchema } from "./schema"
10
10
  import type { FileCollection } from "../types/file"
11
+
12
+ /**
13
+ * Mongo index options must be real booleans: `unique`/`sparse`/… accept
14
+ * `1`/`0`/`true`/`false` but NEVER `null`/`undefined` (otherwise the server
15
+ * throws `The field 'sparse' has value null, which is not convertible to bool`).
16
+ * Coerce truthiness (`1` → `true`, `0` → `false`) and drop nullish values entirely.
17
+ */
18
+ const BOOLEAN_INDEX_OPTIONS = ['unique', 'sparse', 'background', 'hidden'];
19
+ function applyIndexOptions(spec: IndexDescription, options?: Field['indexOptions']): IndexDescription {
20
+ for (const [k, v] of Object.entries(options ?? {})) {
21
+ if (v === undefined || v === null) continue; // never send null/undefined on the wire
22
+ (spec as unknown as Record<string, unknown>)[k] = BOOLEAN_INDEX_OPTIONS.includes(k) ? !!v : v;
23
+ }
24
+ return spec;
25
+ }
26
+
11
27
  async function syncCollections() {
12
28
  try {
13
29
 
@@ -115,17 +131,17 @@ async function initializeOnDatabase(collections: Collection[]) {
115
131
  }
116
132
  }
117
133
 
118
- let isSparse = field.type === 'random'
119
- || (field.unique && field.required !== true);
134
+ // !! : field.unique peut être 1/0 (Mongo-style) → coercer en vrai booléen
135
+ let isSparse = !!(field.type === 'random'
136
+ || (field.unique && field.required !== true));
120
137
 
121
- specsFieldsIndexes.push({
138
+ specsFieldsIndexes.push(applyIndexOptions({
122
139
  key: {
123
140
  [field.name]: indexValue as any
124
141
  },
125
- unique: field.unique ?? false,
142
+ unique: !!field.unique, // 1/0/true/false → real boolean
126
143
  sparse: isSparse,
127
- ...field.indexOptions, // explicit overrides win
128
- })
144
+ }, field.indexOptions)) // explicit overrides win — sanitized
129
145
 
130
146
  }
131
147
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@anteros/core",
3
3
  "module": "index.ts",
4
- "version": "0.0.31",
4
+ "version": "0.0.33",
5
5
  "type": "module",
6
6
  "description": "Anteros Core",
7
7
  "private": false,
package/server/boot.ts CHANGED
@@ -245,6 +245,21 @@ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
245
245
  box += `${NAME}`.gray.underline + ` (PID: ${process.pid}) — ${role}\n\n`
246
246
  box += `Env: ${env || 'dev'}`.green.bold + '\n'
247
247
  box += `reusePort: ${(reusePort || isWorker) ? 'On'.green.bold : 'Off'.red.bold}\n`.gray.bold
248
+
249
+ // DB query cache status (server.cache) — driver: memory | filesystem | redis
250
+ const cacheCfg = cfg.server.cache;
251
+ // any: @colors/colors type les styles chaînés (.red.bold) comme des fonctions Color
252
+ let cacheInfo: any = 'Off'.red.bold;
253
+ if (cacheCfg?.enabled) {
254
+ const driver = cacheCfg.driver ?? 'memory';
255
+ const detail = driver === 'filesystem'
256
+ ? ` ${cacheCfg.directory ?? './.cache'}`
257
+ : driver === 'redis'
258
+ ? ` ${cacheCfg.redis?.host ?? 'localhost'}${cacheCfg.redis?.port ? `:${cacheCfg.redis.port}` : ''}`
259
+ : '';
260
+ cacheInfo = `On`.green.bold + ` (${driver}${detail})`;
261
+ }
262
+ box += `Cache: ${cacheInfo}`.gray.bold + '\n'
248
263
  box += `Url: http://localhost:${PORT}`.gray.bold;
249
264
  box += '\n\n';
250
265
  box += `Last boot: 🔄 ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`.gray;
@@ -0,0 +1,14 @@
1
+ import { define } from "../../../../index";
2
+
3
+ // Reproduces the Mongo error `sparse: null is not convertible to bool`:
4
+ // `indexOptions.sparse` explicit à null doit être ignoré (jamais envoyé à Mongo),
5
+ // et `unique` accepte le style Mongo 1/0 (coercé en vrai booléen).
6
+ export default define.Collection({
7
+ slug: "variants",
8
+ fields: [
9
+ { name: "name", type: "string", index: true, indexOptions: { sparse: null as any } },
10
+ { name: "sku", type: "string", unique: 1 },
11
+ { name: "price", type: "number" },
12
+ ],
13
+ api: { access: { "*": true } },
14
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "bun:test";
2
+ import { formatConfig, cfg } from "../server/config";
3
+ import { syncTenants } from "../database/tenant";
4
+ import { syncCollections } from "../database/collection";
5
+
6
+ const TENANT = "idx-test";
7
+ const DIR = "packages/core/tests/fixtures/index-tenant";
8
+ const DB = "mongodb://localhost:27017/_DB_IDX_TEST";
9
+
10
+ // initializeOnDatabase est fire-and-forget dans syncCollections — on attend
11
+ // que les index existent réellement en base.
12
+ async function waitForIndexes(colName: string): Promise<any[]> {
13
+ const db = cfg.tenants?.[0]?.database?.db as any;
14
+ const col = db.collection(colName);
15
+ const deadline = Date.now() + 5000;
16
+ while (Date.now() < deadline) {
17
+ try {
18
+ const idxs = await col.listIndexes().toArray();
19
+ if (idxs.length) return idxs;
20
+ } catch {}
21
+ await Bun.sleep(50);
22
+ }
23
+ throw new Error(`indexes not created for ${colName}`);
24
+ }
25
+
26
+ beforeAll(async () => {
27
+ formatConfig({
28
+ server: { port: 4000 },
29
+ tenants: [{ id: TENANT, dir: DIR, database: { uri: DB } }],
30
+ });
31
+ await syncTenants();
32
+ await syncCollections();
33
+ });
34
+
35
+ afterAll(async () => {
36
+ try { await (cfg.tenants?.[0]?.database?.db as any)?.collection("variants").drop(); } catch {}
37
+ });
38
+
39
+ describe("index spec construction", () => {
40
+ it("drops null/undefined options and coerces Mongo-style 1/0 booleans", async () => {
41
+ const idxs = await waitForIndexes("variants");
42
+
43
+ // name : indexOptions.sparse = null → ignoré (pas de sparse:null), index créé
44
+ const nameIdx = idxs.find((i) => i.name === "name_1");
45
+ expect(nameIdx).toBeDefined();
46
+ expect(nameIdx!.sparse).not.toBe(null);
47
+ expect(nameIdx!.sparse).not.toBe(true); // false / undefined — jamais null
48
+
49
+ // sku : unique: 1 (Mongo-style) → index unique réel
50
+ const skuIdx = idxs.find((i) => i.name === "sku_1");
51
+ expect(skuIdx).toBeDefined();
52
+ expect(skuIdx!.unique).toBe(true);
53
+ });
54
+ });
package/types/field.d.ts CHANGED
@@ -52,7 +52,7 @@ export type Field = {
52
52
  required?: boolean;
53
53
  enumOptions?: enumOptions;
54
54
  slugOptions?: SlugOptions;
55
- unique?: boolean;
55
+ unique?: boolean | 1 | 0; // Mongo-style: true/1 → unique index, false/0 → non-unique
56
56
  nullable?: boolean;
57
57
  empty?: boolean;
58
58
  defaultValue?: number | string | boolean | object | array<any>;
@@ -63,9 +63,9 @@ export type Field = {
63
63
  | "2d"
64
64
  indexOptions?: {
65
65
  expireAfterSeconds?: number;
66
- sparse?: boolean;
66
+ sparse?: boolean | 1 | 0;
67
67
  version?: number;
68
- unique?: boolean;
68
+ unique?: boolean | 1 | 0;
69
69
  };
70
70
  relation?: {
71
71
  to: string;