@vibeorm/extensions 2.0.0-alpha.1 → 2.0.0-alpha.10

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 CHANGED
@@ -85,6 +85,74 @@ Two gates worth knowing before you pick a dialect:
85
85
  - `engine: "bm25"` is refused on `pglite` (it does not bundle `pg_search`) and on non-postgres dialects. `"auto"` never selects it, because ParadeDB availability is not knowable at generate time.
86
86
  - The `fts5` engine requires the searched model to have a single `Int` or `BigInt` `@id`. That is the only primary key that aliases and pins the SQLite rowid the external-content table keys on; anything else is refused at generate time.
87
87
 
88
+ ### Type-ahead (prefix) matching
89
+
90
+ All engines match complete words by default — a user still typing gets nothing for `"piz"`. For search-as-you-type boxes pass `prefix: true`:
91
+
92
+ ```ts
93
+ const hits = await db.post.search({ query: "piz", prefix: true, take: 8 });
94
+ ```
95
+
96
+ The query is split into unicode letter/number tokens (punctuation separates and never matches) and every token matches as a prefix. Per engine: tsvector builds `to_tsquery('lang', 'piz:*')` (still one bound parameter — `websearch_to_tsquery` has no prefix syntax), fts5 uses `"piz"*` in the MATCH string, mysql switches to `+piz*` in BOOLEAN MODE. A query with no tokens resolves to `[]` without touching the database. Three caveats:
97
+
98
+ - tsvector prefixes run against **stemmed** lexemes, so a stemming `language` ("english", "norwegian") can miss mid-stem prefixes. Declare `language: "simple"` on models meant for type-ahead.
99
+ - `engine: "bm25"` refuses `prefix` (`VIBE_UNSUPPORTED_CAPABILITY`) — ParadeDB prefix behavior is decided by the index tokenizer, which this extension does not configure.
100
+ - mysql InnoDB ignores tokens shorter than `innodb_ft_min_token_size` (default 3), so one- and two-letter prefixes match nothing there.
101
+
102
+ ### Filtering hits: `where` composes with the core filter language
103
+
104
+ `search({ where })` compiles through the same filter compiler as every core query — scalar
105
+ filters, relation filters and `dbNow()` comparisons all AND into the match. The menu
106
+ composition (type-ahead over items that are orderable *right now*):
107
+
108
+ ```prisma
109
+ model MenuItem {
110
+ id Int @id @default(autoincrement())
111
+ name String
112
+ description String
113
+ availableFrom DateTime?
114
+ availableTo DateTime?
115
+ category Category? @relation(fields: [categoryId], references: [id])
116
+ categoryId Int?
117
+
118
+ @@search([name, description], weights: [2, 1], language: "simple")
119
+ }
120
+ ```
121
+
122
+ ```ts
123
+ import { dbNow } from "@vibeorm/runtime";
124
+
125
+ const hits = await db.menuItem.search({
126
+ query: "piz", // mid-keystroke
127
+ prefix: true,
128
+ take: 8,
129
+ where: {
130
+ category: { is: { active: true } },
131
+ availableFrom: { lte: dbNow() }, // the DATABASE's clock, not the process's
132
+ availableTo: { gte: dbNow() },
133
+ },
134
+ });
135
+ ```
136
+
137
+ For typo tolerance ("piza") declare `@@trgm([name])` too and fall back to
138
+ `findMany({ where: { name: { similarTo: query } } })` when a prefix search returns nothing
139
+ (postgres/pglite only).
140
+
141
+ ### Hydrating hits with relations
142
+
143
+ `search()` returns bare rows — it has no `include`/`select` of relations. The intended pattern is the `select: "id"` projection plus a re-read through your own typed query, which carries relations, `omit`, computed fields and everything else the core API does:
144
+
145
+ ```ts
146
+ const hits = await db.post.search({ query: "piz", prefix: true, select: "id", take: 8 });
147
+ const rows = await db.post.findMany({
148
+ where: { id: { in: hits.map((hit) => hit.row.id) } },
149
+ include: { author: { select: { id: true, name: true } } },
150
+ });
151
+ // findMany does not preserve rank — re-order by score:
152
+ const byId = new Map(rows.map((row) => [row.id, row]));
153
+ const ranked = hits.flatMap((hit) => byId.get(hit.row.id) ?? []);
154
+ ```
155
+
88
156
  `createSearchDelegate(config)` is the runtime glue the generator emits into the client. You do not call it by hand.
