@tsproxy/cli 0.0.1 → 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/README.md +63 -0
- package/dist/index.js +200 -4
- package/package.json +11 -8
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @tsproxy/cli
|
|
2
|
+
|
|
3
|
+
CLI for [tsproxy](https://github.com/akshitkrnagpal/tsproxy) — a Typesense search proxy framework.
|
|
4
|
+
|
|
5
|
+
> **This project is under heavy development.**
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @tsproxy/cli init
|
|
11
|
+
docker compose up -d
|
|
12
|
+
npx tsproxy dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
tsproxy init # Interactive project setup
|
|
19
|
+
tsproxy dev # Start proxy in dev mode (hot reload)
|
|
20
|
+
tsproxy start # Start proxy in production mode
|
|
21
|
+
tsproxy build # Build for production
|
|
22
|
+
tsproxy seed <file> # Seed data via the ingest API
|
|
23
|
+
tsproxy migrate # Sync Typesense schema with config
|
|
24
|
+
tsproxy health # Check Typesense + Redis status
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## `tsproxy init`
|
|
28
|
+
|
|
29
|
+
Interactive setup that asks:
|
|
30
|
+
|
|
31
|
+
1. **What to set up** — Backend / Frontend / Both
|
|
32
|
+
2. **How you run Typesense** — Docker / Typesense Cloud / Self-hosted
|
|
33
|
+
3. **Persistent queue** — Redis (optional)
|
|
34
|
+
4. **Frontend framework** — React / Vanilla JS
|
|
35
|
+
|
|
36
|
+
Generates `tsproxy.config.ts`, `docker-compose.yml`, `.env`, and installs dependencies.
|
|
37
|
+
|
|
38
|
+
## `tsproxy migrate`
|
|
39
|
+
|
|
40
|
+
Diffs your `tsproxy.config.ts` collections against live Typesense schema:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx tsproxy migrate # dry run
|
|
44
|
+
npx tsproxy migrate --apply # apply changes
|
|
45
|
+
npx tsproxy migrate --apply --drop # drop and recreate
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## `tsproxy seed`
|
|
49
|
+
|
|
50
|
+
Seeds data through the proxy's ingest API (applies computed fields, uses queue):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx tsproxy seed products.json --collection products
|
|
54
|
+
npx tsproxy seed products.json --collection products --locale en
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Documentation
|
|
58
|
+
|
|
59
|
+
[tsproxy.akshit.io](https://tsproxy.akshit.io)
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
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,13 +1,14 @@
|
|
|
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": {
|
|
7
7
|
"tsproxy": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
11
12
|
],
|
|
12
13
|
"publishConfig": {
|
|
13
14
|
"access": "public"
|
|
@@ -24,6 +25,11 @@
|
|
|
24
25
|
"proxy",
|
|
25
26
|
"cli"
|
|
26
27
|
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup src/index.ts --format esm --target node22",
|
|
30
|
+
"dev": "tsx src/index.ts",
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
},
|
|
27
33
|
"dependencies": {
|
|
28
34
|
"@clack/prompts": "^0.10.0",
|
|
29
35
|
"commander": "^13.1.0",
|
|
@@ -33,10 +39,7 @@
|
|
|
33
39
|
"@types/node": "^20",
|
|
34
40
|
"tsup": "^8.4.0",
|
|
35
41
|
"tsx": "^4.19.0",
|
|
36
|
-
"typescript": "^5"
|
|
37
|
-
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "tsup src/index.ts --format esm --target node22",
|
|
40
|
-
"dev": "tsx src/index.ts"
|
|
42
|
+
"typescript": "^5",
|
|
43
|
+
"vitest": "^3.2.4"
|
|
41
44
|
}
|
|
42
|
-
}
|
|
45
|
+
}
|