nestjs-typesense 0.1.0
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/LICENSE +21 -0
- package/README.md +318 -0
- package/dist/client/typesense.client.d.ts +38 -0
- package/dist/client/typesense.client.d.ts.map +1 -0
- package/dist/client/typesense.client.js +100 -0
- package/dist/client/typesense.client.js.map +1 -0
- package/dist/collections/typesense-collections.d.ts +21 -0
- package/dist/collections/typesense-collections.d.ts.map +1 -0
- package/dist/collections/typesense-collections.js +120 -0
- package/dist/collections/typesense-collections.js.map +1 -0
- package/dist/collectors/typesense-collector.d.ts +22 -0
- package/dist/collectors/typesense-collector.d.ts.map +1 -0
- package/dist/collectors/typesense-collector.decorator.d.ts +11 -0
- package/dist/collectors/typesense-collector.decorator.d.ts.map +1 -0
- package/dist/collectors/typesense-collector.decorator.js +27 -0
- package/dist/collectors/typesense-collector.decorator.js.map +1 -0
- package/dist/collectors/typesense-collector.js +3 -0
- package/dist/collectors/typesense-collector.js.map +1 -0
- package/dist/collectors/typesense-indexer.d.ts +34 -0
- package/dist/collectors/typesense-indexer.d.ts.map +1 -0
- package/dist/collectors/typesense-indexer.js +110 -0
- package/dist/collectors/typesense-indexer.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/schema/collection.d.ts +87 -0
- package/dist/schema/collection.d.ts.map +1 -0
- package/dist/schema/collection.js +64 -0
- package/dist/schema/collection.js.map +1 -0
- package/dist/schema/decorators.d.ts +73 -0
- package/dist/schema/decorators.d.ts.map +1 -0
- package/dist/schema/decorators.js +126 -0
- package/dist/schema/decorators.js.map +1 -0
- package/dist/schema/field.d.ts +71 -0
- package/dist/schema/field.d.ts.map +1 -0
- package/dist/schema/field.js +40 -0
- package/dist/schema/field.js.map +1 -0
- package/dist/typesense.constants.d.ts +9 -0
- package/dist/typesense.constants.d.ts.map +1 -0
- package/dist/typesense.constants.js +12 -0
- package/dist/typesense.constants.js.map +1 -0
- package/dist/typesense.module-options.d.ts +24 -0
- package/dist/typesense.module-options.d.ts.map +1 -0
- package/dist/typesense.module-options.js +3 -0
- package/dist/typesense.module-options.js.map +1 -0
- package/dist/typesense.module.d.ts +13 -0
- package/dist/typesense.module.d.ts.map +1 -0
- package/dist/typesense.module.js +42 -0
- package/dist/typesense.module.js.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ebenezer Domey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# nestjs-typesense
|
|
2
|
+
|
|
3
|
+
An unofficial NestJS module for [Typesense](https://typesense.org): typed collection schemas,
|
|
4
|
+
declarative collection migrations, and **database-agnostic** index syncing.
|
|
5
|
+
|
|
6
|
+
> Not affiliated with or endorsed by Typesense Inc. "Typesense" is their trademark.
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
The existing NestJS integrations either sync from MongoDB change streams — unusable if your
|
|
11
|
+
data lives anywhere else — or pull an opinionated pagination and error envelope into your API
|
|
12
|
+
responses. This module does neither. Indexing is expressed as ordinary queries against your own
|
|
13
|
+
store, and nothing about your HTTP contract is dictated.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i nestjs-typesense typesense
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Peer dependencies: `@nestjs/common`, `@nestjs/core`, `reflect-metadata`, `typesense`.
|
|
22
|
+
|
|
23
|
+
## Define a collection
|
|
24
|
+
|
|
25
|
+
Field declarations drive both the Typesense schema and the TypeScript document type.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { defineCollection, field } from 'nestjs-typesense'
|
|
29
|
+
|
|
30
|
+
export const videoCollection = defineCollection({
|
|
31
|
+
name: 'videos',
|
|
32
|
+
fields: {
|
|
33
|
+
title: field.string(),
|
|
34
|
+
description: field.string({ optional: true }),
|
|
35
|
+
durationMs: field.int32({ sort: true }),
|
|
36
|
+
tags: field.stringArray({ facet: true }),
|
|
37
|
+
thumbnailUrl: field.string({ index: false, optional: true }),
|
|
38
|
+
},
|
|
39
|
+
defaultSortingField: 'durationMs',
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`InferDocument` gives you the document type, with `id` always present and `optional` fields
|
|
44
|
+
genuinely optional:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import type { InferDocument } from 'nestjs-typesense'
|
|
48
|
+
|
|
49
|
+
type VideoDocument = InferDocument<typeof videoCollection>
|
|
50
|
+
// { id: string; title: string; durationMs: number; tags: string[]
|
|
51
|
+
// description?: string; thumbnailUrl?: string }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Or declare it with decorators
|
|
55
|
+
|
|
56
|
+
If you prefer the `@nestjs/mongoose` style, decorate a class instead. The class *is* the
|
|
57
|
+
document type, so there is no `InferDocument` step:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import {
|
|
61
|
+
TypesenseArray,
|
|
62
|
+
TypesenseInt32,
|
|
63
|
+
TypesenseSchema,
|
|
64
|
+
TypesenseString,
|
|
65
|
+
} from 'nestjs-typesense'
|
|
66
|
+
|
|
67
|
+
@TypesenseSchema({ name: 'videos', defaultSortingField: 'durationMs' })
|
|
68
|
+
export class VideoDocument {
|
|
69
|
+
@TypesenseString() title!: string
|
|
70
|
+
@TypesenseString({ optional: true }) description?: string
|
|
71
|
+
@TypesenseInt32({ sort: true }) durationMs!: number
|
|
72
|
+
@TypesenseArray({ type: 'string', facet: true }) tags!: string[]
|
|
73
|
+
@TypesenseString({ index: false, optional: true }) thumbnailUrl?: string
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Both forms produce the same collection, schema hash included, and either can be passed
|
|
78
|
+
wherever a collection is expected.
|
|
79
|
+
|
|
80
|
+
Unlike `@nestjs/mongoose`'s `@Prop()`, these decorators check the field against the property
|
|
81
|
+
they annotate. Every one of these fails to compile:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
@TypesenseInt32() title!: string // int32 on a string property
|
|
85
|
+
@TypesenseString() thumbnailUrl?: string // required field, optional property
|
|
86
|
+
@TypesenseArray({ type: 'string' }) tags!: number[] // element type disagrees
|
|
87
|
+
@TypesenseArray({ type: 'string' }) tags!: string // not an array at all
|
|
88
|
+
@TypesenseArray({ type: 'typo' }) tags!: string[] // not a field type
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
There is one decorator per scalar type — `TypesenseString`, `TypesenseInt32`,
|
|
92
|
+
`TypesenseInt64`, `TypesenseFloat`, `TypesenseBool`, `TypesenseGeopoint`, `TypesenseObject`
|
|
93
|
+
and `TypesenseAuto` — plus `TypesenseArray({ type })` covering all seven array types, and
|
|
94
|
+
`TypesenseProp({ type })` if you would rather name the type explicitly.
|
|
95
|
+
|
|
96
|
+
Modifiers stay inside the options object rather than stacking as separate decorators the way
|
|
97
|
+
`class-validator` does with `@IsOptional()`. That is deliberate: `optional` has to travel in
|
|
98
|
+
the same call as the type for TypeScript to check them against each other, and a stacked
|
|
99
|
+
modifier is checked independently, so the type decorator could never see it.
|
|
100
|
+
|
|
101
|
+
Fields declared on a base class are collected as well, so shared audit columns can live in
|
|
102
|
+
one place. `id` is always implicit — declare `id: string` on the class if you want it in the
|
|
103
|
+
document type and it will not be emitted twice.
|
|
104
|
+
|
|
105
|
+
### Which style to use
|
|
106
|
+
|
|
107
|
+
Both produce the same collection and are interchangeable everywhere downstream, so this is
|
|
108
|
+
purely about how you like to declare things — with one hard constraint:
|
|
109
|
+
|
|
110
|
+
| | `defineCollection` | `@TypesenseSchema` |
|
|
111
|
+
| -------------------------------------- | ------------------ | ------------------ |
|
|
112
|
+
| TypeScript | yes | yes |
|
|
113
|
+
| Plain JavaScript | yes | no — decorator syntax needs a transpiler |
|
|
114
|
+
| Requires `experimentalDecorators` | no | yes |
|
|
115
|
+
| Document type | `InferDocument<typeof collection>` | the class itself |
|
|
116
|
+
| Doubles as a DTO (`class-validator`, Swagger) | no | yes |
|
|
117
|
+
|
|
118
|
+
NestJS projects already enable `experimentalDecorators` and `emitDecoratorMetadata`, so both
|
|
119
|
+
styles work there without any config change. Reach for `defineCollection` if you are consuming
|
|
120
|
+
this from plain JavaScript, from a non-Nest TypeScript project, or if you would rather keep
|
|
121
|
+
schemas as plain data.
|
|
122
|
+
|
|
123
|
+
The package ships CommonJS, so `require('nestjs-typesense')` works as-is.
|
|
124
|
+
|
|
125
|
+
## Register the module
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { Module } from '@nestjs/common'
|
|
129
|
+
import { ConfigService } from '@nestjs/config'
|
|
130
|
+
import { TypesenseModule } from 'nestjs-typesense'
|
|
131
|
+
import { videoCollection } from './search/video.collection.js'
|
|
132
|
+
|
|
133
|
+
@Module({
|
|
134
|
+
imports: [
|
|
135
|
+
TypesenseModule.forRootAsync({
|
|
136
|
+
inject: [ConfigService],
|
|
137
|
+
useFactory: (config: ConfigService) => ({
|
|
138
|
+
nodes: [{ host: config.getOrThrow('TYPESENSE_HOST'), port: 8108, protocol: 'http' }],
|
|
139
|
+
apiKey: config.getOrThrow('TYPESENSE_API_KEY'),
|
|
140
|
+
connectionTimeoutSeconds: 5,
|
|
141
|
+
collections: [videoCollection],
|
|
142
|
+
migrations: 'alter',
|
|
143
|
+
}),
|
|
144
|
+
}),
|
|
145
|
+
],
|
|
146
|
+
})
|
|
147
|
+
export class AppModule {}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`collections` accepts either declaration style — `defineCollection()` results,
|
|
151
|
+
`@TypesenseSchema()` classes, or a mix of both.
|
|
152
|
+
|
|
153
|
+
### Migration strategies
|
|
154
|
+
|
|
155
|
+
At bootstrap the module compares each declared collection against the live cluster.
|
|
156
|
+
|
|
157
|
+
| Strategy | Behaviour |
|
|
158
|
+
| ---------- | --------- |
|
|
159
|
+
| `off` | Never touches the cluster. |
|
|
160
|
+
| `create` | Creates missing collections; logs a warning on drift. **Default.** |
|
|
161
|
+
| `alter` | Also adds new fields and drops removed ones in place. |
|
|
162
|
+
| `recreate` | Drops and rebuilds on drift. **Destroys documents** — pair with a reindex. |
|
|
163
|
+
|
|
164
|
+
Start on `create`, move to `alter` once you trust it. Every collection also carries a stable
|
|
165
|
+
`hash` of its schema, computed independently of field declaration order.
|
|
166
|
+
|
|
167
|
+
One Typesense constraint leaks through `alter`: a **required** field cannot be added to a
|
|
168
|
+
collection that already holds documents, since the existing rows would have no value for
|
|
169
|
+
it. Rather than failing the bootstrap, the module logs a warning naming the field and
|
|
170
|
+
leaves the collection alone. Declare the field `optional: true`, or use `recreate` and
|
|
171
|
+
reindex.
|
|
172
|
+
|
|
173
|
+
## Sync from Postgres (or anything else)
|
|
174
|
+
|
|
175
|
+
Implement a collector. There are no change streams — each method is just a query.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { RegisterTypesenseCollector, type TypesenseCollector, type InferDocument } from 'nestjs-typesense'
|
|
179
|
+
import { videoCollection } from './video.collection.js'
|
|
180
|
+
|
|
181
|
+
const BATCH = 500
|
|
182
|
+
|
|
183
|
+
@RegisterTypesenseCollector(videoCollection)
|
|
184
|
+
export class VideoCollector implements TypesenseCollector<typeof videoCollection> {
|
|
185
|
+
constructor(private readonly repo: Repository<Video>) {}
|
|
186
|
+
|
|
187
|
+
transform(videos: Video[]): InferDocument<typeof videoCollection>[] {
|
|
188
|
+
return videos.map((v) => ({
|
|
189
|
+
id: v.id,
|
|
190
|
+
title: v.title,
|
|
191
|
+
durationMs: v.durationMs,
|
|
192
|
+
tags: v.tags,
|
|
193
|
+
}))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async *fetchAll(ids?: string[]) {
|
|
197
|
+
for (let skip = 0; ; skip += BATCH) {
|
|
198
|
+
const batch = await this.repo.find({
|
|
199
|
+
where: ids ? { id: In(ids) } : {},
|
|
200
|
+
order: { id: 'ASC' },
|
|
201
|
+
take: BATCH,
|
|
202
|
+
skip,
|
|
203
|
+
})
|
|
204
|
+
if (batch.length === 0) return
|
|
205
|
+
yield batch
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async *fetchChanged(since: Date) {
|
|
210
|
+
yield await this.repo.find({ where: { updatedAt: MoreThan(since) } })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async *fetchRemoved(since: Date) {
|
|
214
|
+
const rows = await this.repo.find({ where: { deletedAt: MoreThan(since) }, withDeleted: true })
|
|
215
|
+
yield rows.map((r) => r.id)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Register it as an ordinary provider — `@RegisterTypesenseCollector` applies `@Injectable()` for you.
|
|
221
|
+
|
|
222
|
+
Then drive it from a cron job or CLI command:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
constructor(private readonly indexer: TypesenseIndexer) {}
|
|
226
|
+
|
|
227
|
+
await this.indexer.reindex(videoCollection) // full rebuild
|
|
228
|
+
await this.indexer.sync(videoCollection, lastRunAt) // incremental
|
|
229
|
+
await this.indexer.syncAll(lastRunAt) // every collection with a collector
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Search
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
constructor(private readonly typesense: TypesenseClient) {}
|
|
236
|
+
|
|
237
|
+
const results = await this.typesense.search(videoCollection, {
|
|
238
|
+
q: 'mountain sunset',
|
|
239
|
+
query_by: 'title,description',
|
|
240
|
+
filter_by: 'durationMs:<60000',
|
|
241
|
+
per_page: 20,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
results.hits[0].document.title // typed as string
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`results` is a plain `{ found, page, hits, facets }` — shape your own API response from it.
|
|
248
|
+
For anything not covered, `typesense.raw` is the official client.
|
|
249
|
+
|
|
250
|
+
## Testing
|
|
251
|
+
|
|
252
|
+
Unit tests cover schema inference and hashing and need nothing running:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
npm test
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The integration suite exercises the client, the bootstrap migrator and the indexer
|
|
259
|
+
against a real Typesense server:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
docker compose up -d # or: brew services start typesense-server
|
|
263
|
+
npm run test:integration
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
It refuses to run rather than skipping when no server answers, so a green run always
|
|
267
|
+
means the server was hit. Point it elsewhere with `TYPESENSE_HOST`, `TYPESENSE_PORT`
|
|
268
|
+
and `TYPESENSE_API_KEY`.
|
|
269
|
+
|
|
270
|
+
It imports the built package rather than `src`, because vitest transpiles with esbuild,
|
|
271
|
+
which honours `experimentalDecorators` but drops `emitDecoratorMetadata` — Nest cannot
|
|
272
|
+
resolve constructor injection from source under that transform. `npm run
|
|
273
|
+
test:integration` builds first.
|
|
274
|
+
|
|
275
|
+
## Contributing
|
|
276
|
+
|
|
277
|
+
Checks run at three points, each a superset of the one before.
|
|
278
|
+
|
|
279
|
+
`bun install` installs [husky](https://typicode.github.io/husky/), which wires up two hooks:
|
|
280
|
+
|
|
281
|
+
| Hook | Runs | Cost |
|
|
282
|
+
| ---- | ---- | ---- |
|
|
283
|
+
| `pre-commit` | `biome check` on staged files only | sub-second |
|
|
284
|
+
| `pre-push` | build, typecheck, lint, format, unit tests | ~15s |
|
|
285
|
+
|
|
286
|
+
CI then repeats the `pre-push` set across Node 20, 22 and 24, and additionally
|
|
287
|
+
runs the integration suite against a real Typesense in Docker.
|
|
288
|
+
|
|
289
|
+
`pre-push` deliberately stops short of the integration suite: that suite throws
|
|
290
|
+
rather than skips when no server answers, so including it would block every push
|
|
291
|
+
made while your local Typesense is down. Run it yourself with
|
|
292
|
+
`npm run test:integration`. In a genuine emergency, `git push --no-verify`.
|
|
293
|
+
|
|
294
|
+
One wrinkle worth knowing if you add checks: the typecheck needs `dist` to
|
|
295
|
+
exist. `src/typesense.integration.test.ts` imports `../dist/index.js` by design,
|
|
296
|
+
so `tsc --noEmit` fails on a clean checkout until you have built once. Both the
|
|
297
|
+
hook and CI build first.
|
|
298
|
+
|
|
299
|
+
## Status
|
|
300
|
+
|
|
301
|
+
v0.1.0, CommonJS. ESM output can be added without a breaking change.
|
|
302
|
+
|
|
303
|
+
Verified against Typesense 29/30: collection creation, the `create`/`alter`/`recreate`
|
|
304
|
+
migration strategies, typed search with filtering, faceting and pagination, upsert and
|
|
305
|
+
delete, and full plus incremental indexing through collectors.
|
|
306
|
+
|
|
307
|
+
Known rough edges:
|
|
308
|
+
|
|
309
|
+
- `query_by`, `filter_by`, `sort_by` and `facet_by` are unchecked strings; a typo fails
|
|
310
|
+
at runtime, not at compile time.
|
|
311
|
+
- A collection reached through `resolveCollection()` or `TypesenseCollections.get()`
|
|
312
|
+
loses its field types, so `search` returns loosely typed documents. Import the
|
|
313
|
+
`defineCollection()` result directly to keep them.
|
|
314
|
+
- No multi-search, and no point lookup by id — drop to `client.raw` for both.
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT © Ebenezer Domey
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Client } from "typesense";
|
|
2
|
+
import type { InferDocument, TypesenseCollection } from "../schema/collection.js";
|
|
3
|
+
import type { TypesenseModuleOptions } from "../typesense.module-options.js";
|
|
4
|
+
export interface SearchParams {
|
|
5
|
+
q: string;
|
|
6
|
+
query_by: string;
|
|
7
|
+
filter_by?: string;
|
|
8
|
+
sort_by?: string;
|
|
9
|
+
facet_by?: string;
|
|
10
|
+
page?: number;
|
|
11
|
+
per_page?: number;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface SearchResult<TDocument> {
|
|
15
|
+
found: number;
|
|
16
|
+
page: number;
|
|
17
|
+
hits: {
|
|
18
|
+
document: TDocument;
|
|
19
|
+
highlights?: unknown[];
|
|
20
|
+
}[];
|
|
21
|
+
facets?: unknown[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thin wrapper over the official Typesense client, typed against collection definitions.
|
|
25
|
+
* Deliberately does not impose a pagination or error envelope — callers keep their own.
|
|
26
|
+
*/
|
|
27
|
+
export declare class TypesenseClient {
|
|
28
|
+
private readonly options;
|
|
29
|
+
private readonly logger;
|
|
30
|
+
readonly raw: Client;
|
|
31
|
+
constructor(options: TypesenseModuleOptions);
|
|
32
|
+
search<TCollection extends TypesenseCollection>(collection: TCollection, params: SearchParams): Promise<SearchResult<InferDocument<TCollection>>>;
|
|
33
|
+
upsert<TCollection extends TypesenseCollection>(collection: TCollection, documents: InferDocument<TCollection>[]): Promise<void>;
|
|
34
|
+
private reportImportFailures;
|
|
35
|
+
delete(collection: TypesenseCollection, ids: string[]): Promise<void>;
|
|
36
|
+
private handleError;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=typesense.client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typesense.client.d.ts","sourceRoot":"","sources":["../../src/client/typesense.client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAElF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAE7E,MAAM,WAAW,YAAY;IAC3B,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAQD,MAAM,WAAW,YAAY,CAAC,SAAS;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QAAE,QAAQ,EAAE,SAAS,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,EAAE,CAAA;KAAE,EAAE,CAAC;IACxD,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;CACpB;AAED;;;GAGG;AACH,qBACa,eAAe;IAIoB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHtE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;IAC3D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;gBAE0C,OAAO,EAAE,sBAAsB;IAKxF,MAAM,CAAC,WAAW,SAAS,mBAAmB,EAClD,UAAU,EAAE,WAAW,EACvB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC;IAc9C,MAAM,CAAC,WAAW,SAAS,mBAAmB,EAClD,UAAU,EAAE,WAAW,EACvB,SAAS,EAAE,aAAa,CAAC,WAAW,CAAC,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;IAqBhB,OAAO,CAAC,oBAAoB;IAWtB,MAAM,CAAC,UAAU,EAAE,mBAAmB,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAe3E,OAAO,CAAC,WAAW;CAOpB"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var TypesenseClient_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.TypesenseClient = void 0;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const typesense_1 = require("typesense");
|
|
19
|
+
const typesense_constants_js_1 = require("../typesense.constants.js");
|
|
20
|
+
/**
|
|
21
|
+
* Thin wrapper over the official Typesense client, typed against collection definitions.
|
|
22
|
+
* Deliberately does not impose a pagination or error envelope — callers keep their own.
|
|
23
|
+
*/
|
|
24
|
+
let TypesenseClient = TypesenseClient_1 = class TypesenseClient {
|
|
25
|
+
options;
|
|
26
|
+
logger = new common_1.Logger(TypesenseClient_1.name);
|
|
27
|
+
raw;
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
const { collections: _c, migrations: _m, onError: _e, ...config } = options;
|
|
31
|
+
this.raw = new typesense_1.Client(config);
|
|
32
|
+
}
|
|
33
|
+
async search(collection, params) {
|
|
34
|
+
const result = await this.raw
|
|
35
|
+
.collections(collection.name)
|
|
36
|
+
.documents()
|
|
37
|
+
.search(params);
|
|
38
|
+
return {
|
|
39
|
+
found: result.found,
|
|
40
|
+
page: result.page,
|
|
41
|
+
hits: (result.hits ?? []),
|
|
42
|
+
...(result.facet_counts ? { facets: result.facet_counts } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async upsert(collection, documents) {
|
|
46
|
+
if (documents.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
// typesense-js throws `ImportError` when any document is rejected rather than
|
|
49
|
+
// returning the per-document results, so the failure path lives in the catch.
|
|
50
|
+
// Transport errors carry no `importResults` and are rethrown — only per-document
|
|
51
|
+
// rejections are routed through `onError`.
|
|
52
|
+
try {
|
|
53
|
+
const results = await this.raw
|
|
54
|
+
.collections(collection.name)
|
|
55
|
+
.documents()
|
|
56
|
+
.import(documents, { action: "upsert" });
|
|
57
|
+
this.reportImportFailures(results, documents.length, collection.name);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const results = error.importResults;
|
|
61
|
+
if (!results)
|
|
62
|
+
throw error;
|
|
63
|
+
this.reportImportFailures(results, documents.length, collection.name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
reportImportFailures(results, total, name) {
|
|
67
|
+
const failures = results.filter((r) => !r.success);
|
|
68
|
+
if (failures.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
this.handleError(new Error(`${failures.length}/${total} documents failed to index into "${name}": ${failures[0]?.error}`));
|
|
71
|
+
}
|
|
72
|
+
async delete(collection, ids) {
|
|
73
|
+
if (ids.length === 0)
|
|
74
|
+
return;
|
|
75
|
+
await Promise.all(ids.map(async (id) => {
|
|
76
|
+
try {
|
|
77
|
+
await this.raw.collections(collection.name).documents(id).delete();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
// A document already gone is not a failure — the desired end state holds.
|
|
81
|
+
if (error.httpStatus !== 404)
|
|
82
|
+
this.handleError(error);
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
handleError(error) {
|
|
87
|
+
if (this.options.onError) {
|
|
88
|
+
this.options.onError(error);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.logger.error(error instanceof Error ? error.message : String(error));
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
exports.TypesenseClient = TypesenseClient;
|
|
95
|
+
exports.TypesenseClient = TypesenseClient = TypesenseClient_1 = __decorate([
|
|
96
|
+
(0, common_1.Injectable)(),
|
|
97
|
+
__param(0, (0, common_1.Inject)(typesense_constants_js_1.TYPESENSE_MODULE_OPTIONS)),
|
|
98
|
+
__metadata("design:paramtypes", [Object])
|
|
99
|
+
], TypesenseClient);
|
|
100
|
+
//# sourceMappingURL=typesense.client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typesense.client.js","sourceRoot":"","sources":["../../src/client/typesense.client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type OnApplicationBootstrap } from "@nestjs/common";
|
|
2
|
+
import { TypesenseClient } from "../client/typesense.client.js";
|
|
3
|
+
import { type TypesenseCollection } from "../schema/collection.js";
|
|
4
|
+
import type { TypesenseModuleOptions } from "../typesense.module-options.js";
|
|
5
|
+
/**
|
|
6
|
+
* Reconciles declared collections with the live Typesense cluster at bootstrap,
|
|
7
|
+
* according to the configured migration strategy.
|
|
8
|
+
*/
|
|
9
|
+
export declare class TypesenseCollections implements OnApplicationBootstrap {
|
|
10
|
+
private readonly options;
|
|
11
|
+
private readonly client;
|
|
12
|
+
private readonly logger;
|
|
13
|
+
private readonly byName;
|
|
14
|
+
constructor(options: TypesenseModuleOptions, client: TypesenseClient);
|
|
15
|
+
get(name: string): TypesenseCollection;
|
|
16
|
+
all(): TypesenseCollection[];
|
|
17
|
+
onApplicationBootstrap(): Promise<void>;
|
|
18
|
+
private reconcile;
|
|
19
|
+
private retrieve;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=typesense-collections.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typesense-collections.d.ts","sourceRoot":"","sources":["../../src/collections/typesense-collections.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACzF,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,KAAK,mBAAmB,EAAY,MAAM,yBAAyB,CAAC;AAG7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAE7E;;;GAGG;AACH,qBACa,oBAAqB,YAAW,sBAAsB;IAK7B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyC;IAChE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;gBAGZ,OAAO,EAAE,sBAAsB,EACjE,MAAM,EAAE,eAAe;IAQ1C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB;IAMtC,GAAG,IAAI,mBAAmB,EAAE;IAItB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;YAS/B,SAAS;YAiET,QAAQ;CAQvB"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var TypesenseCollections_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.TypesenseCollections = void 0;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const typesense_client_js_1 = require("../client/typesense.client.js");
|
|
19
|
+
const collection_js_1 = require("../schema/collection.js");
|
|
20
|
+
const decorators_js_1 = require("../schema/decorators.js");
|
|
21
|
+
const typesense_constants_js_1 = require("../typesense.constants.js");
|
|
22
|
+
/**
|
|
23
|
+
* Reconciles declared collections with the live Typesense cluster at bootstrap,
|
|
24
|
+
* according to the configured migration strategy.
|
|
25
|
+
*/
|
|
26
|
+
let TypesenseCollections = TypesenseCollections_1 = class TypesenseCollections {
|
|
27
|
+
options;
|
|
28
|
+
client;
|
|
29
|
+
logger = new common_1.Logger(TypesenseCollections_1.name);
|
|
30
|
+
byName = new Map();
|
|
31
|
+
constructor(options, client) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.client = client;
|
|
34
|
+
for (const source of options.collections ?? []) {
|
|
35
|
+
const collection = (0, decorators_js_1.resolveCollection)(source);
|
|
36
|
+
this.byName.set(collection.name, collection);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
get(name) {
|
|
40
|
+
const collection = this.byName.get(name);
|
|
41
|
+
if (!collection)
|
|
42
|
+
throw new Error(`Typesense collection "${name}" is not registered`);
|
|
43
|
+
return collection;
|
|
44
|
+
}
|
|
45
|
+
all() {
|
|
46
|
+
return [...this.byName.values()];
|
|
47
|
+
}
|
|
48
|
+
async onApplicationBootstrap() {
|
|
49
|
+
const strategy = this.options.migrations ?? "create";
|
|
50
|
+
if (strategy === "off")
|
|
51
|
+
return;
|
|
52
|
+
for (const collection of this.byName.values()) {
|
|
53
|
+
await this.reconcile(collection, strategy);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async reconcile(collection, strategy) {
|
|
57
|
+
const schema = (0, collection_js_1.toSchema)(collection);
|
|
58
|
+
const live = await this.retrieve(collection.name);
|
|
59
|
+
if (!live) {
|
|
60
|
+
await this.client.raw.collections().create(schema);
|
|
61
|
+
this.logger.log(`Created collection "${collection.name}" (${collection.hash})`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const liveNames = new Set((live.fields ?? []).map((f) => f.name));
|
|
65
|
+
const declaredNames = new Set(schema.fields.map((f) => f.name));
|
|
66
|
+
// `id` is implicit in Typesense: `toSchema` emits it, but `retrieve()` never lists it
|
|
67
|
+
// and `update()` rejects it outright ("Field `id` cannot be altered"). Skip it on both
|
|
68
|
+
// sides of the diff or every alter of a drifted collection fails with a 400.
|
|
69
|
+
const added = schema.fields.filter((f) => f.name !== "id" && !liveNames.has(f.name));
|
|
70
|
+
const removed = [...liveNames].filter((n) => n !== "id" && !declaredNames.has(n));
|
|
71
|
+
if (added.length === 0 && removed.length === 0)
|
|
72
|
+
return;
|
|
73
|
+
const drift = `+${added.length} -${removed.length} field(s)`;
|
|
74
|
+
if (strategy === "create") {
|
|
75
|
+
this.logger.warn(`Collection "${collection.name}" has drifted (${drift}). Strategy is "create" — not altering. Set migrations: "alter" to apply.`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (strategy === "recreate") {
|
|
79
|
+
await this.client.raw.collections(collection.name).delete();
|
|
80
|
+
await this.client.raw.collections().create(schema);
|
|
81
|
+
this.logger.warn(`Recreated collection "${collection.name}" (${drift}). Documents dropped — reindex required.`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Typesense refuses to add a required field to a collection that already holds
|
|
85
|
+
// documents — the existing rows would have no value for it. Catch it here so the
|
|
86
|
+
// failure names the fix instead of surfacing as a bare 400 during bootstrap.
|
|
87
|
+
const documents = live.num_documents ?? 0;
|
|
88
|
+
const required = added.filter((f) => !f.optional).map((f) => f.name);
|
|
89
|
+
if (documents > 0 && required.length > 0) {
|
|
90
|
+
// Warn and skip rather than throw, matching the "create" branch above: unappliable
|
|
91
|
+
// drift should not take an app down at bootstrap.
|
|
92
|
+
this.logger.warn(`Collection "${collection.name}" not altered: ${required.join(", ")} ` +
|
|
93
|
+
`${required.length === 1 ? "is" : "are"} required, and the collection already holds ` +
|
|
94
|
+
`${documents} document(s) with no value for ${required.length === 1 ? "it" : "them"}. ` +
|
|
95
|
+
'Declare the field optional, or use migrations: "recreate" and reindex.');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await this.client.raw.collections(collection.name).update({
|
|
99
|
+
fields: [...removed.map((name) => ({ name, drop: true })), ...added],
|
|
100
|
+
});
|
|
101
|
+
this.logger.log(`Altered collection "${collection.name}" (${drift})`);
|
|
102
|
+
}
|
|
103
|
+
async retrieve(name) {
|
|
104
|
+
try {
|
|
105
|
+
return await this.client.raw.collections(name).retrieve();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.httpStatus === 404)
|
|
109
|
+
return undefined;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
exports.TypesenseCollections = TypesenseCollections;
|
|
115
|
+
exports.TypesenseCollections = TypesenseCollections = TypesenseCollections_1 = __decorate([
|
|
116
|
+
(0, common_1.Injectable)(),
|
|
117
|
+
__param(0, (0, common_1.Inject)(typesense_constants_js_1.TYPESENSE_MODULE_OPTIONS)),
|
|
118
|
+
__metadata("design:paramtypes", [Object, typesense_client_js_1.TypesenseClient])
|
|
119
|
+
], TypesenseCollections);
|
|
120
|
+
//# sourceMappingURL=typesense-collections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typesense-collections.js","sourceRoot":"","sources":["../../src/collections/typesense-collections.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { InferDocument, TypesenseCollection } from "../schema/collection.js";
|
|
2
|
+
/**
|
|
3
|
+
* Feeds documents into a collection.
|
|
4
|
+
*
|
|
5
|
+
* This is deliberately pull-based rather than driven by database change events, so it
|
|
6
|
+
* works over any store. With Postgres each method is an ordinary query — `fetchChanged`
|
|
7
|
+
* is a `WHERE updated_at > $1`, `fetchRemoved` reads your soft-delete or tombstone table.
|
|
8
|
+
*
|
|
9
|
+
* Every method yields batches so a full reindex streams instead of loading the table
|
|
10
|
+
* into memory.
|
|
11
|
+
*/
|
|
12
|
+
export interface TypesenseCollector<TCollection extends TypesenseCollection = TypesenseCollection> {
|
|
13
|
+
/** Map a batch of source rows to Typesense documents. */
|
|
14
|
+
transform(entities: unknown[]): InferDocument<TCollection>[];
|
|
15
|
+
/** Every row, for a full rebuild. Optionally narrowed to specific ids. */
|
|
16
|
+
fetchAll(ids?: string[]): AsyncGenerator<unknown[], void, void>;
|
|
17
|
+
/** Rows created or updated since `since`, for an incremental pass. */
|
|
18
|
+
fetchChanged(since: Date): AsyncGenerator<unknown[], void, void>;
|
|
19
|
+
/** Ids deleted since `since`. Omit if the source never hard-deletes. */
|
|
20
|
+
fetchRemoved?(since: Date): AsyncGenerator<string[], void, void>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=typesense-collector.d.ts.map
|