89
157
 
90
158
  ## Vector
package/dist/index.js CHANGED
@@ -74,7 +74,7 @@ async function pgExtensionCheck(params) {
74
74
  }
75
75
 
76
76
  // packages/extensions/src/search/index.ts
77
- var VERSION = "2.0.0-alpha.0";
77
+ var VERSION = "2.0.0-alpha.1";
78
78
  var SCORE_ALIAS = "_vibe_score";
79
79
  var HIGHLIGHT_ALIAS = "_vibe_highlight";
80
80
  var WEIGHT_LETTERS = ["A", "B", "C", "D"];
@@ -101,6 +101,10 @@ function resolveSearchEngine(params) {
101
101
  hint: `this dialect supports "${native}"${dialect === "postgres" ? ' and "bm25"' : ""}`
102
102
  });
103
103
  }
104
+ function declRank(declaration) {
105
+ const value = declaration["rank"];
106
+ return value === "ts_rank" || value === "ts_rank_cd" ? value : undefined;
107
+ }
104
108
  function readDeclaration(params) {
105
109
  const fields = declStringArray(params.declaration, "fields");
106
110
  if (fields === undefined || fields.length === 0)
@@ -109,7 +113,10 @@ function readDeclaration(params) {
109
113
  fields,
110
114
  weights: declNumberArray(params.declaration, "weights"),
111
115
  base: declString(params.declaration, "name") ?? `${tableNameOf({ model: params.model })}_search`,
112
- language: declString(params.declaration, "language")
116
+ language: declString(params.declaration, "language"),
117
+ columnName: declString(params.declaration, "columnName"),
118
+ indexName: declString(params.declaration, "indexName"),
119
+ rank: declRank(params.declaration)
113
120
  };
114
121
  }
