@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.
package/database/collection.ts
CHANGED
|
@@ -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
|
-
|
|
119
|
-
|
|
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
|
|
142
|
+
unique: !!field.unique, // 1/0/true/false → real boolean
|
|
126
143
|
sparse: isSparse,
|
|
127
|
-
|
|
128
|
-
})
|
|
144
|
+
}, field.indexOptions)) // explicit overrides win — sanitized
|
|
129
145
|
|
|
130
146
|
}
|
|
131
147
|
|
package/package.json
CHANGED
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;
|