@takyonic/cli 1.0.2
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 +37 -0
- package/dist/commands/generate.d.ts +3 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +107 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +77 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/pull.d.ts +3 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/pull.js +100 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/push.d.ts +3 -0
- package/dist/commands/push.d.ts.map +1 -0
- package/dist/commands/push.js +135 -0
- package/dist/commands/push.js.map +1 -0
- package/dist/dsl-to-json.d.ts +28 -0
- package/dist/dsl-to-json.d.ts.map +1 -0
- package/dist/dsl-to-json.js +69 -0
- package/dist/dsl-to-json.js.map +1 -0
- package/dist/generator.d.ts +19 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +161 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +36 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +234 -0
- package/dist/parser.js.map +1 -0
- package/dist/type-mapper.d.ts +17 -0
- package/dist/type-mapper.d.ts.map +1 -0
- package/dist/type-mapper.js +90 -0
- package/dist/type-mapper.js.map +1 -0
- package/generated/index.ts +89 -0
- package/package.json +33 -0
- package/src/commands/generate.ts +78 -0
- package/src/commands/init.ts +63 -0
- package/src/commands/inspect.ts +98 -0
- package/src/commands/pull.ts +68 -0
- package/src/commands/push.ts +106 -0
- package/src/dsl-to-json.ts +99 -0
- package/src/generator.ts +178 -0
- package/src/index.ts +32 -0
- package/src/parser.ts +285 -0
- package/src/test-bulk.ts +38 -0
- package/src/test-load.ts +28 -0
- package/src/type-mapper.ts +90 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL to JSON Converter
|
|
3
|
+
* Converts parsed Takyonic DSL schema to JSON format expected by server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TakyonicSchema, TableDefinition } from './parser';
|
|
7
|
+
import { dslTypeToPostgres } from './type-mapper';
|
|
8
|
+
|
|
9
|
+
export interface ColumnSchema {
|
|
10
|
+
name: string;
|
|
11
|
+
data_type: string;
|
|
12
|
+
nullable: boolean;
|
|
13
|
+
default_value?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IndexInfo {
|
|
17
|
+
name: string;
|
|
18
|
+
columns: string[];
|
|
19
|
+
unique: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TableSchema {
|
|
23
|
+
name: string;
|
|
24
|
+
columns: ColumnSchema[];
|
|
25
|
+
indexes?: IndexInfo[];
|
|
26
|
+
foreign_keys?: any[];
|
|
27
|
+
cache_ttl?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert Takyonic DSL schema to server JSON format
|
|
32
|
+
*/
|
|
33
|
+
export function dslToJson(schema: TakyonicSchema): TableSchema[] {
|
|
34
|
+
return schema.tables.map(table => convertTable(table));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert a single table definition to server format
|
|
39
|
+
*/
|
|
40
|
+
function convertTable(table: TableDefinition): TableSchema {
|
|
41
|
+
const columns: ColumnSchema[] = [];
|
|
42
|
+
const indexes: IndexInfo[] = [];
|
|
43
|
+
const primaryKeyColumns: string[] = [];
|
|
44
|
+
|
|
45
|
+
// Convert fields to columns
|
|
46
|
+
for (const field of table.fields) {
|
|
47
|
+
const postgresType = dslTypeToPostgres(field.type);
|
|
48
|
+
|
|
49
|
+
// Check if field is nullable (default to false, unless explicitly marked)
|
|
50
|
+
// For now, we'll assume all fields are non-nullable unless we add nullable decorator
|
|
51
|
+
const nullable = false;
|
|
52
|
+
|
|
53
|
+
columns.push({
|
|
54
|
+
name: field.name,
|
|
55
|
+
data_type: postgresType,
|
|
56
|
+
nullable,
|
|
57
|
+
default_value: field.decorators.default,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Track primary key fields
|
|
61
|
+
if (field.decorators.primary) {
|
|
62
|
+
primaryKeyColumns.push(field.name);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create primary key index if we have primary key fields
|
|
67
|
+
if (primaryKeyColumns.length > 0) {
|
|
68
|
+
indexes.push({
|
|
69
|
+
name: `${table.name}_pkey`,
|
|
70
|
+
columns: primaryKeyColumns,
|
|
71
|
+
unique: true,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Always ensure we have an 'id' column for Takyonic (if not present, add it)
|
|
76
|
+
const hasIdColumn = columns.some(col => col.name === 'id');
|
|
77
|
+
if (!hasIdColumn && primaryKeyColumns.length === 0) {
|
|
78
|
+
// Add id column if no primary key is defined
|
|
79
|
+
columns.unshift({
|
|
80
|
+
name: 'id',
|
|
81
|
+
data_type: 'text',
|
|
82
|
+
nullable: false,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
indexes.push({
|
|
86
|
+
name: `${table.name}_pkey`,
|
|
87
|
+
columns: ['id'],
|
|
88
|
+
unique: true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: table.name,
|
|
94
|
+
columns,
|
|
95
|
+
indexes: indexes.length > 0 ? indexes : undefined,
|
|
96
|
+
foreign_keys: [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
package/src/generator.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
interface ColumnSchema {
|
|
2
|
+
name: string;
|
|
3
|
+
data_type: string;
|
|
4
|
+
nullable: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface TableSchema {
|
|
8
|
+
name: string;
|
|
9
|
+
columns: ColumnSchema[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Maps PostgreSQL data types to TypeScript types
|
|
14
|
+
*/
|
|
15
|
+
function mapPostgresToTypeScript(dataType: string): string {
|
|
16
|
+
const normalized = dataType.toLowerCase();
|
|
17
|
+
|
|
18
|
+
switch (normalized) {
|
|
19
|
+
case 'text':
|
|
20
|
+
case 'varchar':
|
|
21
|
+
case 'character varying':
|
|
22
|
+
case 'char':
|
|
23
|
+
case 'uuid':
|
|
24
|
+
return 'string';
|
|
25
|
+
case 'integer':
|
|
26
|
+
case 'int':
|
|
27
|
+
case 'int4':
|
|
28
|
+
case 'bigint':
|
|
29
|
+
case 'int8':
|
|
30
|
+
case 'numeric':
|
|
31
|
+
case 'decimal':
|
|
32
|
+
case 'double precision':
|
|
33
|
+
case 'real':
|
|
34
|
+
case 'float':
|
|
35
|
+
case 'smallint':
|
|
36
|
+
case 'int2':
|
|
37
|
+
return 'number';
|
|
38
|
+
case 'boolean':
|
|
39
|
+
case 'bool':
|
|
40
|
+
return 'boolean';
|
|
41
|
+
case 'jsonb':
|
|
42
|
+
case 'json':
|
|
43
|
+
return 'Record<string, unknown>';
|
|
44
|
+
case 'timestamp':
|
|
45
|
+
case 'timestamp without time zone':
|
|
46
|
+
case 'timestamp with time zone':
|
|
47
|
+
case 'date':
|
|
48
|
+
case 'time':
|
|
49
|
+
case 'time without time zone':
|
|
50
|
+
case 'time with time zone':
|
|
51
|
+
return 'Date';
|
|
52
|
+
default:
|
|
53
|
+
return 'unknown';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Converts table name to PascalCase for interface names
|
|
59
|
+
*/
|
|
60
|
+
function toPascalCase(str: string): string {
|
|
61
|
+
return str
|
|
62
|
+
.split('_')
|
|
63
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
64
|
+
.join('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generates TypeScript interface for a table
|
|
69
|
+
*/
|
|
70
|
+
function generateInterface(table: TableSchema): string {
|
|
71
|
+
const interfaceName = toPascalCase(table.name);
|
|
72
|
+
const columns = table.columns
|
|
73
|
+
.map(col => {
|
|
74
|
+
const tsType = mapPostgresToTypeScript(col.data_type);
|
|
75
|
+
const optional = col.nullable ? '?' : '';
|
|
76
|
+
return ` ${col.name}${optional}: ${tsType};`;
|
|
77
|
+
})
|
|
78
|
+
.join('\n');
|
|
79
|
+
|
|
80
|
+
return `export interface ${interfaceName} {\n${columns}\n}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generates TypeScript code from schema
|
|
85
|
+
*
|
|
86
|
+
* This generates type definitions that work with the @takyonic/sdk package.
|
|
87
|
+
* Instead of generating a full client, it produces interfaces that can be
|
|
88
|
+
* used with the SDK's generic table methods.
|
|
89
|
+
*/
|
|
90
|
+
export function generateTypeScript(schema: TableSchema[]): string {
|
|
91
|
+
const interfaces = schema.map(generateInterface).join('\n\n');
|
|
92
|
+
|
|
93
|
+
// Generate table name type union
|
|
94
|
+
const tableNames = schema.map(t => `'${t.name}'`).join(' | ');
|
|
95
|
+
|
|
96
|
+
// Generate table type mapping
|
|
97
|
+
const tableTypeMap = schema
|
|
98
|
+
.map(t => ` ${t.name}: ${toPascalCase(t.name)};`)
|
|
99
|
+
.join('\n');
|
|
100
|
+
|
|
101
|
+
// Generate usage examples
|
|
102
|
+
const exampleTable = schema[0];
|
|
103
|
+
const exampleType = exampleTable ? toPascalCase(exampleTable.name) : 'YourTable';
|
|
104
|
+
const exampleName = exampleTable ? exampleTable.name : 'your_table';
|
|
105
|
+
|
|
106
|
+
const code = `/**
|
|
107
|
+
* Auto-generated by Takyonic CLI
|
|
108
|
+
*
|
|
109
|
+
* This file contains TypeScript interfaces for your database schema.
|
|
110
|
+
* Use these types with the @takyonic/sdk package for type-safe queries.
|
|
111
|
+
*
|
|
112
|
+
* Do not edit this file manually - regenerate with: takyonic-cli generate
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
import type { TakyonicClient } from '@takyonic/sdk';
|
|
116
|
+
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// Table Interfaces
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
${interfaces}
|
|
122
|
+
|
|
123
|
+
// =============================================================================
|
|
124
|
+
// Type Utilities
|
|
125
|
+
// =============================================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Union type of all table names in the schema
|
|
129
|
+
*/
|
|
130
|
+
export type TableName = ${tableNames || 'never'};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Mapping from table names to their interface types
|
|
134
|
+
*/
|
|
135
|
+
export interface TableTypes {
|
|
136
|
+
${tableTypeMap}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Helper type to get the interface type for a table name
|
|
141
|
+
*/
|
|
142
|
+
export type TableType<T extends TableName> = TableTypes[T];
|
|
143
|
+
|
|
144
|
+
// =============================================================================
|
|
145
|
+
// Usage Examples
|
|
146
|
+
// =============================================================================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Example usage with @takyonic/sdk:
|
|
150
|
+
*
|
|
151
|
+
* \`\`\`typescript
|
|
152
|
+
* import { TakyonicClient } from '@takyonic/sdk';
|
|
153
|
+
* import type { ${exampleType} } from './generated';
|
|
154
|
+
*
|
|
155
|
+
* const db = new TakyonicClient({
|
|
156
|
+
* endpoint: 'http://localhost:8080',
|
|
157
|
+
* token: process.env.TAKYONIC_API_KEY!
|
|
158
|
+
* });
|
|
159
|
+
*
|
|
160
|
+
* // Type-safe queries
|
|
161
|
+
* const records = await db.table<${exampleType}>('${exampleName}').get();
|
|
162
|
+
*
|
|
163
|
+
* // Type-safe inserts
|
|
164
|
+
* await db.table<${exampleType}>('${exampleName}').insert({
|
|
165
|
+
* id: 'new-id',
|
|
166
|
+
* // ... other fields
|
|
167
|
+
* });
|
|
168
|
+
*
|
|
169
|
+
* // Type-safe search with filters
|
|
170
|
+
* const filtered = await db.table<${exampleType}>('${exampleName}')
|
|
171
|
+
* .where('id', '=', 'some-id')
|
|
172
|
+
* .get();
|
|
173
|
+
* \`\`\`
|
|
174
|
+
*/
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
return code;
|
|
178
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { inspectCommand } from './commands/inspect';
|
|
6
|
+
import { initCommand } from './commands/init';
|
|
7
|
+
import { generateCommand } from './commands/generate';
|
|
8
|
+
import { pullCommand } from './commands/pull';
|
|
9
|
+
import { pushCommand } from './commands/push';
|
|
10
|
+
|
|
11
|
+
// Load environment variables
|
|
12
|
+
dotenv.config();
|
|
13
|
+
|
|
14
|
+
const program = new Command();
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name('takyonic-cli')
|
|
18
|
+
.description('⚡ Takyonic CLI - High-performance Data Framework for PostgreSQL')
|
|
19
|
+
.version('1.0.0');
|
|
20
|
+
|
|
21
|
+
program.addCommand(initCommand);
|
|
22
|
+
program.addCommand(generateCommand);
|
|
23
|
+
program.addCommand(pullCommand);
|
|
24
|
+
program.addCommand(pushCommand);
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command('inspect')
|
|
28
|
+
.description('Inspect database schema via Takyonic server API')
|
|
29
|
+
.action(inspectCommand);
|
|
30
|
+
|
|
31
|
+
program.parse();
|
|
32
|
+
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Takyonic DSL Parser
|
|
3
|
+
* Parses .takyonic schema files into structured data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface EngineConfig {
|
|
7
|
+
provider: string;
|
|
8
|
+
db?: string;
|
|
9
|
+
cache?: string;
|
|
10
|
+
port?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TableField {
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
decorators: {
|
|
17
|
+
primary?: boolean;
|
|
18
|
+
default?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TableDefinition {
|
|
23
|
+
name: string;
|
|
24
|
+
fields: TableField[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TakyonicSchema {
|
|
28
|
+
engine: EngineConfig;
|
|
29
|
+
tables: TableDefinition[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ParseError extends Error {
|
|
33
|
+
constructor(message: string, public line?: number, public column?: number) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = 'ParseError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a Takyonic DSL file into structured schema
|
|
41
|
+
*/
|
|
42
|
+
export function parseTakyonicSchema(content: string): TakyonicSchema {
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
let currentLine = 0;
|
|
45
|
+
let currentColumn = 0;
|
|
46
|
+
|
|
47
|
+
const schema: TakyonicSchema = {
|
|
48
|
+
engine: { provider: 'takyonic' },
|
|
49
|
+
tables: [],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < lines.length) {
|
|
54
|
+
const line = lines[i].trim();
|
|
55
|
+
currentLine = i + 1;
|
|
56
|
+
|
|
57
|
+
// Skip empty lines and comments
|
|
58
|
+
if (!line || line.startsWith('//')) {
|
|
59
|
+
i++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parse engine block
|
|
64
|
+
if (line.startsWith('engine')) {
|
|
65
|
+
const engineEnd = findBlockEnd(lines, i);
|
|
66
|
+
schema.engine = parseEngineBlock(lines.slice(i, engineEnd + 1));
|
|
67
|
+
i = engineEnd + 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Parse table definitions
|
|
72
|
+
if (line.startsWith('table')) {
|
|
73
|
+
const tableEnd = findBlockEnd(lines, i);
|
|
74
|
+
const table = parseTableBlock(lines.slice(i, tableEnd + 1));
|
|
75
|
+
schema.tables.push(table);
|
|
76
|
+
i = tableEnd + 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Validate engine has provider
|
|
84
|
+
if (!schema.engine.provider) {
|
|
85
|
+
throw new ParseError('Engine block must specify provider', currentLine);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (schema.engine.provider !== 'takyonic') {
|
|
89
|
+
throw new ParseError('Provider must be "takyonic"', currentLine);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return schema;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Find the closing brace for a block starting at startLine
|
|
97
|
+
*/
|
|
98
|
+
function findBlockEnd(lines: string[], startLine: number): number {
|
|
99
|
+
let depth = 0;
|
|
100
|
+
let foundOpen = false;
|
|
101
|
+
|
|
102
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
103
|
+
const line = lines[i];
|
|
104
|
+
for (let j = 0; j < line.length; j++) {
|
|
105
|
+
if (line[j] === '{') {
|
|
106
|
+
depth++;
|
|
107
|
+
foundOpen = true;
|
|
108
|
+
} else if (line[j] === '}') {
|
|
109
|
+
depth--;
|
|
110
|
+
if (foundOpen && depth === 0) {
|
|
111
|
+
return i;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw new ParseError('Unclosed block', startLine + 1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse engine block
|
|
122
|
+
*/
|
|
123
|
+
function parseEngineBlock(lines: string[]): EngineConfig {
|
|
124
|
+
const config: EngineConfig = { provider: 'takyonic' };
|
|
125
|
+
const content = lines.join('\n');
|
|
126
|
+
|
|
127
|
+
// Extract provider
|
|
128
|
+
const providerMatch = content.match(/provider\s*=\s*"([^"]+)"/);
|
|
129
|
+
if (providerMatch) {
|
|
130
|
+
config.provider = providerMatch[1];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Extract db
|
|
134
|
+
const dbMatch = content.match(/db\s*=\s*(env\([^)]+\)|"[^"]+")/);
|
|
135
|
+
if (dbMatch) {
|
|
136
|
+
config.db = dbMatch[1];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Extract cache
|
|
140
|
+
const cacheMatch = content.match(/cache\s*=\s*"([^"]+)"/);
|
|
141
|
+
if (cacheMatch) {
|
|
142
|
+
config.cache = cacheMatch[1];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Extract port
|
|
146
|
+
const portMatch = content.match(/port\s*=\s*(\d+)/);
|
|
147
|
+
if (portMatch) {
|
|
148
|
+
config.port = parseInt(portMatch[1], 10);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return config;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Parse table block
|
|
156
|
+
*/
|
|
157
|
+
function parseTableBlock(lines: string[]): TableDefinition {
|
|
158
|
+
// Extract table name from first line
|
|
159
|
+
const firstLine = lines[0].trim();
|
|
160
|
+
const tableNameMatch = firstLine.match(/table\s+(\w+)\s*\{/);
|
|
161
|
+
if (!tableNameMatch) {
|
|
162
|
+
throw new ParseError('Invalid table definition', 1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tableName = tableNameMatch[1];
|
|
166
|
+
const fields: TableField[] = [];
|
|
167
|
+
|
|
168
|
+
// Parse field definitions (skip first and last lines which are table declaration and closing brace)
|
|
169
|
+
for (let i = 1; i < lines.length - 1; i++) {
|
|
170
|
+
const line = lines[i].trim();
|
|
171
|
+
|
|
172
|
+
// Skip empty lines and comments
|
|
173
|
+
if (!line || line.startsWith('//')) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const field = parseFieldLine(line);
|
|
178
|
+
if (field) {
|
|
179
|
+
fields.push(field);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
name: tableName,
|
|
185
|
+
fields,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Parse a single field line
|
|
191
|
+
* Supports both formats:
|
|
192
|
+
* - Space syntax: "id string @primary"
|
|
193
|
+
* - Colon syntax: "id: string @primary"
|
|
194
|
+
* Example: "price int @default(now())"
|
|
195
|
+
*/
|
|
196
|
+
function parseFieldLine(line: string): TableField | null {
|
|
197
|
+
// Remove comments
|
|
198
|
+
const cleanLine = line.split('//')[0].trim();
|
|
199
|
+
if (!cleanLine) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Extract decorators first (they start with @)
|
|
204
|
+
const decorators: TableField['decorators'] = {};
|
|
205
|
+
const decoratorPattern = /@(\w+)/g;
|
|
206
|
+
let decoratorMatch;
|
|
207
|
+
|
|
208
|
+
while ((decoratorMatch = decoratorPattern.exec(cleanLine)) !== null) {
|
|
209
|
+
const decoratorName = decoratorMatch[1];
|
|
210
|
+
const decoratorStart = decoratorMatch.index + decoratorMatch[0].length;
|
|
211
|
+
|
|
212
|
+
if (decoratorName === 'primary') {
|
|
213
|
+
decorators.primary = true;
|
|
214
|
+
} else if (decoratorName === 'default') {
|
|
215
|
+
// Extract the value with balanced parentheses
|
|
216
|
+
const valueStart = cleanLine.indexOf('(', decoratorStart);
|
|
217
|
+
if (valueStart !== -1) {
|
|
218
|
+
const value = extractBalancedParens(cleanLine, valueStart);
|
|
219
|
+
if (value) {
|
|
220
|
+
// Remove outer quotes if present
|
|
221
|
+
decorators.default = value.replace(/^["']|["']$/g, '');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Remove all decorators from the line to parse name and type
|
|
228
|
+
const lineWithoutDecorators = cleanLine.replace(/@\w+(\([^)]*\)|\(.*?\)\))?/g, '').trim();
|
|
229
|
+
|
|
230
|
+
// Handle both "name: type" and "name type" formats
|
|
231
|
+
let name: string;
|
|
232
|
+
let type: string;
|
|
233
|
+
|
|
234
|
+
if (lineWithoutDecorators.includes(':')) {
|
|
235
|
+
// Colon syntax: "id: string" or "id : string"
|
|
236
|
+
const colonParts = lineWithoutDecorators.split(':').map(p => p.trim());
|
|
237
|
+
if (colonParts.length < 2 || !colonParts[0] || !colonParts[1]) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
name = colonParts[0];
|
|
241
|
+
type = colonParts[1].split(/\s+/)[0]; // Take first word after colon as type
|
|
242
|
+
} else {
|
|
243
|
+
// Space syntax: "id string"
|
|
244
|
+
const parts = lineWithoutDecorators.split(/\s+/);
|
|
245
|
+
if (parts.length < 2) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
name = parts[0];
|
|
249
|
+
type = parts[1];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
name,
|
|
254
|
+
type,
|
|
255
|
+
decorators,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Extract content within balanced parentheses
|
|
261
|
+
* For input "@default(now())" starting at '(' position, returns "now()"
|
|
262
|
+
*/
|
|
263
|
+
function extractBalancedParens(str: string, startPos: number): string | null {
|
|
264
|
+
if (str[startPos] !== '(') {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let depth = 0;
|
|
269
|
+
let start = startPos + 1;
|
|
270
|
+
|
|
271
|
+
for (let i = startPos; i < str.length; i++) {
|
|
272
|
+
if (str[i] === '(') {
|
|
273
|
+
depth++;
|
|
274
|
+
} else if (str[i] === ')') {
|
|
275
|
+
depth--;
|
|
276
|
+
if (depth === 0) {
|
|
277
|
+
return str.substring(start, i);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Unbalanced parentheses - return what we have
|
|
283
|
+
return str.substring(start);
|
|
284
|
+
}
|
|
285
|
+
|
package/src/test-bulk.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
const TOTAL_RECORDS = 10000;
|
|
4
|
+
const BATCH_SIZE = 1000; // Her istekte 1000 kayıt
|
|
5
|
+
|
|
6
|
+
async function runBulkTest() {
|
|
7
|
+
console.log(`🚀 Takyonic BULK Test: Sending ${TOTAL_RECORDS} records in batches of ${BATCH_SIZE}...`);
|
|
8
|
+
const startTime = Date.now();
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < TOTAL_RECORDS; i += BATCH_SIZE) {
|
|
11
|
+
const batch = Array.from({ length: BATCH_SIZE }).map((_, j) => ({
|
|
12
|
+
id: `B-${i + j}`,
|
|
13
|
+
event: 'bulk_test_event',
|
|
14
|
+
payload: { value: Math.random(), batch_group: i / BATCH_SIZE },
|
|
15
|
+
severity: Math.floor(Math.random() * 5) + 1
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await axios.post(`http://localhost:8080/v1/bulk-write/logs`, batch, {
|
|
20
|
+
headers: { 'Authorization': 'Bearer takyonic_admin_secret' }
|
|
21
|
+
});
|
|
22
|
+
console.log(`✅ Sent batch starting at ${i}...`);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (axios.isAxiosError(error)) {
|
|
25
|
+
console.error(`❌ Error at batch ${i}:`, error.response?.data || error.message);
|
|
26
|
+
} else if (error instanceof Error) {
|
|
27
|
+
console.error(`❌ Error at batch ${i}:`, error.message);
|
|
28
|
+
} else {
|
|
29
|
+
console.error(`❌ Error at batch ${i}:`, String(error));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
35
|
+
console.log(`🏁 Bulk test completed in ${duration} seconds!`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
runBulkTest();
|
package/src/test-load.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
const TOTAL_RECORDS = 10000;
|
|
4
|
+
const BATCH_SIZE = 100; // 100'erli gruplar halinde gönderelim
|
|
5
|
+
|
|
6
|
+
async function runLoadTest() {
|
|
7
|
+
console.log(`⚡ Takyonic Load Test: Sending ${TOTAL_RECORDS} logs...`);
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < TOTAL_RECORDS; i += BATCH_SIZE) {
|
|
10
|
+
const promises = Array.from({ length: BATCH_SIZE }).map((_, j) => {
|
|
11
|
+
const id = `L-${i + j}`;
|
|
12
|
+
return axios.post('http://localhost:8080/v1/write/logs', {
|
|
13
|
+
id,
|
|
14
|
+
event: 'load_test_event',
|
|
15
|
+
payload: { value: Math.random(), batch: i },
|
|
16
|
+
severity: Math.floor(Math.random() * 5)
|
|
17
|
+
}, {
|
|
18
|
+
headers: { 'Authorization': 'Bearer takyonic_admin_secret' }
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await Promise.all(promises);
|
|
23
|
+
if (i % 1000 === 0) console.log(`✅ Sent ${i} records...`);
|
|
24
|
+
}
|
|
25
|
+
console.log('🏁 Load test completed!');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
runLoadTest();
|