@lumerahq/cli 0.22.2 → 0.22.4

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/dist/index.js CHANGED
@@ -226,25 +226,25 @@ async function main() {
226
226
  switch (command) {
227
227
  // Resource commands
228
228
  case "plan":
229
- await import("./resources-TJUKE2O3.js").then((m) => m.plan(args.slice(1)));
229
+ await import("./resources-62N77NOM.js").then((m) => m.plan(args.slice(1)));
230
230
  break;
231
231
  case "apply":
232
- await import("./resources-TJUKE2O3.js").then((m) => m.apply(args.slice(1)));
232
+ await import("./resources-62N77NOM.js").then((m) => m.apply(args.slice(1)));
233
233
  break;
234
234
  case "pull":
235
- await import("./resources-TJUKE2O3.js").then((m) => m.pull(args.slice(1)));
235
+ await import("./resources-62N77NOM.js").then((m) => m.pull(args.slice(1)));
236
236
  break;
237
237
  case "destroy":
238
- await import("./resources-TJUKE2O3.js").then((m) => m.destroy(args.slice(1)));
238
+ await import("./resources-62N77NOM.js").then((m) => m.destroy(args.slice(1)));
239
239
  break;
240
240
  case "list":
241
- await import("./resources-TJUKE2O3.js").then((m) => m.list(args.slice(1)));
241
+ await import("./resources-62N77NOM.js").then((m) => m.list(args.slice(1)));
242
242
  break;
243
243
  case "show":
244
- await import("./resources-TJUKE2O3.js").then((m) => m.show(args.slice(1)));
244
+ await import("./resources-62N77NOM.js").then((m) => m.show(args.slice(1)));
245
245
  break;
246
246
  case "diff":
247
- await import("./resources-TJUKE2O3.js").then((m) => m.diff(args.slice(1)));
247
+ await import("./resources-62N77NOM.js").then((m) => m.diff(args.slice(1)));
248
248
  break;
249
249
  // Development
250
250
  case "dev":
@@ -125,6 +125,15 @@ var collectionSchemaRule = {
125
125
  const name = parsed.name;
126
126
  const fields = parsed.fields;
127
127
  const indexes = parsed.indexes;
128
+ const search = parsed.search;
129
+ const localFieldTypes = /* @__PURE__ */ new Map();
130
+ if (Array.isArray(fields)) {
131
+ for (const field of fields) {
132
+ if (isRecord2(field) && typeof field.name === "string") {
133
+ localFieldTypes.set(field.name, field.type);
134
+ }
135
+ }
136
+ }
128
137
  if (typeof id !== "string" || id.trim() === "") {
129
138
  issues.push(error(target, 'Missing required string field "id".'));
130
139
  }
@@ -173,6 +182,84 @@ var collectionSchemaRule = {
173
182
  }
174
183
  }
175
184
  }
185
+ if (search !== void 0) {
186
+ if (!isRecord2(search)) {
187
+ issues.push(error(target, 'Optional field "search" must be an object when present.'));
188
+ } else {
189
+ const unknownSearchKeys = Object.keys(search).filter((key) => key !== "fulltext" && key !== "semantic");
190
+ for (const key of unknownSearchKeys) {
191
+ issues.push(error(target, `Unknown search option "${key}".`));
192
+ }
193
+ const fulltext = search.fulltext;
194
+ const semantic = search.semantic;
195
+ if (fulltext === void 0 && semantic === void 0) {
196
+ issues.push(error(target, '"search" must include "fulltext" or "semantic".'));
197
+ }
198
+ if (fulltext !== void 0 && !isRecord2(fulltext)) {
199
+ issues.push(error(target, '"search.fulltext" must be an object when present.'));
200
+ } else if (isRecord2(fulltext)) {
201
+ const allowed = /* @__PURE__ */ new Set(["fields", "language", "fuzzy_fields"]);
202
+ for (const key of Object.keys(fulltext).filter((key2) => !allowed.has(key2))) {
203
+ issues.push(error(target, `Unknown search.fulltext option "${key}".`));
204
+ }
205
+ if (!isRecord2(fulltext.fields) || Object.keys(fulltext.fields).length === 0) {
206
+ issues.push(error(target, '"search.fulltext.fields" must be a non-empty field-to-weight object.'));
207
+ } else {
208
+ if (Object.keys(fulltext.fields).length > 8) {
209
+ issues.push(error(target, '"search.fulltext.fields" cannot contain more than 8 fields.'));
210
+ }
211
+ for (const [field, weight] of Object.entries(fulltext.fields)) {
212
+ if (!["A", "B", "C", "D"].includes(String(weight))) {
213
+ issues.push(error(target, `Search field "${field}" must use weight A, B, C, or D.`));
214
+ }
215
+ const type = localFieldTypes.get(field);
216
+ if (type !== "text" && type !== "editor") {
217
+ issues.push(error(target, `Search field "${field}" must name a text or editor field.`));
218
+ }
219
+ }
220
+ }
221
+ if (fulltext.language !== void 0 && fulltext.language !== "english" && fulltext.language !== "simple") {
222
+ issues.push(error(target, '"search.fulltext.language" must be "english" or "simple".'));
223
+ }
224
+ if (fulltext.fuzzy_fields !== void 0) {
225
+ if (!Array.isArray(fulltext.fuzzy_fields) || !fulltext.fuzzy_fields.every((field) => typeof field === "string")) {
226
+ issues.push(error(target, '"search.fulltext.fuzzy_fields" must be an array of field names.'));
227
+ } else {
228
+ for (const field of fulltext.fuzzy_fields) {
229
+ const type = localFieldTypes.get(field);
230
+ if (type !== "text" && type !== "editor") {
231
+ issues.push(error(target, `Fuzzy search field "${field}" must name a text or editor field.`));
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+ if (semantic !== void 0 && !isRecord2(semantic)) {
238
+ issues.push(error(target, '"search.semantic" must be an object when present.'));
239
+ } else if (isRecord2(semantic)) {
240
+ const allowed = /* @__PURE__ */ new Set(["fields", "model"]);
241
+ for (const key of Object.keys(semantic).filter((key2) => !allowed.has(key2))) {
242
+ issues.push(error(target, `Unknown search.semantic option "${key}".`));
243
+ }
244
+ if (!Array.isArray(semantic.fields) || semantic.fields.length === 0 || !semantic.fields.every((field) => typeof field === "string")) {
245
+ issues.push(error(target, '"search.semantic.fields" must be a non-empty array of field names.'));
246
+ } else {
247
+ if (semantic.fields.length > 8) {
248
+ issues.push(error(target, '"search.semantic.fields" cannot contain more than 8 fields.'));
249
+ }
250
+ for (const field of semantic.fields) {
251
+ const type = localFieldTypes.get(field);
252
+ if (type !== "text" && type !== "editor") {
253
+ issues.push(error(target, `Semantic search field "${field}" must name a text or editor field.`));
254
+ }
255
+ }
256
+ }
257
+ if (semantic.model !== void 0 && semantic.model !== "text-embedding-3-small") {
258
+ issues.push(error(target, '"search.semantic.model" must be "text-embedding-3-small".'));
259
+ }
260
+ }
261
+ }
262
+ }
176
263
  return issues;
177
264
  }
178
265
  };
@@ -1455,7 +1542,7 @@ function convertCollectionToApiFormat(local) {
1455
1542
  const indexName = `idx_${local.id}_${idx.fields.join("_")}`;
1456
1543
  return `CREATE ${unique}INDEX ${indexName} ON ${local.name} (${fields})`;
1457
1544
  });
1458
- return { id: local.id, name: local.name, schema, indexes };
1545
+ return { id: local.id, name: local.name, schema, indexes, search: local.search ?? null };
1459
1546
  }
1460
1547
  function mapFieldType(type) {
1461
1548
  const typeMap = {
@@ -1545,6 +1632,26 @@ function indexesDiffer(local, remoteIndexes) {
1545
1632
  }
1546
1633
  return false;
1547
1634
  }
1635
+ function normalizeCollectionSearch(search) {
1636
+ if (!search) return "null";
1637
+ return JSON.stringify({
1638
+ ...search.fulltext ? {
1639
+ fulltext: {
1640
+ fields: Object.fromEntries(
1641
+ Object.entries(search.fulltext.fields).sort(([left], [right]) => left.localeCompare(right))
1642
+ ),
1643
+ language: search.fulltext.language ?? "english",
1644
+ fuzzy_fields: [...search.fulltext.fuzzy_fields ?? []].sort()
1645
+ }
1646
+ } : {},
1647
+ ...search.semantic ? {
1648
+ semantic: {
1649
+ fields: [...search.semantic.fields],
1650
+ model: search.semantic.model ?? "text-embedding-3-small"
1651
+ }
1652
+ } : {}
1653
+ });
1654
+ }
1548
1655
  async function planCollections(api, localCollections) {
1549
1656
  const changes = [];
1550
1657
  const remoteCollections = await api.listCollections();
@@ -1582,12 +1689,14 @@ async function planCollections(api, localCollections) {
1582
1689
  }
1583
1690
  }
1584
1691
  const indexesChanged = indexesDiffer(local, remote.indexes || []);
1585
- if (added.length > 0 || removed.length > 0 || modified.length > 0 || indexesChanged) {
1692
+ const searchChanged = normalizeCollectionSearch(local.search) !== normalizeCollectionSearch(remote.search);
1693
+ if (added.length > 0 || removed.length > 0 || modified.length > 0 || indexesChanged || searchChanged) {
1586
1694
  const details = [];
1587
1695
  if (added.length > 0) details.push(`+${added.length} field${added.length > 1 ? "s" : ""}`);
1588
1696
  if (removed.length > 0) details.push(`-${removed.length} field${removed.length > 1 ? "s" : ""}`);
1589
1697
  if (modified.length > 0) details.push(`~${modified.length} field${modified.length > 1 ? "s" : ""} (${modified.join(", ")})`);
1590
1698
  if (indexesChanged) details.push("indexes changed");
1699
+ if (searchChanged) details.push("search changed");
1591
1700
  const fieldDetails = [];
1592
1701
  for (const name of added) {
1593
1702
  const f = localFieldMap.get(name);
@@ -2099,7 +2208,8 @@ async function pullCollections(api, platformDir, filterName, appName) {
2099
2208
  return { fields: match[2].split(",").map((f) => f.trim()), unique: !!match[1] };
2100
2209
  }
2101
2210
  return null;
2102
- }).filter((idx) => idx !== null)
2211
+ }).filter((idx) => idx !== null),
2212
+ search: collection.search
2103
2213
  };
2104
2214
  const fileName = toSafeFilename(localName);
2105
2215
  const filePath = join2(collectionsDir, `${fileName}.json`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.22.2",
3
+ "version": "0.22.4",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -24,7 +24,7 @@
24
24
  "check:ci": "biome check . && tsr generate && tsc --noEmit"
25
25
  },
26
26
  "dependencies": {
27
- "@lumerahq/ui": "^0.11.1",
27
+ "@lumerahq/ui": "^0.11.3",
28
28
  "@tanstack/react-query": "^5.90.11",
29
29
  "@tanstack/react-router": "1.155.0",
30
30
  "clsx": "^2.1.1",