@tsproxy/cli 0.0.2 → 0.0.3
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 +200 -4
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -310,8 +310,8 @@ function findCliEntry() {
|
|
|
310
310
|
resolve2(process.cwd(), "node_modules/@tsproxy/api/dist/cli.js"),
|
|
311
311
|
resolve2(process.cwd(), "node_modules/@tsproxy/api/src/cli.ts")
|
|
312
312
|
];
|
|
313
|
-
for (const
|
|
314
|
-
if (existsSync2(
|
|
313
|
+
for (const p3 of paths) {
|
|
314
|
+
if (existsSync2(p3)) return p3;
|
|
315
315
|
}
|
|
316
316
|
return null;
|
|
317
317
|
}
|
|
@@ -597,6 +597,72 @@ Dry run \u2014 ${pendingActions.length} change(s). Run with --apply to execute.`
|
|
|
597
597
|
);
|
|
598
598
|
}
|
|
599
599
|
}
|
|
600
|
+
for (const [name, def] of Object.entries(collections)) {
|
|
601
|
+
const names = [name];
|
|
602
|
+
if (def.locales?.length) {
|
|
603
|
+
for (const locale of def.locales) {
|
|
604
|
+
names.push(`${name}_${locale}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
for (const colName of names) {
|
|
608
|
+
if (def.synonyms) {
|
|
609
|
+
for (const [synName, synDef] of Object.entries(def.synonyms)) {
|
|
610
|
+
try {
|
|
611
|
+
const body = {};
|
|
612
|
+
if (synDef.synonyms) {
|
|
613
|
+
body.synonyms = synDef.synonyms;
|
|
614
|
+
} else if (synDef.root && synDef.words) {
|
|
615
|
+
body.root = synDef.root;
|
|
616
|
+
body.synonyms = synDef.words;
|
|
617
|
+
}
|
|
618
|
+
await fetch(`${baseUrl}/collections/${colName}/synonyms/${synName}`, {
|
|
619
|
+
method: "PUT",
|
|
620
|
+
headers: {
|
|
621
|
+
"Content-Type": "application/json",
|
|
622
|
+
"X-TYPESENSE-API-KEY": tsApiKey
|
|
623
|
+
},
|
|
624
|
+
body: JSON.stringify(body)
|
|
625
|
+
});
|
|
626
|
+
console.log(` ${pc3.green("\u2713")} ${colName}/synonyms/${synName}`);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
console.error(` ${pc3.red("\u2717")} ${colName}/synonyms/${synName} \u2014 ${err.message}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (def.curations) {
|
|
633
|
+
for (const [curName, curDef] of Object.entries(def.curations)) {
|
|
634
|
+
try {
|
|
635
|
+
const body = {
|
|
636
|
+
rule: {
|
|
637
|
+
query: curDef.query,
|
|
638
|
+
match: curDef.match || "exact"
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
if (curDef.pinnedIds?.length) {
|
|
642
|
+
body.includes = curDef.pinnedIds.map((id, i) => ({
|
|
643
|
+
id,
|
|
644
|
+
position: i + 1
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
if (curDef.hiddenIds?.length) {
|
|
648
|
+
body.excludes = curDef.hiddenIds.map((id) => ({ id }));
|
|
649
|
+
}
|
|
650
|
+
await fetch(`${baseUrl}/collections/${colName}/overrides/${curName}`, {
|
|
651
|
+
method: "PUT",
|
|
652
|
+
headers: {
|
|
653
|
+
"Content-Type": "application/json",
|
|
654
|
+
"X-TYPESENSE-API-KEY": tsApiKey
|
|
655
|
+
},
|
|
656
|
+
body: JSON.stringify(body)
|
|
657
|
+
});
|
|
658
|
+
console.log(` ${pc3.green("\u2713")} ${colName}/overrides/${curName}`);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.error(` ${pc3.red("\u2717")} ${colName}/overrides/${curName} \u2014 ${err.message}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
600
666
|
console.log(pc3.green("\nMigration complete."));
|
|
601
667
|
}
|
|
602
668
|
function buildSchema(name, def) {
|
|
@@ -620,8 +686,8 @@ function findConfig() {
|
|
|
620
686
|
const root = resolve5("/");
|
|
621
687
|
while (dir !== root) {
|
|
622
688
|
for (const name of names) {
|
|
623
|
-
const
|
|
624
|
-
if (existsSync4(
|
|
689
|
+
const p3 = resolve5(dir, name);
|
|
690
|
+
if (existsSync4(p3)) return p3;
|
|
625
691
|
}
|
|
626
692
|
dir = resolve5(dir, "..");
|
|
627
693
|
}
|
|
@@ -663,6 +729,135 @@ async function health() {
|
|
|
663
729
|
}
|
|
664
730
|
}
|
|
665
731
|
|
|
732
|
+
// src/commands/generate.ts
|
|
733
|
+
import * as p2 from "@clack/prompts";
|
|
734
|
+
import pc5 from "picocolors";
|
|
735
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync5 } from "fs";
|
|
736
|
+
import { resolve as resolve6 } from "path";
|
|
737
|
+
async function generate(opts) {
|
|
738
|
+
p2.intro(pc5.bgCyan(pc5.black(" tsproxy generate ")));
|
|
739
|
+
const tsHost = opts.host || process.env.TYPESENSE_HOST || "localhost";
|
|
740
|
+
const tsPort = opts.port || process.env.TYPESENSE_PORT || "8108";
|
|
741
|
+
const tsApiKey = opts.key || process.env.TYPESENSE_API_KEY;
|
|
742
|
+
if (!tsApiKey) {
|
|
743
|
+
p2.log.error("Typesense API key is required. Set TYPESENSE_API_KEY or pass --key.");
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
const s = p2.spinner();
|
|
747
|
+
s.start("Fetching collections from Typesense");
|
|
748
|
+
let collections;
|
|
749
|
+
try {
|
|
750
|
+
const res = await fetch(`http://${tsHost}:${tsPort}/collections`, {
|
|
751
|
+
headers: { "X-TYPESENSE-API-KEY": tsApiKey }
|
|
752
|
+
});
|
|
753
|
+
if (!res.ok) {
|
|
754
|
+
throw new Error(`${res.status} ${res.statusText}`);
|
|
755
|
+
}
|
|
756
|
+
collections = await res.json();
|
|
757
|
+
} catch (err) {
|
|
758
|
+
s.stop("Failed to connect");
|
|
759
|
+
p2.log.error(`Cannot connect to Typesense at ${tsHost}:${tsPort}`);
|
|
760
|
+
p2.log.error(err.message);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
s.stop(`Found ${collections.length} collection(s)`);
|
|
764
|
+
if (collections.length === 0) {
|
|
765
|
+
p2.log.warn("No collections found. Create some collections first.");
|
|
766
|
+
process.exit(0);
|
|
767
|
+
}
|
|
768
|
+
const selected = await p2.multiselect({
|
|
769
|
+
message: "Which collections to include?",
|
|
770
|
+
options: collections.map((col) => ({
|
|
771
|
+
value: col.name,
|
|
772
|
+
label: `${col.name} (${col.num_documents} docs, ${col.fields.length} fields)`
|
|
773
|
+
})),
|
|
774
|
+
required: true
|
|
775
|
+
});
|
|
776
|
+
if (p2.isCancel(selected)) return process.exit(0);
|
|
777
|
+
const TYPE_MAP = {
|
|
778
|
+
string: "string",
|
|
779
|
+
"string[]": "string[]",
|
|
780
|
+
int32: "int32",
|
|
781
|
+
"int32[]": "int32[]",
|
|
782
|
+
int64: "int64",
|
|
783
|
+
"int64[]": "int64[]",
|
|
784
|
+
float: "float",
|
|
785
|
+
"float[]": "float[]",
|
|
786
|
+
bool: "bool",
|
|
787
|
+
"bool[]": "bool[]",
|
|
788
|
+
geopoint: "geopoint",
|
|
789
|
+
"geopoint[]": "geopoint[]",
|
|
790
|
+
auto: "auto",
|
|
791
|
+
object: "object",
|
|
792
|
+
"object[]": "object[]"
|
|
793
|
+
};
|
|
794
|
+
const collectionConfigs = [];
|
|
795
|
+
for (const colName of selected) {
|
|
796
|
+
const col = collections.find((c) => c.name === colName);
|
|
797
|
+
if (!col) continue;
|
|
798
|
+
const fieldLines = [];
|
|
799
|
+
for (const field of col.fields) {
|
|
800
|
+
if (field.name === ".*") continue;
|
|
801
|
+
const type = TYPE_MAP[field.type] || "string";
|
|
802
|
+
const props = [`type: "${type}"`];
|
|
803
|
+
if ((field.type === "string" || field.type === "string[]") && field.index !== false) {
|
|
804
|
+
props.push("searchable: true");
|
|
805
|
+
}
|
|
806
|
+
if (field.facet) props.push("facet: true");
|
|
807
|
+
if (field.sort) props.push("sortable: true");
|
|
808
|
+
if (field.optional) props.push("optional: true");
|
|
809
|
+
if (field.infix) props.push("infix: true");
|
|
810
|
+
fieldLines.push(` ${field.name}: { ${props.join(", ")} },`);
|
|
811
|
+
}
|
|
812
|
+
const sortField = col.default_sorting_field ? `
|
|
813
|
+
defaultSortBy: "${col.default_sorting_field}",` : "";
|
|
814
|
+
collectionConfigs.push(` ${colName}: {
|
|
815
|
+
fields: {
|
|
816
|
+
${fieldLines.join("\n")}
|
|
817
|
+
},${sortField}
|
|
818
|
+
},`);
|
|
819
|
+
}
|
|
820
|
+
const configContent = `import { defineConfig } from "@tsproxy/api";
|
|
821
|
+
|
|
822
|
+
export default defineConfig({
|
|
823
|
+
typesense: {
|
|
824
|
+
host: "${tsHost}",
|
|
825
|
+
port: ${tsPort},
|
|
826
|
+
protocol: "http",
|
|
827
|
+
apiKey: process.env.TYPESENSE_API_KEY || "${tsApiKey}",
|
|
828
|
+
},
|
|
829
|
+
|
|
830
|
+
server: {
|
|
831
|
+
port: 3000,
|
|
832
|
+
apiKey: process.env.PROXY_API_KEY || "change-me",
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
cache: { ttl: 60, maxSize: 1000 },
|
|
836
|
+
queue: { concurrency: 5, maxSize: 10000 },
|
|
837
|
+
rateLimit: { search: 100, ingest: 30 },
|
|
838
|
+
|
|
839
|
+
collections: {
|
|
840
|
+
${collectionConfigs.join("\n\n")}
|
|
841
|
+
},
|
|
842
|
+
});
|
|
843
|
+
`;
|
|
844
|
+
const outputPath = resolve6(process.cwd(), opts.output || "tsproxy.config.ts");
|
|
845
|
+
if (existsSync5(outputPath)) {
|
|
846
|
+
const overwrite = await p2.confirm({
|
|
847
|
+
message: `${opts.output || "tsproxy.config.ts"} already exists. Overwrite?`,
|
|
848
|
+
initialValue: false
|
|
849
|
+
});
|
|
850
|
+
if (p2.isCancel(overwrite) || !overwrite) {
|
|
851
|
+
p2.log.info("Generated config:");
|
|
852
|
+
console.log(configContent);
|
|
853
|
+
p2.outro("Config printed to stdout (file not written).");
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
writeFileSync2(outputPath, configContent);
|
|
858
|
+
p2.outro(pc5.green(`Config written to ${opts.output || "tsproxy.config.ts"}`));
|
|
859
|
+
}
|
|
860
|
+
|
|
666
861
|
// src/index.ts
|
|
667
862
|
var program = new Command();
|
|
668
863
|
program.name("tsproxy").description("Typesense search proxy framework").version("0.1.0");
|
|
@@ -673,4 +868,5 @@ program.command("build").description("Build the proxy for production").action(bu
|
|
|
673
868
|
program.command("seed [file]").description("Seed Typesense with data from a JSON/JSONL file").option("--collection <name>", "Collection name").option("--locale <locale>", "Locale for the collection").option("--locales", "Create locale-specific collections").action(seed);
|
|
674
869
|
program.command("migrate").description("Sync Typesense schema with config").option("--apply", "Apply changes (default is dry-run)").option("--drop", "Drop and recreate collections").action(migrate);
|
|
675
870
|
program.command("health").description("Check Typesense and Redis connectivity").action(health);
|
|
871
|
+
program.command("generate").description("Generate tsproxy.config.ts from existing Typesense schema").option("--host <host>", "Typesense host").option("--port <port>", "Typesense port").option("--key <key>", "Typesense API key").option("-o, --output <path>", "Output file path", "tsproxy.config.ts").action(generate);
|
|
676
872
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tsproxy/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "CLI for tsproxy — Typesense search proxy framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsup src/index.ts --format esm --target node22",
|
|
30
|
-
"dev": "tsx src/index.ts"
|
|
30
|
+
"dev": "tsx src/index.ts",
|
|
31
|
+
"test": "vitest run"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"@clack/prompts": "^0.10.0",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"@types/node": "^20",
|
|
39
40
|
"tsup": "^8.4.0",
|
|
40
41
|
"tsx": "^4.19.0",
|
|
41
|
-
"typescript": "^5"
|
|
42
|
+
"typescript": "^5",
|
|
43
|
+
"vitest": "^3.2.4"
|
|
42
44
|
}
|
|
43
45
|
}
|