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.
- package/dist/index.js +205 -54
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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`));
|