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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/dist/client/typesense.client.d.ts +38 -0
  4. package/dist/client/typesense.client.d.ts.map +1 -0
  5. package/dist/client/typesense.client.js +100 -0
  6. package/dist/client/typesense.client.js.map +1 -0
  7. package/dist/collections/typesense-collections.d.ts +21 -0
  8. package/dist/collections/typesense-collections.d.ts.map +1 -0
  9. package/dist/collections/typesense-collections.js +120 -0
  10. package/dist/collections/typesense-collections.js.map +1 -0
  11. package/dist/collectors/typesense-collector.d.ts +22 -0
  12. package/dist/collectors/typesense-collector.d.ts.map +1 -0
  13. package/dist/collectors/typesense-collector.decorator.d.ts +11 -0
  14. package/dist/collectors/typesense-collector.decorator.d.ts.map +1 -0
  15. package/dist/collectors/typesense-collector.decorator.js +27 -0
  16. package/dist/collectors/typesense-collector.decorator.js.map +1 -0
  17. package/dist/collectors/typesense-collector.js +3 -0
  18. package/dist/collectors/typesense-collector.js.map +1 -0
  19. package/dist/collectors/typesense-indexer.d.ts +34 -0
  20. package/dist/collectors/typesense-indexer.d.ts.map +1 -0
  21. package/dist/collectors/typesense-indexer.js +110 -0
  22. package/dist/collectors/typesense-indexer.js.map +1 -0
  23. package/dist/index.d.ts +18 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +44 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/schema/collection.d.ts +87 -0
  28. package/dist/schema/collection.d.ts.map +1 -0
  29. package/dist/schema/collection.js +64 -0
  30. package/dist/schema/collection.js.map +1 -0
  31. package/dist/schema/decorators.d.ts +73 -0
  32. package/dist/schema/decorators.d.ts.map +1 -0
  33. package/dist/schema/decorators.js +126 -0
  34. package/dist/schema/decorators.js.map +1 -0
  35. package/dist/schema/field.d.ts +71 -0
  36. package/dist/schema/field.d.ts.map +1 -0
  37. package/dist/schema/field.js +40 -0
  38. package/dist/schema/field.js.map +1 -0
  39. package/dist/typesense.constants.d.ts +9 -0
  40. package/dist/typesense.constants.d.ts.map +1 -0
  41. package/dist/typesense.constants.js +12 -0
  42. package/dist/typesense.constants.js.map +1 -0
  43. package/dist/typesense.module-options.d.ts +24 -0
  44. package/dist/typesense.module-options.d.ts.map +1 -0
  45. package/dist/typesense.module-options.js +3 -0
  46. package/dist/typesense.module-options.js.map +1 -0
  47. package/dist/typesense.module.d.ts +13 -0
  48. package/dist/typesense.module.d.ts.map +1 -0
  49. package/dist/typesense.module.js +42 -0
  50. package/dist/typesense.module.js.map +1 -0
  51. 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