tthr 0.0.19 → 0.0.21

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.
Files changed (2) hide show
  1. package/dist/index.js +205 -54
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1567,6 +1567,195 @@ import chalk5 from "chalk";
1567
1567
  import ora4 from "ora";
1568
1568
  import fs6 from "fs-extra";
1569
1569
  import path6 from "path";
1570
+ function parseSchemaFile(source) {
1571
+ const tables = [];
1572
+ const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
1573
+ if (!schemaMatch) return tables;
1574
+ const schemaContent = schemaMatch[1];
1575
+ const tableStartRegex = /(\w+)\s*:\s*\{/g;
1576
+ let match;
1577
+ while ((match = tableStartRegex.exec(schemaContent)) !== null) {
1578
+ const tableName = match[1];
1579
+ const startOffset = match.index + match[0].length;
1580
+ let braceCount = 1;
1581
+ let endOffset = startOffset;
1582
+ for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
1583
+ const char = schemaContent[i];
1584
+ if (char === "{") braceCount++;
1585
+ else if (char === "}") braceCount--;
1586
+ endOffset = i;
1587
+ }
1588
+ const columnsContent = schemaContent.slice(startOffset, endOffset);
1589
+ const columns = parseColumns(columnsContent);
1590
+ tables.push({ name: tableName, columns });
1591
+ }
1592
+ return tables;
1593
+ }
1594
+ function parseColumns(content) {
1595
+ const columns = [];
1596
+ const columnRegex = /(\w+)\s*:\s*(\w+)\s*\(\s*\)([^,\n}]*)/g;
1597
+ let match;
1598
+ while ((match = columnRegex.exec(content)) !== null) {
1599
+ const name = match[1];
1600
+ const schemaType = match[2];
1601
+ const modifiers = match[3] || "";
1602
+ const nullable = !modifiers.includes(".notNull()") && !modifiers.includes(".primaryKey()");
1603
+ const primaryKey = modifiers.includes(".primaryKey()");
1604
+ columns.push({
1605
+ name,
1606
+ type: schemaTypeToTS(schemaType),
1607
+ nullable,
1608
+ primaryKey
1609
+ });
1610
+ }
1611
+ return columns;
1612
+ }
1613
+ function schemaTypeToTS(schemaType) {
1614
+ switch (schemaType) {
1615
+ case "text":
1616
+ return "string";
1617
+ case "integer":
1618
+ return "number";
1619
+ case "real":
1620
+ return "number";
1621
+ case "boolean":
1622
+ return "boolean";
1623
+ case "timestamp":
1624
+ return "string";
1625
+ case "json":
1626
+ return "unknown";
1627
+ case "blob":
1628
+ return "Uint8Array";
1629
+ default:
1630
+ return "unknown";
1631
+ }
1632
+ }
1633
+ function tableNameToInterface(tableName) {
1634
+ let name = tableName;
1635
+ if (name.endsWith("ies")) {
1636
+ name = name.slice(0, -3) + "y";
1637
+ } else if (name.endsWith("s") && !name.endsWith("ss")) {
1638
+ name = name.slice(0, -1);
1639
+ }
1640
+ return name.charAt(0).toUpperCase() + name.slice(1);
1641
+ }
1642
+ function generateDbFile(tables) {
1643
+ const lines = [
1644
+ "// Auto-generated by Tether CLI - do not edit manually",
1645
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1646
+ "",
1647
+ "import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
1648
+ ""
1649
+ ];
1650
+ for (const table of tables) {
1651
+ const interfaceName = tableNameToInterface(table.name);
1652
+ lines.push(`export interface ${interfaceName} {`);
1653
+ for (const col of table.columns) {
1654
+ const typeStr = col.nullable ? `${col.type} | null` : col.type;
1655
+ lines.push(` ${col.name}: ${typeStr};`);
1656
+ }
1657
+ lines.push("}");
1658
+ lines.push("");
1659
+ }
1660
+ lines.push("export interface Schema {");
1661
+ for (const table of tables) {
1662
+ const interfaceName = tableNameToInterface(table.name);
1663
+ lines.push(` ${table.name}: ${interfaceName};`);
1664
+ }
1665
+ lines.push("}");
1666
+ lines.push("");
1667
+ lines.push("// Database client with typed tables");
1668
+ lines.push("// This is a proxy that will be populated by the Tether runtime");
1669
+ lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
1670
+ lines.push("");
1671
+ return lines.join("\n");
1672
+ }
1673
+ async function parseFunctionsDir(functionsDir) {
1674
+ const functions = [];
1675
+ if (!await fs6.pathExists(functionsDir)) {
1676
+ return functions;
1677
+ }
1678
+ const files = await fs6.readdir(functionsDir);
1679
+ for (const file of files) {
1680
+ if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
1681
+ const filePath = path6.join(functionsDir, file);
1682
+ const stat = await fs6.stat(filePath);
1683
+ if (!stat.isFile()) continue;
1684
+ const moduleName = file.replace(/\.(ts|js)$/, "");
1685
+ const source = await fs6.readFile(filePath, "utf-8");
1686
+ const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action)\s*\(/g;
1687
+ let match;
1688
+ while ((match = exportRegex.exec(source)) !== null) {
1689
+ functions.push({
1690
+ name: match[1],
1691
+ moduleName
1692
+ });
1693
+ }
1694
+ }
1695
+ return functions;
1696
+ }
1697
+ async function generateApiFile(functionsDir) {
1698
+ const functions = await parseFunctionsDir(functionsDir);
1699
+ const moduleMap = /* @__PURE__ */ new Map();
1700
+ for (const fn of functions) {
1701
+ if (!moduleMap.has(fn.moduleName)) {
1702
+ moduleMap.set(fn.moduleName, []);
1703
+ }
1704
+ moduleMap.get(fn.moduleName).push(fn.name);
1705
+ }
1706
+ const lines = [
1707
+ "// Auto-generated by Tether CLI - do not edit manually",
1708
+ `// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1709
+ "",
1710
+ "import { createApiProxy } from '@tthr/client';",
1711
+ "",
1712
+ "/**",
1713
+ " * API function reference type for useQuery/useMutation.",
1714
+ ' * The _name property contains the function path (e.g., "users.list").',
1715
+ " */",
1716
+ "export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
1717
+ " _name: string;",
1718
+ " _args?: TArgs;",
1719
+ " _result?: TResult;",
1720
+ "}",
1721
+ ""
1722
+ ];
1723
+ if (moduleMap.size > 0) {
1724
+ for (const [moduleName, fnNames] of moduleMap) {
1725
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
1726
+ lines.push(`export interface ${interfaceName} {`);
1727
+ for (const fnName of fnNames) {
1728
+ lines.push(` ${fnName}: ApiFunction;`);
1729
+ }
1730
+ lines.push("}");
1731
+ lines.push("");
1732
+ }
1733
+ lines.push("export interface Api {");
1734
+ for (const moduleName of moduleMap.keys()) {
1735
+ const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
1736
+ lines.push(` ${moduleName}: ${interfaceName};`);
1737
+ }
1738
+ lines.push("}");
1739
+ } else {
1740
+ lines.push("/**");
1741
+ lines.push(" * Flexible API type that allows access to any function path.");
1742
+ lines.push(" * Functions are accessed as api.moduleName.functionName");
1743
+ lines.push(" * The actual types depend on your function definitions.");
1744
+ lines.push(" */");
1745
+ lines.push("export type Api = {");
1746
+ lines.push(" [module: string]: {");
1747
+ lines.push(" [fn: string]: ApiFunction;");
1748
+ lines.push(" };");
1749
+ lines.push("};");
1750
+ }
1751
+ lines.push("");
1752
+ lines.push("// API client proxy - provides typed access to your functions");
1753
+ lines.push("// On the client: returns { _name } references for useQuery/useMutation");
1754
+ lines.push("// In Tether functions: calls the actual function implementation");
1755
+ lines.push("export const api = createApiProxy<Api>();");
1756
+ lines.push("");
1757
+ return lines.join("\n");
1758
+ }
1570
1759
  async function generateCommand() {
1571
1760
  await requireAuth();
1572
1761
  const configPath = path6.resolve(process.cwd(), "tether.config.ts");
@@ -1581,71 +1770,29 @@ async function generateCommand() {
1581
1770
  const config = await loadConfig();
1582
1771
  const schemaPath = resolvePath(config.schema);
1583
1772
  const outputDir = resolvePath(config.output);
1773
+ const functionsDir = resolvePath(config.functions);
1584
1774
  if (!await fs6.pathExists(schemaPath)) {
1585
1775
  spinner.fail("Schema file not found");
1586
1776
  console.log(chalk5.dim(`Expected: ${schemaPath}
1587
1777
  `));
1588
1778
  process.exit(1);
1589
1779
  }
1590
- spinner.text = "Generating types...";
1780
+ const schemaSource = await fs6.readFile(schemaPath, "utf-8");
1781
+ const tables = parseSchemaFile(schemaSource);
1782
+ if (tables.length === 0) {
1783
+ spinner.warn("No tables found in schema");
1784
+ console.log(chalk5.dim(" Make sure your schema uses defineSchema({ ... })\n"));
1785
+ return;
1786
+ }
1787
+ spinner.text = `Generating types for ${tables.length} table(s)...`;
1591
1788
  await fs6.ensureDir(outputDir);
1592
1789
  await fs6.writeFile(
1593
1790
  path6.join(outputDir, "db.ts"),
1594
- `// Auto-generated by Tether CLI - do not edit manually
1595
- // Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
1596
-
1597
- import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';
1598
-
1599
- export interface Post {
1600
- id: string;
1601
- title: string;
1602
- content: string | null;
1603
- authorId: string;
1604
- createdAt: string;
1605
- updatedAt: string;
1606
- }
1607
-
1608
- export interface Comment {
1609
- id: string;
1610
- postId: string;
1611
- content: string;
1612
- authorId: string;
1613
- createdAt: string;
1614
- }
1615
-
1616
- export interface Schema {
1617
- posts: Post;
1618
- comments: Comment;
1619
- }
1620
-
1621
- // Database client with typed tables
1622
- // This is a proxy that will be populated by the Tether runtime
1623
- export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();
1624
- `
1791
+ generateDbFile(tables)
1625
1792
  );
1626
1793
  await fs6.writeFile(
1627
1794
  path6.join(outputDir, "api.ts"),
1628
- `// Auto-generated by Tether CLI - do not edit manually
1629
- // Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
1630
-
1631
- import { createApiProxy } from '@tthr/client';
1632
- import type { Schema } from './db';
1633
-
1634
- export interface PostsApi {
1635
- list: (args?: { limit?: number }) => Promise<Schema['posts'][]>;
1636
- get: (args: { id: string }) => Promise<Schema['posts'] | null>;
1637
- create: (args: { title: string; content?: string }) => Promise<{ id: string }>;
1638
- update: (args: { id: string; title?: string; content?: string }) => Promise<void>;
1639
- remove: (args: { id: string }) => Promise<void>;
1640
- }
1641
-
1642
- export interface Api {
1643
- posts: PostsApi;
1644
- }
1645
-
1646
- // API client proxy - will be populated by the Tether runtime
1647
- export const api: Api = createApiProxy<Api>();
1648
- `
1795
+ await generateApiFile(functionsDir)
1649
1796
  );
1650
1797
  await fs6.writeFile(
1651
1798
  path6.join(outputDir, "index.ts"),
@@ -1654,7 +1801,11 @@ export * from './db';
1654
1801
  export * from './api';
1655
1802
  `
1656
1803
  );
1657
- spinner.succeed("Types generated");
1804
+ spinner.succeed(`Types generated for ${tables.length} table(s)`);
1805
+ console.log("\n" + chalk5.green("\u2713") + " Tables:");
1806
+ for (const table of tables) {
1807
+ console.log(chalk5.dim(` - ${table.name} (${table.columns.length} columns)`));
1808
+ }
1658
1809
  const relativeOutput = path6.relative(process.cwd(), outputDir);
1659
1810
  console.log("\n" + chalk5.green("\u2713") + " Generated files:");
1660
1811
  console.log(chalk5.dim(` ${relativeOutput}/db.ts`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tthr",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "description": "Tether CLI - project scaffolding, migrations, and deployment",
5
5
  "type": "module",
6
6
  "bin": {