@cero-base/core 0.8.8 → 0.8.9
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/package.json +1 -1
- package/src/database/index.js +46 -1
- package/src/lib/schema.js +4 -2
- package/types/database/index.d.ts +10 -1
- package/types/lib/schema.d.ts +1 -1
package/package.json
CHANGED
package/src/database/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { makeDispatcher } from './dispatch.js'
|
|
|
26
26
|
*
|
|
27
27
|
* @typedef {{ data: any | null }} SingleResult
|
|
28
28
|
* @typedef {{ data: any[], total: number, size: number }} ListResult
|
|
29
|
-
* @typedef {{ gt?: string, gte?: string, lt?: string, lte?: string, reverse?: boolean, limit?: number }} Query
|
|
29
|
+
* @typedef {{ gt?: string, gte?: string, lt?: string, lte?: string, reverse?: boolean, limit?: number, search?: string, fields?: string[] }} Query
|
|
30
30
|
* @typedef {{ kind: string, verb: string, name: string }} Ref
|
|
31
31
|
* @typedef {(ctx: any) => any | Promise<any>} HookFn
|
|
32
32
|
*/
|
|
@@ -428,6 +428,12 @@ export class Database extends ReadyResource {
|
|
|
428
428
|
return { data: await this.view.get(col, { id: query }) }
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
const idx = this.matchIndex(name, query)
|
|
432
|
+
if (idx) {
|
|
433
|
+
const data = await this.view.find(idx.path, idx.range).toArray()
|
|
434
|
+
return { data, total: data.length, size: data.length }
|
|
435
|
+
}
|
|
436
|
+
|
|
431
437
|
const all = await this.view.find(col, {}).toArray()
|
|
432
438
|
// collections carry an auto-increment `index`; return them in that order
|
|
433
439
|
all.sort((a, b) => (a.index ?? 0) - (b.index ?? 0))
|
|
@@ -582,6 +588,25 @@ export class Database extends ReadyResource {
|
|
|
582
588
|
return `@${this.ns}/${ref.name}`
|
|
583
589
|
}
|
|
584
590
|
|
|
591
|
+
// Route an exact-field query to a declared secondary index, when one matches.
|
|
592
|
+
matchIndex(name, query) {
|
|
593
|
+
const indexes = this.refs[name]?.indexes
|
|
594
|
+
if (!indexes || !query) return null
|
|
595
|
+
const fields = Object.keys(query).filter((k) => !INDEX_RESERVED.has(k))
|
|
596
|
+
if (fields.length === 0) return null
|
|
597
|
+
for (const [idx, idxFields] of Object.entries(indexes)) {
|
|
598
|
+
if (idxFields.length === fields.length && idxFields.every((f) => fields.includes(f))) {
|
|
599
|
+
const point = {}
|
|
600
|
+
for (const f of idxFields) point[f] = query[f]
|
|
601
|
+
const range = { gte: point, lte: point }
|
|
602
|
+
if (query.limit !== undefined) range.limit = query.limit
|
|
603
|
+
if (query.reverse) range.reverse = true
|
|
604
|
+
return { path: `@${this.ns}/${name}-${idx}`, range }
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return null
|
|
608
|
+
}
|
|
609
|
+
|
|
585
610
|
_addHook(map, op, fn) {
|
|
586
611
|
if (typeof fn !== 'function') throw CeroError.INVALID('hook fn must be a function')
|
|
587
612
|
let arr = map.get(op)
|
|
@@ -610,6 +635,8 @@ export class Database extends ReadyResource {
|
|
|
610
635
|
}
|
|
611
636
|
}
|
|
612
637
|
|
|
638
|
+
const INDEX_RESERVED = new Set(['gt', 'gte', 'lt', 'lte', 'limit', 'reverse', 'search', 'fields'])
|
|
639
|
+
|
|
613
640
|
function paginate(rows, query) {
|
|
614
641
|
if (!query) return rows
|
|
615
642
|
let out = rows
|
|
@@ -617,7 +644,25 @@ function paginate(rows, query) {
|
|
|
617
644
|
if (query.gte !== undefined) out = out.filter((r) => r.id >= query.gte)
|
|
618
645
|
if (query.lt !== undefined) out = out.filter((r) => r.id < query.lt)
|
|
619
646
|
if (query.lte !== undefined) out = out.filter((r) => r.id <= query.lte)
|
|
647
|
+
if (query.search) out = out.filter((r) => searchHit(r, query.search, query.fields))
|
|
620
648
|
if (query.reverse) out = [...out].reverse()
|
|
621
649
|
if (query.limit !== undefined) out = out.slice(0, query.limit)
|
|
622
650
|
return out
|
|
623
651
|
}
|
|
652
|
+
|
|
653
|
+
// Lowercase, fold diacritics ('jose' → 'José'), drop whitespace.
|
|
654
|
+
const fold = (s) =>
|
|
655
|
+
s
|
|
656
|
+
.normalize('NFD')
|
|
657
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
658
|
+
.toLowerCase()
|
|
659
|
+
.replace(/\s/g, '')
|
|
660
|
+
|
|
661
|
+
// keet's message-search style: every term must be a substring of `fields` (or
|
|
662
|
+
// every string field). 'smith' → 'John Smith', 'jo sm' → both must appear.
|
|
663
|
+
function searchHit(row, term, fields) {
|
|
664
|
+
const terms = String(term).split(/\s+/).map(fold).filter(Boolean)
|
|
665
|
+
const keys = fields && fields.length ? fields : Object.keys(row)
|
|
666
|
+
const hay = keys.map((k) => (typeof row[k] === 'string' ? fold(row[k]) : '')).join(' ')
|
|
667
|
+
return terms.every((t) => hay.includes(t))
|
|
668
|
+
}
|
package/src/lib/schema.js
CHANGED
|
@@ -49,8 +49,10 @@ export const t = {
|
|
|
49
49
|
* @param {Record<string, Prim>} fields
|
|
50
50
|
* @returns {TypeDef}
|
|
51
51
|
*/
|
|
52
|
-
collection(fields) {
|
|
53
|
-
|
|
52
|
+
collection(fields, opts) {
|
|
53
|
+
const def = { kind: COLLECTION, fields }
|
|
54
|
+
if (opts?.indexes) def.indexes = opts.indexes
|
|
55
|
+
return def
|
|
54
56
|
},
|
|
55
57
|
|
|
56
58
|
/**
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @typedef {{ data: any | null }} SingleResult
|
|
15
15
|
* @typedef {{ data: any[], total: number, size: number }} ListResult
|
|
16
|
-
* @typedef {{ gt?: string, gte?: string, lt?: string, lte?: string, reverse?: boolean, limit?: number }} Query
|
|
16
|
+
* @typedef {{ gt?: string, gte?: string, lt?: string, lte?: string, reverse?: boolean, limit?: number, search?: string, fields?: string[] }} Query
|
|
17
17
|
* @typedef {{ kind: string, verb: string, name: string }} Ref
|
|
18
18
|
* @typedef {(ctx: any) => any | Promise<any>} HookFn
|
|
19
19
|
*/
|
|
@@ -270,6 +270,13 @@ export class Database extends ReadyResource {
|
|
|
270
270
|
verb: string;
|
|
271
271
|
};
|
|
272
272
|
col(ref: any): string;
|
|
273
|
+
matchIndex(name: any, query: any): {
|
|
274
|
+
path: string;
|
|
275
|
+
range: {
|
|
276
|
+
gte: {};
|
|
277
|
+
lte: {};
|
|
278
|
+
};
|
|
279
|
+
};
|
|
273
280
|
_addHook(map: any, op: any, fn: any): () => void;
|
|
274
281
|
_write(verb: any, hook: any, publicKey: any): Promise<void>;
|
|
275
282
|
}
|
|
@@ -340,6 +347,8 @@ export type Query = {
|
|
|
340
347
|
lte?: string;
|
|
341
348
|
reverse?: boolean;
|
|
342
349
|
limit?: number;
|
|
350
|
+
search?: string;
|
|
351
|
+
fields?: string[];
|
|
343
352
|
};
|
|
344
353
|
export type Ref = {
|
|
345
354
|
kind: string;
|
package/types/lib/schema.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export namespace t {
|
|
|
28
28
|
* @param {Record<string, Prim>} fields
|
|
29
29
|
* @returns {TypeDef}
|
|
30
30
|
*/
|
|
31
|
-
function collection(fields: Record<string, Prim
|
|
31
|
+
function collection(fields: Record<string, Prim>, opts: any): TypeDef;
|
|
32
32
|
/**
|
|
33
33
|
* RPC-style mutation that doesn't persist a row.
|
|
34
34
|
*
|