@m1212e/rumble 0.13.2 → 0.14.1
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/README.md +64 -1
- package/out/index.cjs +25 -21
- package/out/index.cjs.map +1 -1
- package/out/index.d.cts +7 -11
- package/out/index.d.cts.map +1 -1
- package/out/index.d.mts +7 -11
- package/out/index.d.mts.map +1 -1
- package/out/index.mjs +25 -21
- package/out/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -210,6 +210,69 @@ const enumRef = enum_({
|
|
|
210
210
|
```
|
|
211
211
|
> The enum parameter allows other fields to be used to reference an enum. This is largely due to how this is used internally. Because of the way how drizzle handles enums, we are not able to provide type safety with enums. In case you actually need to use it, the above way is the recommended approach.
|
|
212
212
|
|
|
213
|
+
### Enable search
|
|
214
|
+
> Search is currently only supported in postgres!
|
|
215
|
+
|
|
216
|
+
rumble and its helpers offer out of the box search capabilities. You can activate this functionality by passing the `search` parameter to the `createRumble` function.
|
|
217
|
+
```ts
|
|
218
|
+
const rumble = createRumble({
|
|
219
|
+
...
|
|
220
|
+
search: {
|
|
221
|
+
enabled: true,
|
|
222
|
+
threshold: 0.2, // optionally adjust this value to your needs
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
This will add a search field to all list queries and many relations created by the rumble helper functions. E.g. if you have a `users` table with a `name` and `email` field, and use the `object` and `query` helpers to implement queries for this table, you will get a search argument like this:
|
|
227
|
+
```graphql
|
|
228
|
+
{
|
|
229
|
+
users(limit: 10, search: "alice") {
|
|
230
|
+
id
|
|
231
|
+
name
|
|
232
|
+
email
|
|
233
|
+
search_distance
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
Additionally, a search distance will be returned for each result if the search argument is used (null otherwise). The results are returned sorted by their distance with the best matching fields in the first place. The search will respect all text fields (IDs included) on the table, you always search for all text columns at once. Matches in multiple columns stack and therefore result in a better matching score. So if you search for "alice" and there is a user with the name "Alice" and an email "alice@example.com", the two matching columns will result in a better score than e.g. "al007@example.com"/"Alice".
|
|
238
|
+
> If you have abilities in place which prevent a caller from accessing certain columns, the search will not respect those columns to prevent leaking information.
|
|
239
|
+
#### Pro tip: Indexing
|
|
240
|
+
In case your table grows large, it can be a good idea to create an index to increase performance. Under the hood, searching uses the [pg_trgm](https://www.postgresql.org/docs/current/pgtrgm.html#PGTRGM-INDEX) extension. To create indexes for our searchable columns, we can adjust our drizzle schema accordingly:
|
|
241
|
+
```ts
|
|
242
|
+
export const user = pgTable('user', {
|
|
243
|
+
id: text()
|
|
244
|
+
.$defaultFn(() => nanoid())
|
|
245
|
+
.primaryKey(),
|
|
246
|
+
email: text().notNull().unique(),
|
|
247
|
+
name: text().notNull(),
|
|
248
|
+
},
|
|
249
|
+
(table) => [
|
|
250
|
+
index('user_id_trgm')
|
|
251
|
+
.using('gin', sql`${table.id} gin_trgm_ops`)
|
|
252
|
+
.concurrently(),
|
|
253
|
+
index('user_email_trgm')
|
|
254
|
+
.using('gin', sql`${table.email} gin_trgm_ops`)
|
|
255
|
+
.concurrently(),
|
|
256
|
+
index('user_name_trgm')
|
|
257
|
+
.using('gin', sql`${table.name} gin_trgm_ops`)
|
|
258
|
+
.concurrently(),
|
|
259
|
+
],
|
|
260
|
+
);
|
|
261
|
+
```
|
|
262
|
+
> When deciding to create indexes for faster searches, ensure that you create them for all text based columns that exist in your table. This includes `text`, `char`, `varchar` and so on. Since rumble always searches all the text columns, it is recommended to create indexes for all text columns in your table, otherwise the search might not be as fast as possible.
|
|
263
|
+
|
|
264
|
+
In case you never started rumble with the search mode activated and you want to create the indexes in your migration, please ensure that the `pg_trgm` extension is installed and enabled in your database. You can do this by running the following SQL command:
|
|
265
|
+
```sql
|
|
266
|
+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
|
267
|
+
```
|
|
268
|
+
rumble does this automatically on startup if the search feature is enabled, nonetheless it is recommended to include the extension installation in your migration scripts to ensure that the extension is available when you create the indexes which rely on it.
|
|
269
|
+
|
|
270
|
+
**What this means for you**: After creating the migration files containing the indexes the first time with the `drizzle-kit generate` command (or your respective migration tool), add the above SQL statement before any index creation inside that generated migration file. This will ensure that the extension is installed and enabled before the indexes that rely on it are created.
|
|
271
|
+
|
|
272
|
+
Another important aspect to consider when using indexes is that the PostgreSQL instance should be properly configured for optimal performance (e.g. the drive type you use might have an impact). This includes tuning settings such as `random_page_cost` and `effective_io_concurrency`. Please see [this quick writeup on the topic](https://blog.frehi.be/2025/07/28/tuning-postgresql-performance-for-ssd). If not done properly, your indexes might not be used as expected rendering them useless.
|
|
273
|
+
|
|
274
|
+
Please also note that `GIST` indexes are also supported by `pg_trgm` but will oftentimes not be useable for the sorting since a lot of columns are accumulated in the query which causes the query planner to not use them. Feel free to experiment with different index types and configurations to find the best fit for your specific use case.
|
|
275
|
+
|
|
213
276
|
### and more...
|
|
214
277
|
See [the example file](./example/src/main.ts) or clone it and let intellisense guide you. Rumble offers various other helpers which might become handy!
|
|
215
278
|
|
|
@@ -288,4 +351,4 @@ await generateFromSchema({
|
|
|
288
351
|
outputPath: "./generated";
|
|
289
352
|
})
|
|
290
353
|
```
|
|
291
|
-
This might become handy in separate code bases for api and client.
|
|
354
|
+
This might become handy in separate code bases for api and client.
|
package/out/index.cjs
CHANGED
|
@@ -1357,12 +1357,18 @@ function isPostgresDB(db) {
|
|
|
1357
1357
|
|
|
1358
1358
|
//#endregion
|
|
1359
1359
|
//#region lib/search.ts
|
|
1360
|
-
async function initSearchIfApplicable(
|
|
1361
|
-
if (!isPostgresDB(db)) {
|
|
1362
|
-
console.info("Database dialect is not compatible with search, skipping search initialization.");
|
|
1360
|
+
async function initSearchIfApplicable(input) {
|
|
1361
|
+
if (!isPostgresDB(input.db)) {
|
|
1362
|
+
console.info("Database dialect is not compatible with search, skipping search initialization. Only PostgreSQL is supported.");
|
|
1363
1363
|
return;
|
|
1364
1364
|
}
|
|
1365
|
-
await db.execute(drizzle_orm.sql`CREATE EXTENSION IF NOT EXISTS pg_trgm;`);
|
|
1365
|
+
await input.db.execute(drizzle_orm.sql`CREATE EXTENSION IF NOT EXISTS pg_trgm;`);
|
|
1366
|
+
if (input.search?.threshold) {
|
|
1367
|
+
const threshold = Number(input.search.threshold);
|
|
1368
|
+
if (threshold < 0 || threshold > 1) throw new Error(`Search threshold must be between 0 and 1`);
|
|
1369
|
+
const dbName = (await input.db.execute(drizzle_orm.sql`SELECT current_database()`)).rows[0].current_database;
|
|
1370
|
+
await input.db.execute(drizzle_orm.sql.raw(`ALTER DATABASE ${dbName} SET pg_trgm.similarity_threshold = ${threshold};`));
|
|
1371
|
+
}
|
|
1366
1372
|
}
|
|
1367
1373
|
/**
|
|
1368
1374
|
* Performs adjustments to the query args to issue a full text search in case the
|
|
@@ -1370,14 +1376,11 @@ async function initSearchIfApplicable(db) {
|
|
|
1370
1376
|
*/
|
|
1371
1377
|
function adjustQueryArgsForSearch({ search, args, tableSchema, abilities }) {
|
|
1372
1378
|
if (search?.enabled && args.search && args.search.length > 0) {
|
|
1373
|
-
const columnsToSearch = abilities.query.many.columns ? Object.entries(tableSchema.columns).filter(([key]) => abilities.query.many.columns[key]) : Object.entries(tableSchema.columns);
|
|
1379
|
+
const columnsToSearch = (abilities.query.many.columns ? Object.entries(tableSchema.columns).filter(([key]) => abilities.query.many.columns[key]) : Object.entries(tableSchema.columns)).filter(([key, col]) => isStringLikeSQLTypeString(col.getSQLType()) || isIDLikeSQLTypeString(col.getSQLType()));
|
|
1374
1380
|
const searchParam = drizzle_orm.sql`${args.search}`;
|
|
1375
|
-
|
|
1376
|
-
return drizzle_orm.sql`COALESCE(
|
|
1377
|
-
}), drizzle_orm.sql.raw(" + "))}
|
|
1378
|
-
return drizzle_orm.sql`similarity(${table[key]}::TEXT, ${searchParam})`;
|
|
1379
|
-
}), drizzle_orm.sql.raw(", "))})`;
|
|
1380
|
-
args.extras = { search_score: scoring };
|
|
1381
|
+
args.extras = { search_distance: (table) => drizzle_orm.sql`${drizzle_orm.sql.join(columnsToSearch.map(([key]) => {
|
|
1382
|
+
return drizzle_orm.sql`COALESCE((${table[key]}::TEXT <-> ${searchParam}), 1)`;
|
|
1383
|
+
}), drizzle_orm.sql.raw(" + "))}` };
|
|
1381
1384
|
const originalOrderBy = (0, es_toolkit.cloneDeep)(args.orderBy);
|
|
1382
1385
|
args.orderBy = (table) => {
|
|
1383
1386
|
const argsOrderBySQL = drizzle_orm.sql.join(Object.entries(originalOrderBy ?? {}).map(([key, value]) => {
|
|
@@ -1385,12 +1388,12 @@ function adjustQueryArgsForSearch({ search, args, tableSchema, abilities }) {
|
|
|
1385
1388
|
else if (value === "desc") return drizzle_orm.sql`${table[key]} DESC`;
|
|
1386
1389
|
else throw new Error(`Invalid value ${value} for orderBy`);
|
|
1387
1390
|
}), drizzle_orm.sql.raw(", "));
|
|
1388
|
-
const searchSQL = drizzle_orm.sql`
|
|
1391
|
+
const searchSQL = drizzle_orm.sql`search_distance ASC`;
|
|
1389
1392
|
return originalOrderBy ? drizzle_orm.sql.join([argsOrderBySQL, searchSQL], drizzle_orm.sql.raw(", ")) : searchSQL;
|
|
1390
1393
|
};
|
|
1391
|
-
args.where = { AND: [(0, es_toolkit.cloneDeep)(args.where) ?? {}, { RAW: (table) => {
|
|
1392
|
-
return drizzle_orm.sql`${
|
|
1393
|
-
} }] };
|
|
1394
|
+
args.where = { AND: [(0, es_toolkit.cloneDeep)(args.where) ?? {}, { RAW: (table) => drizzle_orm.sql`(${drizzle_orm.sql.join(columnsToSearch.map(([key]) => {
|
|
1395
|
+
return drizzle_orm.sql`${table[key]} % ${searchParam}`;
|
|
1396
|
+
}), drizzle_orm.sql.raw(" OR "))})` }] };
|
|
1394
1397
|
}
|
|
1395
1398
|
}
|
|
1396
1399
|
|
|
@@ -1551,11 +1554,11 @@ const createObjectImplementer = ({ db, search, schemaBuilder, makePubSubInstance
|
|
|
1551
1554
|
return acc;
|
|
1552
1555
|
}, {});
|
|
1553
1556
|
if (search?.enabled) {
|
|
1554
|
-
if (fields.
|
|
1555
|
-
fields.
|
|
1556
|
-
description: "The search
|
|
1557
|
+
if (fields.search_distance) throw new Error("Reserved field name 'search_distance' found on " + tableSchema.tsName + ". If search is enabled, the 'search_distance' field is automatically added and cannot be defined manually.");
|
|
1558
|
+
fields.search_distance = t.float({
|
|
1559
|
+
description: "The search distance of the object. If a search is provided, this field will be populated with the search distance.",
|
|
1557
1560
|
nullable: true,
|
|
1558
|
-
resolve: (parent, args, ctx, info) => parent.
|
|
1561
|
+
resolve: (parent, args, ctx, info) => parent.search_distance
|
|
1559
1562
|
});
|
|
1560
1563
|
}
|
|
1561
1564
|
return {
|
|
@@ -1890,7 +1893,8 @@ const createSchemaBuilder = ({ db, disableDefaultObjects, pubsub, pothosConfig }
|
|
|
1890
1893
|
},
|
|
1891
1894
|
smartSubscriptions: { ...(0, __pothos_plugin_smart_subscriptions.subscribeOptionsFromIterator)((name, _context) => {
|
|
1892
1895
|
return pubsub.subscribe(name);
|
|
1893
|
-
}) }
|
|
1896
|
+
}) },
|
|
1897
|
+
defaultFieldNullability: false
|
|
1894
1898
|
});
|
|
1895
1899
|
schemaBuilder.addScalarType("JSON", graphql_scalars.JSONResolver);
|
|
1896
1900
|
schemaBuilder.addScalarType("Date", graphql_scalars.DateResolver);
|
|
@@ -1937,7 +1941,7 @@ export const db = drizzle(
|
|
|
1937
1941
|
"delete"
|
|
1938
1942
|
];
|
|
1939
1943
|
if (rumbleInput.defaultLimit === void 0) rumbleInput.defaultLimit = 100;
|
|
1940
|
-
if (rumbleInput.search?.enabled) initSearchIfApplicable(rumbleInput
|
|
1944
|
+
if (rumbleInput.search?.enabled) initSearchIfApplicable(rumbleInput);
|
|
1941
1945
|
const abilityBuilder = createAbilityBuilder(rumbleInput);
|
|
1942
1946
|
const context = createContextFunction({
|
|
1943
1947
|
...rumbleInput,
|