@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cero-base/core",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
4
4
  "description": "cero p2p primitives — identity, storage, network, database, blobs, rpc, pairing.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -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
- return { kind: COLLECTION, fields }
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;
@@ -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>): TypeDef;
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
  *