@cyblow/paginate-core 0.1.3

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.
Files changed (4) hide show
  1. package/README.md +68 -0
  2. package/index.d.ts +85 -0
  3. package/index.js +326 -0
  4. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # paginate-node
2
+
3
+ napi-rs (Node-API) adapter that exposes the pure-Rust [`paginate-core`](../core)
4
+ engine to Node.js / TypeScript. It mirrors the [PyO3 adapter](../py) surface so
5
+ both runtimes share one engine and one set of behaviours (the cursor wire format
6
+ is byte-identical across Python, Node, and the core itself).
7
+
8
+ ## What it exposes
9
+
10
+ Rust function names are exported to JavaScript as camelCase (napi-rs convention):
11
+
12
+ | JS export | Signature | Description |
13
+ |-----------|-----------|-------------|
14
+ | `normalizeText` | `(value: string) => string` | NFKD accent-stripping text normalization. |
15
+ | `offset` | `(page: number, limit: number) => number` | Zero-based row offset. |
16
+ | `maxPages` | `(total: number, limit: number) => number` | Total page count (ceil). |
17
+ | `clampPage` | `(page, limit, total) => number` | Clamp page into `[1, maxPage]`. |
18
+ | `offsetMeta` | `(page, limit, total) => OffsetMeta` | `{ page, pages, hasNext, hasPrevious }`. |
19
+ | `encodeCursor` | `(values: unknown) => string` | Encode ordering values into a URL-safe cursor. |
20
+ | `decodeCursor` | `(cursor: string) => unknown[]` | Decode a cursor back into its ordering values. |
21
+ | `filterIndices` | `(items, specs) => number[]` | Indices matching flat filter specs `[{field,op,value,logic?}]`. |
22
+ | `sortIndices` | `(items, specs) => number[]` | Index permutation for sort specs `[{field,direction?,nulls?}]`. |
23
+ | `searchIndices` | `(items, query, fields, …) => number[]` | Ranked search indices over `fields`. |
24
+
25
+ ### Cursor values
26
+
27
+ Ordering values cross the boundary as plain JSON (`serde_json::Value`):
28
+ `null` / `bool` / `number` (integral -> Int, otherwise Float) / `string` /
29
+ `array` / `object`. JavaScript has no native datetime / Decimal / UUID types, so
30
+ the typed-scalar variants the Python codec round-trips are not minted here —
31
+ pass their ISO/canonical strings instead. Cursors remain interoperable: a cursor
32
+ produced by Python or the Rust core decodes here and vice versa.
33
+
34
+ ### Performance — prefer native Array methods for in-memory work
35
+
36
+ `filterIndices` / `sortIndices` / `searchIndices` exist for **behaviour parity**
37
+ with pypaginate's exact semantics — **not** for speed. Marshalling a large array
38
+ across the napi boundary costs far more than the per-item work, so V8's
39
+ `Array.filter` / `Array.sort` are **40–230× faster** (see
40
+ [../../BENCHMARKS.md](../../BENCHMARKS.md)). Reach for the native engines only
41
+ when you need pypaginate's precise operator / ranking behaviour; otherwise use
42
+ plain JS. The **cursor codec** and `normalizeText` are the consistency-critical
43
+ paths genuinely worth crossing the boundary for.
44
+
45
+ ## Building
46
+
47
+ This crate compiles to a Node-API addon (`crate-type = ["cdylib"]`). The native
48
+ addon plus its generated `index.js` / `index.d.ts` glue are produced by
49
+ [`@napi-rs/cli`](https://www.npmjs.com/package/@napi-rs/cli):
50
+
51
+ ```bash
52
+ npm install # installs @napi-rs/cli
53
+ npm run build # napi build --platform --release -> *.node + index.{js,d.ts}
54
+ ```
55
+
56
+ `build.rs` calls `napi_build::setup()` so the linker is configured for the host
57
+ Node-API toolchain.
58
+
59
+ A plain `cargo build -p paginate-node` / `cargo check -p paginate-node` compiles
60
+ the Rust object too, but only `@napi-rs/cli` emits the loadable addon and TS
61
+ type definitions.
62
+
63
+ ## Status
64
+
65
+ Builds and is validated from Node — cursor wire-identical to Python/core, and
66
+ filter/sort/search return correct results. The generated artifacts (`index.js`,
67
+ `index.d.ts`, `*.node`) are build outputs and are not checked in. Consumed by
68
+ the `packages/ts` TypeScript package.
package/index.d.ts ADDED
@@ -0,0 +1,85 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /* auto-generated by NAPI-RS */
5
+
6
+ /**
7
+ * One page of results returned by [`Dataset::page`]. Numeric fields export as
8
+ * JS `number`; `has_next`/`has_previous` as `hasNext`/`hasPrevious`.
9
+ */
10
+ export interface DatasetPage {
11
+ /** Indices (into the original array) of this page's rows, in final order. */
12
+ indices: Array<number>
13
+ /** Total matched rows after filtering, before paging. */
14
+ total: number
15
+ /** The requested page number. */
16
+ page: number
17
+ /** Total number of pages. */
18
+ pages: number
19
+ /** Whether a following page exists. */
20
+ hasNext: boolean
21
+ /** Whether a preceding page exists. */
22
+ hasPrevious: boolean
23
+ }
24
+ /** Indices of items matching flat filter specs `[{field, op, value, logic?}]`. */
25
+ export declare function filterIndices(items: Json, specs: Json): Array<number>
26
+ /**
27
+ * Indices of items matching a nested filter `group`. A leaf is
28
+ * `{field, op, value, logic?}`; a group is `{logic, conditions: [node, ...]}`.
29
+ * Mirrors the PyO3 `filter_group_indices` so JS/TS gets nested And/Or filters.
30
+ */
31
+ export declare function filterGroupIndices(items: Json, group: Json): Array<number>
32
+ /** A permutation of item indices for sort specs `[{field, direction?, nulls?}]`. */
33
+ export declare function sortIndices(items: Json, specs: Json): Array<number>
34
+ /** Ranked search: indices of items by relevance of `query` over `fields`. */
35
+ export declare function searchIndices(items: Json, query: string, fields: Array<string>, mode?: string | undefined | null, fuzzy?: string | undefined | null, threshold?: number | undefined | null, minLength?: number | undefined | null, maxResults?: number | undefined | null): Array<number>
36
+ /** Encode a list of ordering values into a URL-safe cursor string. */
37
+ export declare function encodeCursor(values: Json): string
38
+ /** Decode a cursor string back into its array of ordering values. */
39
+ export declare function decodeCursor(cursor: string): Json
40
+ /** Normalize text for search/filtering (ASCII fast path + NFKD accent strip). */
41
+ export declare function normalizeText(value: string): string
42
+ /**
43
+ * Page metadata returned by [`offset_meta`]. Fields export as camelCase
44
+ * (`hasNext`, `hasPrevious`) under napi-rs.
45
+ */
46
+ export interface OffsetMeta {
47
+ /** The (possibly clamped) 1-based page number. */
48
+ page: number
49
+ /** Total number of pages. */
50
+ pages: number
51
+ /** Whether a following page exists. */
52
+ hasNext: boolean
53
+ /** Whether a preceding page exists. */
54
+ hasPrevious: boolean
55
+ }
56
+ /** Zero-based row offset for `(page, limit)`. */
57
+ export declare function offset(page: number, limit: number): number
58
+ /** Total page count for `total` rows at `limit` per page. */
59
+ export declare function maxPages(total: number, limit: number): number
60
+ /** Page metadata as an [`OffsetMeta`] object for `(page, limit, total)`. */
61
+ export declare function offsetMeta(page: number, limit: number, total: number): OffsetMeta
62
+ /** Clamp `page` into the valid `[1, max_page]` range. */
63
+ export declare function clampPage(page: number, limit: number, total: number): number
64
+ /**
65
+ * An in-memory dataset held in Rust as `core::Value` rows, queried by index.
66
+ * Build it once from a JS array of objects, then call `filter`/`sort`/`search`/
67
+ * `page`; each returns indices the caller maps back to its own objects.
68
+ */
69
+ export declare class Dataset {
70
+ /** Marshal a JS array of objects into the resident dataset once. */
71
+ constructor(items: Json)
72
+ /** Number of rows. */
73
+ get size(): number
74
+ /** Indices matching flat filter specs `[{field, op, value, logic?}]`. */
75
+ filter(specs: Json): Array<number>
76
+ /** A permutation of row indices for sort specs `[{field, direction?, nulls?}]`. */
77
+ sort(specs: Json): Array<number>
78
+ /** Ranked-search indices over `fields`. */
79
+ search(query: string, fields: Array<string>, mode?: string | undefined | null, fuzzy?: string | undefined | null, threshold?: number | undefined | null, minLength?: number | undefined | null, maxResults?: number | undefined | null): Array<number>
80
+ /**
81
+ * Filter + sort + offset-paginate in ONE native call. Returns the page's
82
+ * row indices + offset metadata; the caller selects its rows by index.
83
+ */
84
+ page(page: number, limit: number, filters?: Json | undefined | null, sorts?: Json | undefined | null): DatasetPage
85
+ }
package/index.js ADDED
@@ -0,0 +1,326 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ /* prettier-ignore */
4
+
5
+ /* auto-generated by NAPI-RS */
6
+
7
+ const { existsSync, readFileSync } = require('fs')
8
+ const { join } = require('path')
9
+
10
+ const { platform, arch } = process
11
+
12
+ let nativeBinding = null
13
+ let localFileExisted = false
14
+ let loadError = null
15
+
16
+ function isMusl() {
17
+ // For Node 10
18
+ if (!process.report || typeof process.report.getReport !== 'function') {
19
+ try {
20
+ const lddPath = require('child_process').execSync('which ldd').toString().trim()
21
+ return readFileSync(lddPath, 'utf8').includes('musl')
22
+ } catch (e) {
23
+ return true
24
+ }
25
+ } else {
26
+ const { glibcVersionRuntime } = process.report.getReport().header
27
+ return !glibcVersionRuntime
28
+ }
29
+ }
30
+
31
+ switch (platform) {
32
+ case 'android':
33
+ switch (arch) {
34
+ case 'arm64':
35
+ localFileExisted = existsSync(join(__dirname, '@cyblow/paginate-core.android-arm64.node'))
36
+ try {
37
+ if (localFileExisted) {
38
+ nativeBinding = require('./@cyblow/paginate-core.android-arm64.node')
39
+ } else {
40
+ nativeBinding = require('@cyblow/paginate-core-android-arm64')
41
+ }
42
+ } catch (e) {
43
+ loadError = e
44
+ }
45
+ break
46
+ case 'arm':
47
+ localFileExisted = existsSync(join(__dirname, '@cyblow/paginate-core.android-arm-eabi.node'))
48
+ try {
49
+ if (localFileExisted) {
50
+ nativeBinding = require('./@cyblow/paginate-core.android-arm-eabi.node')
51
+ } else {
52
+ nativeBinding = require('@cyblow/paginate-core-android-arm-eabi')
53
+ }
54
+ } catch (e) {
55
+ loadError = e
56
+ }
57
+ break
58
+ default:
59
+ throw new Error(`Unsupported architecture on Android ${arch}`)
60
+ }
61
+ break
62
+ case 'win32':
63
+ switch (arch) {
64
+ case 'x64':
65
+ localFileExisted = existsSync(
66
+ join(__dirname, '@cyblow/paginate-core.win32-x64-msvc.node')
67
+ )
68
+ try {
69
+ if (localFileExisted) {
70
+ nativeBinding = require('./@cyblow/paginate-core.win32-x64-msvc.node')
71
+ } else {
72
+ nativeBinding = require('@cyblow/paginate-core-win32-x64-msvc')
73
+ }
74
+ } catch (e) {
75
+ loadError = e
76
+ }
77
+ break
78
+ case 'ia32':
79
+ localFileExisted = existsSync(
80
+ join(__dirname, '@cyblow/paginate-core.win32-ia32-msvc.node')
81
+ )
82
+ try {
83
+ if (localFileExisted) {
84
+ nativeBinding = require('./@cyblow/paginate-core.win32-ia32-msvc.node')
85
+ } else {
86
+ nativeBinding = require('@cyblow/paginate-core-win32-ia32-msvc')
87
+ }
88
+ } catch (e) {
89
+ loadError = e
90
+ }
91
+ break
92
+ case 'arm64':
93
+ localFileExisted = existsSync(
94
+ join(__dirname, '@cyblow/paginate-core.win32-arm64-msvc.node')
95
+ )
96
+ try {
97
+ if (localFileExisted) {
98
+ nativeBinding = require('./@cyblow/paginate-core.win32-arm64-msvc.node')
99
+ } else {
100
+ nativeBinding = require('@cyblow/paginate-core-win32-arm64-msvc')
101
+ }
102
+ } catch (e) {
103
+ loadError = e
104
+ }
105
+ break
106
+ default:
107
+ throw new Error(`Unsupported architecture on Windows: ${arch}`)
108
+ }
109
+ break
110
+ case 'darwin':
111
+ localFileExisted = existsSync(join(__dirname, '@cyblow/paginate-core.darwin-universal.node'))
112
+ try {
113
+ if (localFileExisted) {
114
+ nativeBinding = require('./@cyblow/paginate-core.darwin-universal.node')
115
+ } else {
116
+ nativeBinding = require('@cyblow/paginate-core-darwin-universal')
117
+ }
118
+ break
119
+ } catch {}
120
+ switch (arch) {
121
+ case 'x64':
122
+ localFileExisted = existsSync(join(__dirname, '@cyblow/paginate-core.darwin-x64.node'))
123
+ try {
124
+ if (localFileExisted) {
125
+ nativeBinding = require('./@cyblow/paginate-core.darwin-x64.node')
126
+ } else {
127
+ nativeBinding = require('@cyblow/paginate-core-darwin-x64')
128
+ }
129
+ } catch (e) {
130
+ loadError = e
131
+ }
132
+ break
133
+ case 'arm64':
134
+ localFileExisted = existsSync(
135
+ join(__dirname, '@cyblow/paginate-core.darwin-arm64.node')
136
+ )
137
+ try {
138
+ if (localFileExisted) {
139
+ nativeBinding = require('./@cyblow/paginate-core.darwin-arm64.node')
140
+ } else {
141
+ nativeBinding = require('@cyblow/paginate-core-darwin-arm64')
142
+ }
143
+ } catch (e) {
144
+ loadError = e
145
+ }
146
+ break
147
+ default:
148
+ throw new Error(`Unsupported architecture on macOS: ${arch}`)
149
+ }
150
+ break
151
+ case 'freebsd':
152
+ if (arch !== 'x64') {
153
+ throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
154
+ }
155
+ localFileExisted = existsSync(join(__dirname, '@cyblow/paginate-core.freebsd-x64.node'))
156
+ try {
157
+ if (localFileExisted) {
158
+ nativeBinding = require('./@cyblow/paginate-core.freebsd-x64.node')
159
+ } else {
160
+ nativeBinding = require('@cyblow/paginate-core-freebsd-x64')
161
+ }
162
+ } catch (e) {
163
+ loadError = e
164
+ }
165
+ break
166
+ case 'linux':
167
+ switch (arch) {
168
+ case 'x64':
169
+ if (isMusl()) {
170
+ localFileExisted = existsSync(
171
+ join(__dirname, '@cyblow/paginate-core.linux-x64-musl.node')
172
+ )
173
+ try {
174
+ if (localFileExisted) {
175
+ nativeBinding = require('./@cyblow/paginate-core.linux-x64-musl.node')
176
+ } else {
177
+ nativeBinding = require('@cyblow/paginate-core-linux-x64-musl')
178
+ }
179
+ } catch (e) {
180
+ loadError = e
181
+ }
182
+ } else {
183
+ localFileExisted = existsSync(
184
+ join(__dirname, '@cyblow/paginate-core.linux-x64-gnu.node')
185
+ )
186
+ try {
187
+ if (localFileExisted) {
188
+ nativeBinding = require('./@cyblow/paginate-core.linux-x64-gnu.node')
189
+ } else {
190
+ nativeBinding = require('@cyblow/paginate-core-linux-x64-gnu')
191
+ }
192
+ } catch (e) {
193
+ loadError = e
194
+ }
195
+ }
196
+ break
197
+ case 'arm64':
198
+ if (isMusl()) {
199
+ localFileExisted = existsSync(
200
+ join(__dirname, '@cyblow/paginate-core.linux-arm64-musl.node')
201
+ )
202
+ try {
203
+ if (localFileExisted) {
204
+ nativeBinding = require('./@cyblow/paginate-core.linux-arm64-musl.node')
205
+ } else {
206
+ nativeBinding = require('@cyblow/paginate-core-linux-arm64-musl')
207
+ }
208
+ } catch (e) {
209
+ loadError = e
210
+ }
211
+ } else {
212
+ localFileExisted = existsSync(
213
+ join(__dirname, '@cyblow/paginate-core.linux-arm64-gnu.node')
214
+ )
215
+ try {
216
+ if (localFileExisted) {
217
+ nativeBinding = require('./@cyblow/paginate-core.linux-arm64-gnu.node')
218
+ } else {
219
+ nativeBinding = require('@cyblow/paginate-core-linux-arm64-gnu')
220
+ }
221
+ } catch (e) {
222
+ loadError = e
223
+ }
224
+ }
225
+ break
226
+ case 'arm':
227
+ if (isMusl()) {
228
+ localFileExisted = existsSync(
229
+ join(__dirname, '@cyblow/paginate-core.linux-arm-musleabihf.node')
230
+ )
231
+ try {
232
+ if (localFileExisted) {
233
+ nativeBinding = require('./@cyblow/paginate-core.linux-arm-musleabihf.node')
234
+ } else {
235
+ nativeBinding = require('@cyblow/paginate-core-linux-arm-musleabihf')
236
+ }
237
+ } catch (e) {
238
+ loadError = e
239
+ }
240
+ } else {
241
+ localFileExisted = existsSync(
242
+ join(__dirname, '@cyblow/paginate-core.linux-arm-gnueabihf.node')
243
+ )
244
+ try {
245
+ if (localFileExisted) {
246
+ nativeBinding = require('./@cyblow/paginate-core.linux-arm-gnueabihf.node')
247
+ } else {
248
+ nativeBinding = require('@cyblow/paginate-core-linux-arm-gnueabihf')
249
+ }
250
+ } catch (e) {
251
+ loadError = e
252
+ }
253
+ }
254
+ break
255
+ case 'riscv64':
256
+ if (isMusl()) {
257
+ localFileExisted = existsSync(
258
+ join(__dirname, '@cyblow/paginate-core.linux-riscv64-musl.node')
259
+ )
260
+ try {
261
+ if (localFileExisted) {
262
+ nativeBinding = require('./@cyblow/paginate-core.linux-riscv64-musl.node')
263
+ } else {
264
+ nativeBinding = require('@cyblow/paginate-core-linux-riscv64-musl')
265
+ }
266
+ } catch (e) {
267
+ loadError = e
268
+ }
269
+ } else {
270
+ localFileExisted = existsSync(
271
+ join(__dirname, '@cyblow/paginate-core.linux-riscv64-gnu.node')
272
+ )
273
+ try {
274
+ if (localFileExisted) {
275
+ nativeBinding = require('./@cyblow/paginate-core.linux-riscv64-gnu.node')
276
+ } else {
277
+ nativeBinding = require('@cyblow/paginate-core-linux-riscv64-gnu')
278
+ }
279
+ } catch (e) {
280
+ loadError = e
281
+ }
282
+ }
283
+ break
284
+ case 's390x':
285
+ localFileExisted = existsSync(
286
+ join(__dirname, '@cyblow/paginate-core.linux-s390x-gnu.node')
287
+ )
288
+ try {
289
+ if (localFileExisted) {
290
+ nativeBinding = require('./@cyblow/paginate-core.linux-s390x-gnu.node')
291
+ } else {
292
+ nativeBinding = require('@cyblow/paginate-core-linux-s390x-gnu')
293
+ }
294
+ } catch (e) {
295
+ loadError = e
296
+ }
297
+ break
298
+ default:
299
+ throw new Error(`Unsupported architecture on Linux: ${arch}`)
300
+ }
301
+ break
302
+ default:
303
+ throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
304
+ }
305
+
306
+ if (!nativeBinding) {
307
+ if (loadError) {
308
+ throw loadError
309
+ }
310
+ throw new Error(`Failed to load native binding`)
311
+ }
312
+
313
+ const { Dataset, filterIndices, filterGroupIndices, sortIndices, searchIndices, encodeCursor, decodeCursor, normalizeText, offset, maxPages, offsetMeta, clampPage } = nativeBinding
314
+
315
+ module.exports.Dataset = Dataset
316
+ module.exports.filterIndices = filterIndices
317
+ module.exports.filterGroupIndices = filterGroupIndices
318
+ module.exports.sortIndices = sortIndices
319
+ module.exports.searchIndices = searchIndices
320
+ module.exports.encodeCursor = encodeCursor
321
+ module.exports.decodeCursor = decodeCursor
322
+ module.exports.normalizeText = normalizeText
323
+ module.exports.offset = offset
324
+ module.exports.maxPages = maxPages
325
+ module.exports.offsetMeta = offsetMeta
326
+ module.exports.clampPage = clampPage
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@cyblow/paginate-core",
3
+ "version": "0.1.3",
4
+ "description": "Node/TypeScript native bindings for paginate-core (cursor codec, text normalization, pagination math, filtering, sorting, search).",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/CybLow/paginate"
9
+ },
10
+ "main": "index.js",
11
+ "types": "index.d.ts",
12
+ "files": [
13
+ "index.js",
14
+ "index.d.ts"
15
+ ],
16
+ "napi": {
17
+ "name": "@cyblow/paginate-core",
18
+ "triples": {
19
+ "defaults": true,
20
+ "additional": [
21
+ "aarch64-apple-darwin",
22
+ "aarch64-unknown-linux-gnu",
23
+ "x86_64-unknown-linux-musl",
24
+ "aarch64-unknown-linux-musl",
25
+ "aarch64-pc-windows-msvc"
26
+ ]
27
+ }
28
+ },
29
+ "engines": {
30
+ "node": ">=12.22.0"
31
+ },
32
+ "scripts": {
33
+ "build": "napi build --platform --release",
34
+ "build:debug": "napi build --platform",
35
+ "artifacts": "napi artifacts",
36
+ "prepublishOnly": "napi prepublish -t npm"
37
+ },
38
+ "devDependencies": {
39
+ "@napi-rs/cli": "^2.18.0"
40
+ },
41
+ "_note": "Scaffold. Generated bindings (index.js/index.d.ts/*.node) are produced by `@napi-rs/cli` (`npm run build`) and are not checked in. Consumed by packages/ts.",
42
+ "optionalDependencies": {
43
+ "@cyblow/paginate-core-win32-x64-msvc": "0.1.3",
44
+ "@cyblow/paginate-core-darwin-x64": "0.1.3",
45
+ "@cyblow/paginate-core-linux-x64-gnu": "0.1.3",
46
+ "@cyblow/paginate-core-darwin-arm64": "0.1.3",
47
+ "@cyblow/paginate-core-linux-arm64-gnu": "0.1.3",
48
+ "@cyblow/paginate-core-linux-x64-musl": "0.1.3",
49
+ "@cyblow/paginate-core-linux-arm64-musl": "0.1.3",
50
+ "@cyblow/paginate-core-win32-arm64-msvc": "0.1.3"
51
+ }
52
+ }