115
122
  function weightLetters(params) {
@@ -119,6 +126,41 @@ function weightLetters(params) {
119
126
  const ranked = [...new Set(decl.weights)].sort((a, b) => b - a);
120
127
  return decl.weights.map((weight) => WEIGHT_LETTERS[Math.min(ranked.indexOf(weight), WEIGHT_LETTERS.length - 1)]);
121
128
  }
129
+ function resolveArtifactNames(params) {
130
+ const { decl, engine, dialect } = params;
131
+ if (engine === "fts5" && (decl.columnName !== undefined || decl.indexName !== undefined)) {
132
+ throw unsupportedCapability({
133
+ dialect,
134
+ feature: "@@search columnName/indexName overrides on the fts5 engine",
135
+ hint: "fts5 emits a shadow table + triggers \u2014 there is no column or index to rename; use name: to change the table base"
136
+ });
137
+ }
138
+ if (decl.columnName !== undefined && engine !== "tsvector") {
139
+ throw unsupportedCapability({
140
+ dialect,
141
+ feature: `@@search columnName on the ${engine} engine`,
142
+ hint: "only the tsvector engine materializes a generated column \u2014 drop columnName or target tsvector"
143
+ });
144
+ }
145
+ return {
146
+ tsvColumn: decl.columnName ?? `${decl.base}_tsv`,
147
+ indexName: decl.indexName ?? `${decl.base}_idx`
148
+ };
149
+ }
150
+ function resolveRank(params) {
151
+ const { decl, options, engine, dialect } = params;
152
+ if (decl.rank !== undefined && engine !== "tsvector") {
153
+ throw unsupportedCapability({
154
+ dialect,
155
+ feature: `@@search rank on the ${engine} engine`,
156
+ hint: "rank chooses between the postgres ranking functions ts_rank and ts_rank_cd \u2014 a tsvector-only concept"
157
+ });
158
+ }
159
+ if (engine !== "tsvector")
160
+ return;
161
+ const fallback = options["rank"];
162
+ return decl.rank ?? (fallback === "ts_rank" || fallback === "ts_rank_cd" ? fallback : undefined);
163
+ }
122
164
  function tsvectorExpression(params) {
123
165
  const { dialect, model, decl, language } = params;
124
166
  const quote = (name) => dialect.quoteIdent({ name });
@@ -152,9 +194,10 @@ function searchArtifacts(ctx) {
152
194
  continue;
153
195
  const table = tableNameOf({ model });
154
196
  const columns = decl.fields.map((field) => columnNameOf({ model, field }));
197
+ resolveRank({ decl, options: ctx.options, engine, dialect: ctx.dialect });
155
198
  if (engine === "tsvector") {
156
199
  const language = decl.language ?? ctx.options["language"] ?? "english";
157
- const tsvColumn = `${decl.base}_tsv`;
200
+ const { tsvColumn, indexName } = resolveArtifactNames({ decl, engine, dialect: ctx.dialect });
158
201
  const expression = tsvectorExpression({ dialect, model, decl, language });
159
202
  artifacts.push({
160
203
  id: `search:${modelName}:column`,
@@ -165,11 +208,11 @@ function searchArtifacts(ctx) {
165
208
  drop: [`ALTER TABLE ${quote(table)} DROP COLUMN IF EXISTS ${quote(tsvColumn)};`]
166
209
  }, {
167
210
  id: `search:${modelName}:index`,
168
- owns: { kind: "index", table, name: `${decl.base}_idx` },
211
+ owns: { kind: "index", table, name: indexName },
169
212
  create: [
170
- `CREATE INDEX IF NOT EXISTS ${quote(`${decl.base}_idx`)} ON ${quote(table)} USING gin (${quote(tsvColumn)});`
213
+ `CREATE INDEX IF NOT EXISTS ${quote(indexName)} ON ${quote(table)} USING gin (${quote(tsvColumn)});`
171
214
  ],
172
- drop: [`DROP INDEX IF EXISTS ${quote(`${decl.base}_idx`)};`]
215
+ drop: [`DROP INDEX IF EXISTS ${quote(indexName)};`]
173
216
  });
174
217
  continue;
175
218
  }
@@ -183,17 +226,19 @@ function searchArtifacts(ctx) {
183
226
  });
184
227
  }
185
228
  const pkColumn = columnNameOf({ model, field: pkField });
229
+ const bm25Names = resolveArtifactNames({ decl, engine, dialect: ctx.dialect });
186
230
  artifacts.push({
187
231
  id: `search:${modelName}:index`,
188
- owns: { kind: "index", table, name: `${decl.base}_idx` },
232
+ owns: { kind: "index", table, name: bm25Names.indexName },
189
233
  create: [
190
- `CREATE INDEX IF NOT EXISTS ${quote(`${decl.base}_idx`)} ON ${quote(table)} USING bm25 (${[pkColumn, ...columns].map((column) => quote(column)).join(", ")}) WITH (key_field='${pkColumn}');`
234
+ `CREATE INDEX IF NOT EXISTS ${quote(bm25Names.indexName)} ON ${quote(table)} USING bm25 (${[pkColumn, ...columns].map((column) => quote(column)).join(", ")}) WITH (key_field='${pkColumn}');`
191
235
  ],
192
- drop: [`DROP INDEX IF EXISTS ${quote(`${decl.base}_idx`)};`]
236
+ drop: [`DROP INDEX IF EXISTS ${quote(bm25Names.indexName)};`]
193
237
  });
194
238
  continue;
195
239
  }
196
240
  if (engine === "fts5") {
241
+ resolveArtifactNames({ decl, engine, dialect: ctx.dialect });
197
242
  const pkName = primaryKeyFieldOf({ model });
198
243
  const pkField = pkName === null ? undefined : model.fields.find((field) => field.name === pkName);
199
244
  const pkIsRowidAlias = pkField !== undefined && !pkField.isList && (pkField.type === "Int" || pkField.type === "BigInt");
@@ -242,13 +287,14 @@ END;`
242
287
  });
243
288
  continue;
244
289
  }
290
+ const fulltextNames = resolveArtifactNames({ decl, engine, dialect: ctx.dialect });
245
291
  artifacts.push({
246
292
  id: `search:${modelName}:index`,
247
- owns: { kind: "index", table, name: `${decl.base}_idx` },
293
+ owns: { kind: "index", table, name: fulltextNames.indexName },
248
294
  create: [
249
- `CREATE FULLTEXT INDEX ${quote(`${decl.base}_idx`)} ON ${quote(table)} (${columns.map((column) => quote(column)).join(", ")});`
295
+ `CREATE FULLTEXT INDEX ${quote(fulltextNames.indexName)} ON ${quote(table)} (${columns.map((column) => quote(column)).join(", ")});`
250
296
  ],
251
- drop: [`DROP INDEX ${quote(`${decl.base}_idx`)} ON ${quote(table)};`]
297
+ drop: [`DROP INDEX ${quote(fulltextNames.indexName)} ON ${quote(table)};`]
252
298
  });
253
299
  }
254
300
  return artifacts;
@@ -294,9 +340,33 @@ function createSearchDelegate(config) {
294
340
  meta: { table: config.table }
295
341
  });
296
342
  }
343
+ const prefix = args.prefix === true;
344
+ if (prefix && config.engine === "bm25") {
345
+ throw unsupportedCapability({
346
+ dialect: "postgres",
347
+ feature: 'search prefix mode on the "bm25" engine',
348
+ hint: "ParadeDB prefix behavior is decided by the index tokenizer (ngram/edge-ngram) \u2014 configure the index instead, or use the tsvector engine"
349
+ });
350
+ }
351
+ let matchText = args.query;
352
+ if (prefix) {
353
+ const tokens = args.query.match(/[\p{L}\p{N}]+/gu) ?? [];
354
+ if (tokens.length === 0)
355
+ return [];
356
+ matchText = config.engine === "tsvector" ? tokens.map((token) => `${token}:*`).join(" & ") : config.engine === "fts5" ? tokens.map((token) => `"${token}"*`).join(" ") : tokens.map((token) => `+${token}*`).join(" ");
357
+ }
297
358
  const take = Math.max(1, Math.trunc(args.take ?? 10));
298
359
  const skip = args.skip === undefined ? null : Math.max(0, Math.trunc(args.skip));
299
360
  const hlColumn = highlightColumn({ config, args });
361
+ const idOnly = args.select === "id";
362
+ if (idOnly && config.pkColumn === undefined) {
363
+ throw new VibeError2({
364
+ code: "VIBE_VALIDATION",
365
+ message: `select: "id" needs a single-column primary key baked into the client \u2014 ${config.table} has none, or the client predates the projection (run vibeorm generate)`,
366
+ meta: { table: config.table }
367
+ });
368
+ }
369
+ const projection = idOnly ? `${t}.${quote(config.pkColumn ?? "")}` : `${t}.*`;
300
370
  const collector = new ParamCollector(dialect);
301
371
  const userWhere = (qualify) => {
302
372
  if (args.where === undefined)
@@ -309,9 +379,10 @@ function createSearchDelegate(config) {
309
379
  let engineHasHighlights = true;
310
380
  if (config.engine === "tsvector") {
311
381
  const language = config.language ?? "english";
382
+ const ranker = config.rank ?? "ts_rank_cd";
312
383
  const tsv = `${t}.${quote(config.tsvColumn ?? "")}`;
313
- const tsquery = () => `websearch_to_tsquery('${language}', ${collector.add(args.query)})`;
314
- text = `SELECT ${t}.*, ts_rank_cd(${tsv}, ${tsquery()}) AS ${quote(SCORE_ALIAS)}`;
384
+ const tsquery = () => prefix ? `to_tsquery('${language}', ${collector.add(matchText)})` : `websearch_to_tsquery('${language}', ${collector.add(matchText)})`;
385
+ text = `SELECT ${projection}, ${ranker}(${tsv}, ${tsquery()}) AS ${quote(SCORE_ALIAS)}`;
315
386
  if (hlColumn !== null) {
316
387
  text += `, ts_headline('${language}', ${t}.${quote(hlColumn)}, ${tsquery()}) AS ${quote(HIGHLIGHT_ALIAS)}`;
317
388
  }
@@ -320,19 +391,19 @@ function createSearchDelegate(config) {
320
391
  if (where !== null)
321
392
  text += ` AND (${where})`;
322
393
  if (args.minScore !== undefined) {
323
- text += ` AND ts_rank_cd(${tsv}, ${tsquery()}) >= ${collector.add(args.minScore)}`;
394
+ text += ` AND ${ranker}(${tsv}, ${tsquery()}) >= ${collector.add(args.minScore)}`;
324
395
  }
325
396
  text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
326
- strip = [config.tsvColumn ?? ""];
397
+ strip = idOnly ? [] : [config.tsvColumn ?? ""];
327
398
  } else if (config.engine === "fts5") {
328
399
  const fts = quote(config.ftsTable ?? "");
329
- text = `SELECT ${t}.*, -bm25(${fts}) AS ${quote(SCORE_ALIAS)}`;
400
+ text = `SELECT ${projection}, -bm25(${fts}) AS ${quote(SCORE_ALIAS)}`;
330
401
  if (hlColumn !== null) {
331
402
  const index = config.fields.findIndex((field) => field.column === hlColumn);
332
403
  text += `, snippet(${fts}, ${index}, '<b>', '</b>', '\u2026', 16) AS ${quote(HIGHLIGHT_ALIAS)}`;
333
404
  }
334
405
  text += ` FROM ${fts} JOIN ${quote(config.table)} AS ${t} ON ${t}.rowid = ${fts}.rowid`;
335
- text += ` WHERE ${fts} MATCH ${collector.add(args.query)}`;
406
+ text += ` WHERE ${fts} MATCH ${collector.add(matchText)}`;
336
407
  const where = userWhere("t");
337
408
  if (where !== null)
338
409
  text += ` AND (${where})`;
@@ -342,8 +413,9 @@ function createSearchDelegate(config) {
342
413
  text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
343
414
  } else if (config.engine === "fulltext") {
344
415
  engineHasHighlights = false;
345
- const match = () => `MATCH (${config.fields.map((field) => `${t}.${quote(field.column)}`).join(", ")}) AGAINST (${collector.add(args.query)} IN NATURAL LANGUAGE MODE)`;
346
- text = `SELECT ${t}.*, ${match()} AS ${quote(SCORE_ALIAS)} FROM ${quote(config.table)} AS ${t}`;
416
+ const mode = prefix ? "IN BOOLEAN MODE" : "IN NATURAL LANGUAGE MODE";
417
+ const match = () => `MATCH (${config.fields.map((field) => `${t}.${quote(field.column)}`).join(", ")}) AGAINST (${collector.add(matchText)} ${mode})`;
418
+ text = `SELECT ${projection}, ${match()} AS ${quote(SCORE_ALIAS)} FROM ${quote(config.table)} AS ${t}`;
347
419
  text += ` WHERE ${match()}`;
348
420
  const where = userWhere("t");
349
421
  if (where !== null)
@@ -354,7 +426,7 @@ function createSearchDelegate(config) {
354
426
  text += ` ORDER BY ${quote(SCORE_ALIAS)} DESC`;
355
427
  } else {
356
428
  const pk = `${t}.${quote(config.pkColumn ?? "")}`;
357
- text = `SELECT ${t}.*, paradedb.score(${pk}) AS ${quote(SCORE_ALIAS)}`;
429
+ text = `SELECT ${projection}, paradedb.score(${pk}) AS ${quote(SCORE_ALIAS)}`;
358
430
  if (hlColumn !== null) {
359
431
  text += `, paradedb.snippet(${t}.${quote(hlColumn)}) AS ${quote(HIGHLIGHT_ALIAS)}`;
360
432
  }
@@ -415,6 +487,16 @@ function search(options = {}) {
415
487
  if (language !== undefined && (typeof language !== "string" || !isSafeSqlName({ name: language }))) {
416
488
  return { error: `language must be a plain text-search config name` };
417
489
  }
490
+ for (const key of ["columnName", "indexName"]) {
491
+ const value = args.named[key];
492
+ if (value !== undefined && (typeof value !== "string" || !isSafeSqlName({ name: value }))) {
493
+ return { error: `${key} must be a plain identifier` };
494
+ }
495
+ }
496
+ const rank = args.named["rank"];
497
+ if (rank !== undefined && rank !== "ts_rank" && rank !== "ts_rank_cd") {
498
+ return { error: `rank must be "ts_rank" or "ts_rank_cd"` };
499
+ }
418
500
  return { declaration: { fields, ...args.named } };
419
501
  }
420
502
  }
@@ -438,6 +520,8 @@ function search(options = {}) {
438
520
  });
439
521
  }
440
522
  const pkField = primaryKeyFieldOf({ model });
523
+ const rank = resolveRank({ decl, options: extOptions, engine, dialect });
524
+ const names = resolveArtifactNames({ decl, engine, dialect });
441
525
  const config = {
442
526
  engine,
443
527
  table: tableNameOf({ model }),
@@ -445,26 +529,42 @@ function search(options = {}) {
445
529
  name: field,
446
530
  column: columnNameOf({ model, field })
447
531
  })),
448
- ...engine === "tsvector" ? { tsvColumn: `${decl.base}_tsv`, language } : {},
532
+ ...engine === "tsvector" ? { tsvColumn: names.tsvColumn, language, ...rank === undefined ? {} : { rank } } : {},
449
533
  ...engine === "fts5" ? { ftsTable: `${decl.base}_fts` } : {},
450
- ...engine === "bm25" && pkField !== null ? { pkColumn: columnNameOf({ model, field: pkField }) } : {}
534
+ ...pkField !== null ? { pkColumn: columnNameOf({ model, field: pkField }) } : {}
451
535
  };
452
536
  const fieldUnion = decl.fields.map((field) => JSON.stringify(field)).join(" | ");
537
+ const argLines = [
538
+ ` readonly query: string;`,
539
+ ` readonly where?: ${typeNames.whereInput};`,
540
+ ` readonly take?: number;`,
541
+ ` readonly skip?: number;`,
542
+ ` readonly minScore?: number;`,
543
+ ` readonly highlight?: { readonly field: ${fieldUnion} };`,
544
+ ` readonly prefix?: boolean;`
545
+ ];
546
+ const dts = pkField === null ? [
547
+ `readonly search: (args: {`,
548
+ ...argLines,
549
+ `}) => Promise<SearchHit<${typeNames.payload}>[]>;`
550
+ ] : [
551
+ `readonly search: {`,
552
+ ` (args: {`,
553
+ ...argLines.map((line) => ` ${line}`),
554
+ ` readonly select: "id";`,
555
+ ` }): Promise<SearchHit<Pick<${typeNames.payload}, ${JSON.stringify(pkField)}>>[]>;`,
556
+ ` (args: {`,
557
+ ...argLines.map((line) => ` ${line}`),
558
+ ` readonly select?: "row";`,
559
+ ` }): Promise<SearchHit<${typeNames.payload}>[]>;`,
560
+ `};`
561
+ ];
453
562
  return {
454
563
  kind: "method",
455
564
  runtimeGlue: `createSearchDelegate(${JSON.stringify(config)})`,
456
565
  imports: [{ from: "@vibeorm/extensions/search", names: ["createSearchDelegate"] }],
457
566
  dtsImports: [{ from: "@vibeorm/extensions/search", names: ["SearchHit"] }],
458
- dts: [
459
- `readonly search: (args: {`,
460
- ` readonly query: string;`,
461
- ` readonly where?: ${typeNames.whereInput};`,
462
- ` readonly take?: number;`,
463
- ` readonly skip?: number;`,
464
- ` readonly minScore?: number;`,
465
- ` readonly highlight?: { readonly field: ${fieldUnion} };`,
466
- `}) => Promise<SearchHit<${typeNames.payload}>[]>;`
467
- ]
567
+ dts
468
568
  };
469
569
  }
470
570
  },
@@ -571,7 +671,8 @@ function vectorArtifacts(ctx) {
571
671
  });
572
672
  }
573
673
  const column = columnNameOf({ model, field });
574
- const indexName = `${table}_${column}_${kindRaw}_idx`;
674
+ const nameOverride = indexFields.length === 1 ? declString(declaration, "indexName") : undefined;
675
+ const indexName = nameOverride ?? `${table}_${column}_${kindRaw}_idx`;
575
676
  const withOptions = [];
576
677
  const m = declaration["m"];
577
678
  const efConstruction = declaration["efConstruction"];
@@ -719,6 +820,17 @@ function vector(options = {}) {
719
820
  return { error: `${key} must be a number` };
720
821
  }
721
822
  }
823
+ const indexName = args.named["indexName"];
824
+ if (indexName !== undefined) {
825
+ if (typeof indexName !== "string" || !isSafeSqlName({ name: indexName })) {
826
+ return { error: `indexName must be a plain identifier` };
827
+ }
828
+ if (fields.length !== 1) {
829
+ return {
830
+ error: `indexName needs exactly one indexed field \u2014 split @@vector declarations to name each index`
831
+ };
832
+ }
833
+ }
722
834
  return { declaration: { fields, ...args.named } };
723
835
  }
724
836
  }
@@ -963,4 +1075,4 @@ export {
963
1075
  createSearchDelegate
964
1076
  };
965
1077
 
966
- //# debugId=B1B4930A9A8DBD3964756E2164756E21
1078
+ //# debugId=92CFAFA74775D03064756E2164756E21