startx 0.9.2 → 0.9.8
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/apps/core-server/package.json +4 -3
- package/apps/core-server/src/events/index.ts +19 -19
- package/apps/core-server/src/index.ts +1 -3
- package/apps/startx-cli/dist/index.mjs +52 -52
- package/configs/eslint-config/src/configs/base.ts +26 -11
- package/package.json +2 -2
- package/packages/{@repo/db → @db/drizzle}/package.json +1 -1
- package/packages/@db/drizzle/tsconfig.json +7 -0
- package/packages/@db/sqlite/package.json +47 -0
- package/packages/@db/sqlite/src/index.ts +2 -0
- package/packages/@db/sqlite/src/lib/sqlite-client.ts +146 -0
- package/packages/@db/sqlite/src/lib/sqlite-convertor.ts +235 -0
- package/packages/@db/sqlite/tsconfig.json +7 -0
- package/packages/@repo/env/package.json +2 -1
- package/packages/@repo/env/src/utils.ts +7 -6
- package/packages/@repo/lib/src/events/i-event.ts +59 -0
- package/packages/@repo/lib/src/events/index.ts +1 -0
- package/packages/aix/eslint.config.ts +4 -0
- package/packages/aix/package.json +53 -0
- package/packages/aix/src/aix.ts +519 -0
- package/packages/aix/src/index.ts +3 -0
- package/packages/aix/src/lib/convertor/schema-convertor.ts +108 -0
- package/packages/aix/src/lib/convertor/variable-resolver.ts +161 -0
- package/packages/aix/src/lib/tokenizer/index.ts +1 -0
- package/packages/aix/src/lib/tokenizer/tokenizer.ts +42 -0
- package/packages/aix/src/providers/ai-chat.ts +25 -0
- package/packages/aix/src/providers/ai-event.ts +21 -0
- package/packages/aix/src/providers/ai-interface.ts +236 -0
- package/packages/aix/src/providers/ai-prompt.ts +14 -0
- package/packages/aix/src/providers/default-models.ts +471 -0
- package/packages/aix/src/providers/index.ts +1 -0
- package/packages/aix/src/providers/openai/openai.ts +139 -0
- package/packages/aix/src/providers/providers.ts +39 -0
- package/packages/aix/src/providers/types.ts +54 -0
- package/packages/aix/src/tools/generic/database.ts +290 -0
- package/packages/aix/src/tools/generic/forecast.ts +216 -0
- package/packages/aix/src/tools/generic/index.ts +4 -0
- package/packages/aix/src/tools/generic/planner.ts +114 -0
- package/packages/aix/src/tools/generic/summarizer.ts +101 -0
- package/packages/aix/src/tools/i-tool.ts +33 -0
- package/packages/aix/src/tools/index.ts +2 -0
- package/packages/aix/src/tools/system/index.ts +297 -0
- package/packages/aix/src/tools/tool-manager.ts +241 -0
- package/packages/aix/src/tools/types.ts +109 -0
- package/packages/aix/tsconfig.json +7 -0
- package/packages/aix/vitest.config.ts +3 -0
- package/packages/constants/eslint.config.ts +4 -0
- package/packages/{@repo/constants → constants}/package.json +1 -1
- package/packages/constants/vitest.config.ts +3 -0
- package/pnpm-workspace.yaml +12 -1
- package/turbo.json +0 -1
- package/packages/@repo/db/tsconfig.json +0 -13
- /package/packages/{@repo/db → @db/drizzle}/drizzle.config.ts +0 -0
- /package/packages/{@repo/constants → @db/drizzle}/eslint.config.ts +0 -0
- /package/packages/{@repo/db → @db/drizzle}/src/functions.ts +0 -0
- /package/packages/{@repo/db → @db/drizzle}/src/index.ts +0 -0
- /package/packages/{@repo/db → @db/drizzle}/src/schema/common.ts +0 -0
- /package/packages/{@repo/db → @db/drizzle}/src/schema/index.ts +0 -0
- /package/packages/{@repo/constants → @db/drizzle}/vitest.config.ts +0 -0
- /package/packages/{@repo/db → @db/sqlite}/eslint.config.ts +0 -0
- /package/packages/{@repo/db → @db/sqlite}/vitest.config.ts +0 -0
- /package/packages/{@repo/constants → constants}/src/api.ts +0 -0
- /package/packages/{@repo/constants → constants}/src/index.ts +0 -0
- /package/packages/{@repo/constants → constants}/src/time.ts +0 -0
- /package/packages/{@repo/constants → constants}/tsconfig.json +0 -0
|
@@ -54,6 +54,16 @@ export const baseConfig = tseslint.config(
|
|
|
54
54
|
tsconfigRootDir: import.meta.dirname,
|
|
55
55
|
},
|
|
56
56
|
},
|
|
57
|
+
// ADDED: Settings block to correctly resolve TypeScript imports
|
|
58
|
+
settings: {
|
|
59
|
+
"import-x/resolver": {
|
|
60
|
+
typescript: {
|
|
61
|
+
alwaysTryTypes: true,
|
|
62
|
+
project: true,
|
|
63
|
+
},
|
|
64
|
+
node: true,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
57
67
|
rules: {
|
|
58
68
|
// Core Rules
|
|
59
69
|
"no-void": ["error", { allowAsStatement: true }],
|
|
@@ -81,22 +91,27 @@ export const baseConfig = tseslint.config(
|
|
|
81
91
|
// Naming Conventions (Relaxed for APIs and strict for standard code)
|
|
82
92
|
"@typescript-eslint/naming-convention": [
|
|
83
93
|
"warn",
|
|
84
|
-
{ selector: "default", format: ["camelCase"] },
|
|
85
|
-
{ selector: "import", format: ["camelCase", "PascalCase"] },
|
|
94
|
+
{ "selector": "default", "format": ["camelCase"] },
|
|
95
|
+
{ "selector": "import", "format": ["camelCase", "PascalCase"] },
|
|
96
|
+
{
|
|
97
|
+
"selector": "variable",
|
|
98
|
+
"format": ["camelCase", "snake_case", "UPPER_CASE", "PascalCase"],
|
|
99
|
+
"leadingUnderscore": "allow",
|
|
100
|
+
},
|
|
86
101
|
{
|
|
87
|
-
selector: "
|
|
88
|
-
format: ["camelCase"
|
|
89
|
-
leadingUnderscore: "allow",
|
|
102
|
+
"selector": "parameter",
|
|
103
|
+
"format": ["camelCase"],
|
|
104
|
+
"leadingUnderscore": "allow",
|
|
90
105
|
},
|
|
91
106
|
{
|
|
92
|
-
selector: "
|
|
93
|
-
format: ["camelCase"],
|
|
94
|
-
leadingUnderscore: "allow",
|
|
107
|
+
"selector": "classProperty",
|
|
108
|
+
"format": ["camelCase"],
|
|
109
|
+
"leadingUnderscore": "allow",
|
|
95
110
|
},
|
|
96
|
-
{ selector: "typeLike", format: ["PascalCase"] },
|
|
111
|
+
{ "selector": "typeLike", "format": ["PascalCase"] },
|
|
97
112
|
{
|
|
98
|
-
selector: ["objectLiteralProperty", "typeProperty"],
|
|
99
|
-
format: null,
|
|
113
|
+
"selector": ["objectLiteralProperty", "typeProperty"],
|
|
114
|
+
"format": null,
|
|
100
115
|
},
|
|
101
116
|
],
|
|
102
117
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "startx",
|
|
3
3
|
"description": "",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.8",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/avinashid/startx.git"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"start": "turbo start",
|
|
27
27
|
"startx": "node ./apps/startx-cli/dist/index.mjs",
|
|
28
28
|
"backend": "turbo dev --filter=core-server -- ",
|
|
29
|
-
"cli": "turbo cli --filter=
|
|
29
|
+
"cli": "turbo cli --filter=cli -- ",
|
|
30
30
|
"lint": "turbo lint",
|
|
31
31
|
"typecheck": "turbo typecheck",
|
|
32
32
|
"clean": "turbo clean",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@db/sqlite",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./src/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"clean": "rimraf dist .turbo",
|
|
9
|
+
"watch:dev": "pnpm watch",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"format": "biome format --write .",
|
|
12
|
+
"format:check": "biome ci .",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"lint:fix": "eslint . --fix",
|
|
15
|
+
"watch": "tsc -p tsconfig.build.json --watch"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript-config": "workspace:*",
|
|
19
|
+
"vitest-config": "workspace:*",
|
|
20
|
+
"@types/better-sqlite3": "catalog:",
|
|
21
|
+
"eslint-config": "workspace:*"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"better-sqlite3": "catalog:",
|
|
25
|
+
"xlsx": "catalog:",
|
|
26
|
+
"@repo/env": "workspace:*",
|
|
27
|
+
"@repo/logger": "workspace:*"
|
|
28
|
+
},
|
|
29
|
+
"startx": {
|
|
30
|
+
"iTags": [
|
|
31
|
+
"node",
|
|
32
|
+
"backend"
|
|
33
|
+
],
|
|
34
|
+
"gTags": [
|
|
35
|
+
"db"
|
|
36
|
+
],
|
|
37
|
+
"tags": [
|
|
38
|
+
"sqlite"
|
|
39
|
+
],
|
|
40
|
+
"requiredDeps": [
|
|
41
|
+
"@repo/logger"
|
|
42
|
+
],
|
|
43
|
+
"requiredDevDeps": [
|
|
44
|
+
"typescript-config"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { logger } from "@repo/logger";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
class SqliteManager {
|
|
11
|
+
private static connections: Map<string, Database.Database> = new Map();
|
|
12
|
+
|
|
13
|
+
static getDb(dbPath: string): Database.Database {
|
|
14
|
+
const resolvedPath = path.resolve(dbPath);
|
|
15
|
+
|
|
16
|
+
if (!this.connections.has(resolvedPath)) {
|
|
17
|
+
const dir = path.dirname(resolvedPath);
|
|
18
|
+
if (!fs.existsSync(dir)) {
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const db = new Database(resolvedPath);
|
|
23
|
+
db.pragma("journal_mode = WAL");
|
|
24
|
+
db.pragma("synchronous = NORMAL");
|
|
25
|
+
logger.info(`Connected to SQLite database at ${resolvedPath}`);
|
|
26
|
+
this.connections.set(resolvedPath, db);
|
|
27
|
+
}
|
|
28
|
+
return this.connections.get(resolvedPath)!;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static closeDb(dbPath: string): void {
|
|
32
|
+
const resolvedPath = path.resolve(dbPath);
|
|
33
|
+
const db = this.connections.get(resolvedPath);
|
|
34
|
+
|
|
35
|
+
if (db) {
|
|
36
|
+
db.close();
|
|
37
|
+
this.connections.delete(resolvedPath);
|
|
38
|
+
logger.warn(`SQLite connection for ${resolvedPath} closed.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface TableInfoRow {
|
|
44
|
+
cid: number;
|
|
45
|
+
name: string;
|
|
46
|
+
type: string;
|
|
47
|
+
notnull: number;
|
|
48
|
+
dflt_value: string | null;
|
|
49
|
+
pk: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class SqliteModule {
|
|
53
|
+
public db: Database.Database;
|
|
54
|
+
private dbPath: string;
|
|
55
|
+
|
|
56
|
+
constructor(dbPath: string = path.resolve(__dirname, "db-files", "app.db")) {
|
|
57
|
+
this.dbPath = path.resolve(dbPath);
|
|
58
|
+
this.db = SqliteManager.getDb(this.dbPath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private escapeId(identifier: string): string {
|
|
62
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
createTable(tableName: string, schema: Record<string, string>): void {
|
|
66
|
+
const safeTable = this.escapeId(tableName);
|
|
67
|
+
const columns = Object.entries(schema)
|
|
68
|
+
.map(([col, type]) => `${this.escapeId(col)} ${type}`)
|
|
69
|
+
.join(", ");
|
|
70
|
+
|
|
71
|
+
const sql = `CREATE TABLE IF NOT EXISTS ${safeTable} (${columns});`;
|
|
72
|
+
this.db.prepare(sql).run();
|
|
73
|
+
logger.info(`Table '${tableName}' is ready.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
insert(tableName: string, row: Record<string, any>): void {
|
|
77
|
+
const safeTable = this.escapeId(tableName);
|
|
78
|
+
const cols = Object.keys(row)
|
|
79
|
+
.map(c => this.escapeId(c))
|
|
80
|
+
.join(", ");
|
|
81
|
+
const placeholders = Object.keys(row)
|
|
82
|
+
.map(() => "?")
|
|
83
|
+
.join(", ");
|
|
84
|
+
const values = Object.values(row);
|
|
85
|
+
|
|
86
|
+
const sql = `INSERT INTO ${safeTable} (${cols}) VALUES (${placeholders});`;
|
|
87
|
+
this.db.prepare(sql).run(...values);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
readAll<T = unknown>(tableName: string): T[] {
|
|
91
|
+
const safeTable = this.escapeId(tableName);
|
|
92
|
+
return this.db.prepare(`SELECT * FROM ${safeTable};`).all() as T[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
readOne<T = unknown>(tableName: string, where: string, params: any[] = []): T | undefined {
|
|
96
|
+
const safeTable = this.escapeId(tableName);
|
|
97
|
+
return this.db.prepare(`SELECT * FROM ${safeTable} WHERE ${where} LIMIT 1;`).get(...params) as T | undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
update(tableName: string, updates: Record<string, any>, where: string, params: any[] = []): void {
|
|
101
|
+
const safeTable = this.escapeId(tableName);
|
|
102
|
+
const setClause = Object.keys(updates)
|
|
103
|
+
.map(k => `${this.escapeId(k)} = ?`)
|
|
104
|
+
.join(", ");
|
|
105
|
+
const values = [...Object.values(updates), ...params];
|
|
106
|
+
|
|
107
|
+
const sql = `UPDATE ${safeTable} SET ${setClause} WHERE ${where};`;
|
|
108
|
+
this.db.prepare(sql).run(...values);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
delete(tableName: string, where: string, params: any[] = []): void {
|
|
112
|
+
const safeTable = this.escapeId(tableName);
|
|
113
|
+
const sql = `DELETE FROM ${safeTable} WHERE ${where};`;
|
|
114
|
+
this.db.prepare(sql).run(...params);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
listTables(): string[] {
|
|
118
|
+
const rows = this.db
|
|
119
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';`)
|
|
120
|
+
.all() as Array<{ name: string }>;
|
|
121
|
+
return rows.map(r => r.name);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getTableSchema(tableName: string): string | string[] {
|
|
125
|
+
const exists = this.db
|
|
126
|
+
.prepare(`SELECT 1 FROM sqlite_master WHERE type='table' AND name = ? LIMIT 1;`)
|
|
127
|
+
.get(tableName);
|
|
128
|
+
|
|
129
|
+
if (!exists) {
|
|
130
|
+
logger.warn(`Table not found when fetching schema.`);
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const safeName = tableName.replace(/"/g, '""');
|
|
135
|
+
const rows = this.db.prepare(`PRAGMA table_info("${safeName}");`).all() as TableInfoRow[];
|
|
136
|
+
|
|
137
|
+
const header = ["cid", "name", "type", "notnull", "default_value", "primary_key"];
|
|
138
|
+
const content = rows.map(r => `${r.cid} ${r.name} ${r.type} ${!!r.notnull} ${r.dflt_value} ${!!r.pk}`);
|
|
139
|
+
|
|
140
|
+
return [header.join(" "), ...content].join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
close(): void {
|
|
144
|
+
SqliteManager.closeDb(this.dbPath);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import * as XLSX from "xlsx";
|
|
3
|
+
import { SqliteModule } from "./sqlite-client.js";
|
|
4
|
+
|
|
5
|
+
export type InferredKind = "INTEGER" | "REAL" | "BOOLEAN" | "DATE" | "DATETIME" | "TEXT";
|
|
6
|
+
|
|
7
|
+
export interface ColumnDef {
|
|
8
|
+
name: string;
|
|
9
|
+
original: string;
|
|
10
|
+
kind: InferredKind;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const snake = (s: string) =>
|
|
14
|
+
s
|
|
15
|
+
.trim()
|
|
16
|
+
.replace(/\s+/g, "_")
|
|
17
|
+
.replace(/[^A-Za-z0-9_]/g, "_")
|
|
18
|
+
.replace(/__+/g, "_")
|
|
19
|
+
.replace(/^_+|_+$/g, "")
|
|
20
|
+
.toLowerCase();
|
|
21
|
+
|
|
22
|
+
function uniqueNames(headers: string[]): string[] {
|
|
23
|
+
const used = new Map<string, number>();
|
|
24
|
+
return headers.map(h => {
|
|
25
|
+
let base = snake(h || "col");
|
|
26
|
+
if (!base) base = "col";
|
|
27
|
+
let name = base;
|
|
28
|
+
let i = 1;
|
|
29
|
+
while (used.has(name)) {
|
|
30
|
+
name = `${base}_${++i}`;
|
|
31
|
+
}
|
|
32
|
+
used.set(name, 1);
|
|
33
|
+
return name;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isBooleanish(v: unknown): boolean {
|
|
38
|
+
if (typeof v === "boolean") return true;
|
|
39
|
+
if (typeof v === "number") return v === 0 || v === 1;
|
|
40
|
+
if (typeof v === "string") {
|
|
41
|
+
const s = v.trim().toLowerCase();
|
|
42
|
+
return ["true", "false", "yes", "no", "y", "n", "0", "1"].includes(s);
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toBoolean(v: unknown): boolean | null {
|
|
48
|
+
if (v == null || v === "") return null;
|
|
49
|
+
if (typeof v === "boolean") return v;
|
|
50
|
+
if (typeof v === "number") return v !== 0;
|
|
51
|
+
if (typeof v === "string") {
|
|
52
|
+
const s = v.trim().toLowerCase();
|
|
53
|
+
return ["true", "yes", "y", "1"].includes(s) ? true : ["false", "no", "n", "0"].includes(s) ? false : null;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isExcelDate(d: unknown): d is Date {
|
|
59
|
+
return d instanceof Date && !isNaN(d.getTime());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function looksLikeDateString(s: string): boolean {
|
|
63
|
+
return (
|
|
64
|
+
/^(\d{4}-\d{2}-\d{2})(?:[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)?$/.test(s) ||
|
|
65
|
+
/^(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(s)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseDateFlexible(v: unknown): Date | null {
|
|
70
|
+
if (v == null || v === "") return null;
|
|
71
|
+
if (isExcelDate(v)) return v;
|
|
72
|
+
if (typeof v === "number") {
|
|
73
|
+
if (v > 1e12) return new Date(v);
|
|
74
|
+
if (v > 1e9) return new Date(v * 1000);
|
|
75
|
+
const excelEpoch = new Date(Date.UTC(1899, 11, 30));
|
|
76
|
+
const ms = excelEpoch.getTime() + v * 24 * 60 * 60 * 1000;
|
|
77
|
+
const d = new Date(ms);
|
|
78
|
+
return isNaN(d.getTime()) ? null : d;
|
|
79
|
+
}
|
|
80
|
+
if (typeof v === "string" && looksLikeDateString(v.trim())) {
|
|
81
|
+
const d = new Date(v);
|
|
82
|
+
return isNaN(d.getTime()) ? null : d;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function inferKind(values: unknown[]): InferredKind {
|
|
88
|
+
const nonNull = values.filter(v => v !== undefined && v !== null && v !== "");
|
|
89
|
+
if (nonNull.length === 0) return "TEXT";
|
|
90
|
+
|
|
91
|
+
const allDates = nonNull.every(v => isExcelDate(v) || parseDateFlexible(v) !== null);
|
|
92
|
+
if (allDates) {
|
|
93
|
+
const anyHasTime = nonNull.some(v => {
|
|
94
|
+
const d = isExcelDate(v) ? v : parseDateFlexible(v);
|
|
95
|
+
if (!d) return false;
|
|
96
|
+
return d.getUTCHours() !== 0 || d.getUTCMinutes() !== 0 || d.getUTCSeconds() !== 0;
|
|
97
|
+
});
|
|
98
|
+
return anyHasTime ? "DATETIME" : "DATE";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const allBool = nonNull.every(isBooleanish);
|
|
102
|
+
if (allBool) return "BOOLEAN";
|
|
103
|
+
|
|
104
|
+
const allNumbers = nonNull.every(v => typeof v === "number" || (!isNaN(Number(v)) && String(v).trim() !== ""));
|
|
105
|
+
if (allNumbers) {
|
|
106
|
+
const anyFloat = nonNull.some(v => String(v).includes("."));
|
|
107
|
+
return anyFloat ? "REAL" : "INTEGER";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return "TEXT";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function coerceValue(kind: InferredKind, v: unknown, dateAs: "TEXT" | "INTEGER" | "REAL"): unknown {
|
|
114
|
+
if (v === undefined || v === null || v === "") return null;
|
|
115
|
+
switch (kind) {
|
|
116
|
+
case "BOOLEAN": {
|
|
117
|
+
const b = toBoolean(v);
|
|
118
|
+
return b === null ? null : b ? 1 : 0;
|
|
119
|
+
}
|
|
120
|
+
case "INTEGER": {
|
|
121
|
+
const n = Number(v);
|
|
122
|
+
return isFinite(n) ? Math.trunc(n) : null;
|
|
123
|
+
}
|
|
124
|
+
case "REAL": {
|
|
125
|
+
const n = Number(v);
|
|
126
|
+
return isFinite(n) ? n : null;
|
|
127
|
+
}
|
|
128
|
+
case "DATE":
|
|
129
|
+
case "DATETIME": {
|
|
130
|
+
const d = isExcelDate(v) ? v : parseDateFlexible(v);
|
|
131
|
+
if (!d) return null;
|
|
132
|
+
if (dateAs === "TEXT") return kind === "DATE" ? d.toISOString().slice(0, 10) : d.toISOString();
|
|
133
|
+
if (dateAs === "INTEGER") return Math.floor(d.getTime() / 1000);
|
|
134
|
+
return d.getTime();
|
|
135
|
+
}
|
|
136
|
+
default:
|
|
137
|
+
return JSON.stringify(v);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class ExcelToSqlite {
|
|
142
|
+
static importSheetFromFile(
|
|
143
|
+
filePath: string,
|
|
144
|
+
dbPath: string,
|
|
145
|
+
tableName: string,
|
|
146
|
+
sheetName?: string,
|
|
147
|
+
drop = false,
|
|
148
|
+
headerRow = 1,
|
|
149
|
+
dateAs: "TEXT" | "INTEGER" | "REAL" = "TEXT"
|
|
150
|
+
) {
|
|
151
|
+
const workbook = XLSX.read(fs.readFileSync(filePath), {
|
|
152
|
+
type: "buffer",
|
|
153
|
+
cellDates: true,
|
|
154
|
+
raw: false,
|
|
155
|
+
});
|
|
156
|
+
return this.importSheetFromWorkbook(workbook, dbPath, tableName, sheetName, drop, headerRow, dateAs);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static importSheetFromBuffer(
|
|
160
|
+
buffer: Buffer,
|
|
161
|
+
dbPath: string,
|
|
162
|
+
tableName: string,
|
|
163
|
+
sheetName?: string,
|
|
164
|
+
drop = false,
|
|
165
|
+
headerRow = 1,
|
|
166
|
+
dateAs: "TEXT" | "INTEGER" | "REAL" = "TEXT"
|
|
167
|
+
) {
|
|
168
|
+
const workbook = XLSX.read(buffer, {
|
|
169
|
+
type: "buffer",
|
|
170
|
+
cellDates: true,
|
|
171
|
+
raw: false,
|
|
172
|
+
});
|
|
173
|
+
return this.importSheetFromWorkbook(workbook, dbPath, tableName, sheetName, drop, headerRow, dateAs);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private static importSheetFromWorkbook(
|
|
177
|
+
workbook: XLSX.WorkBook,
|
|
178
|
+
dbPath: string,
|
|
179
|
+
tableName: string,
|
|
180
|
+
sheetName?: string,
|
|
181
|
+
drop = false,
|
|
182
|
+
headerRow = 1,
|
|
183
|
+
dateAs: "TEXT" | "INTEGER" | "REAL" = "TEXT"
|
|
184
|
+
) {
|
|
185
|
+
const sheet = sheetName || workbook.SheetNames[0];
|
|
186
|
+
const worksheet = workbook.Sheets[sheet];
|
|
187
|
+
if (!worksheet) throw new Error(`Sheet not found: ${sheet}`);
|
|
188
|
+
|
|
189
|
+
const rows: any[] = XLSX.utils.sheet_to_json(worksheet, {
|
|
190
|
+
header: 1,
|
|
191
|
+
defval: "",
|
|
192
|
+
blankrows: false,
|
|
193
|
+
raw: false,
|
|
194
|
+
});
|
|
195
|
+
if (rows.length < headerRow) throw new Error(`Sheet has no header at row ${headerRow}`);
|
|
196
|
+
|
|
197
|
+
const header = (rows[headerRow - 1] as unknown[]).map(v => JSON.stringify(v ?? "").trim());
|
|
198
|
+
const dataRows = rows
|
|
199
|
+
.slice(headerRow)
|
|
200
|
+
.filter(r => Array.isArray(r) && r.some(c => c !== "" && c !== null && c !== undefined));
|
|
201
|
+
|
|
202
|
+
const sanitizedNames = uniqueNames(header);
|
|
203
|
+
const columns: ColumnDef[] = sanitizedNames.map((name, idx) => {
|
|
204
|
+
const colValues = dataRows.map(r => r[idx]);
|
|
205
|
+
return {
|
|
206
|
+
name,
|
|
207
|
+
original: header[idx] || name,
|
|
208
|
+
kind: inferKind(colValues),
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const tblName = snake(tableName || sheet);
|
|
213
|
+
const columnDecls = columns.map(c => `"${c.name}" ${c.kind}`);
|
|
214
|
+
const ddl = `CREATE TABLE IF NOT EXISTS "${tblName}" ( ${columnDecls.join(", ")} );`;
|
|
215
|
+
|
|
216
|
+
const db = new SqliteModule(dbPath);
|
|
217
|
+
|
|
218
|
+
const tx = db.db.transaction(() => {
|
|
219
|
+
if (drop) db.db.prepare(`DROP TABLE IF EXISTS "${tblName}";`).run();
|
|
220
|
+
db.db.prepare(ddl).run();
|
|
221
|
+
if (dataRows.length === 0) return;
|
|
222
|
+
|
|
223
|
+
const placeholders = columns.map(() => "?").join(",");
|
|
224
|
+
const insertSql = `INSERT INTO "${tblName}" (${columns.map(c => `"${c.name}"`).join(", ")}) VALUES (${placeholders});`;
|
|
225
|
+
const stmt = db.db.prepare(insertSql);
|
|
226
|
+
for (const row of dataRows) {
|
|
227
|
+
const values = columns.map((c, idx) => coerceValue(c.kind, row[idx], dateAs));
|
|
228
|
+
stmt.run(values);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
tx();
|
|
233
|
+
return { tableName: tblName, columns };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
process.env.DOTENV_CONFIG_QUIET = "true";
|
|
2
2
|
|
|
3
3
|
import { config } from "dotenv";
|
|
4
|
+
import { expand } from "dotenv-expand";
|
|
4
5
|
import path from "path";
|
|
5
6
|
import { fileURLToPath } from "url";
|
|
6
7
|
|
|
@@ -33,20 +34,20 @@ export function loadDotenv(opts?: { root?: string }) {
|
|
|
33
34
|
const root = opts?.root ?? projectRoot();
|
|
34
35
|
|
|
35
36
|
if (process.env.NODE_ENV === "test") {
|
|
36
|
-
config({ path: path.join(root, ".env.test") });
|
|
37
|
+
expand(config({ path: path.join(root, ".env.test") }));
|
|
37
38
|
// optional: if you want local test overrides
|
|
38
|
-
config({ path: path.join(root, ".env.test.local"), override: true });
|
|
39
|
+
expand(config({ path: path.join(root, ".env.test.local"), override: true }));
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// production/dev flow
|
|
43
|
-
config({ path: path.join(process.cwd(), ".env") }); // prod
|
|
44
|
+
expand(config({ path: path.join(process.cwd(), ".env") })); // prod
|
|
44
45
|
// dev env
|
|
45
|
-
config({ path: path.join(root, ".env") });
|
|
46
|
+
expand(config({ path: path.join(root, ".env") }));
|
|
46
47
|
// .env.local should override the base (for dev machine secrets)
|
|
47
|
-
config({ path: path.join(root, ".env.local"), override: true });
|
|
48
|
+
expand(config({ path: path.join(root, ".env.local"), override: true }));
|
|
48
49
|
// also load .env.${NODE_ENV}.local if you want per-env local overrides:
|
|
49
50
|
if (process.env.NODE_ENV) {
|
|
50
|
-
config({ path: path.join(root, `.env.${process.env.NODE_ENV}.local`), override: true });
|
|
51
|
+
expand(config({ path: path.join(root, `.env.${process.env.NODE_ENV}.local`), override: true }));
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type IEventWithPayload<T extends Record<string, unknown>> = {
|
|
2
|
+
[K in keyof T]: { event: K; payload: T[K] };
|
|
3
|
+
}[keyof T];
|
|
4
|
+
|
|
5
|
+
export class IEvent<T extends Record<string, unknown>> {
|
|
6
|
+
private listeners: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};
|
|
7
|
+
private onceListeners: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};
|
|
8
|
+
private everyListeners: Array<(e: IEventWithPayload<T>) => void> = [];
|
|
9
|
+
|
|
10
|
+
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void) {
|
|
11
|
+
(this.listeners[event] ||= []).push(handler);
|
|
12
|
+
return () => this.off(event, handler);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
once<K extends keyof T>(event: K, handler: (payload: T[K]) => void) {
|
|
16
|
+
(this.onceListeners[event] ||= []).push(handler);
|
|
17
|
+
return () => this.off(event, handler);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
off<K extends keyof T>(event: K, handler: (payload: T[K]) => void) {
|
|
21
|
+
if (this.listeners[event]) {
|
|
22
|
+
this.listeners[event] = this.listeners[event].filter(fn => fn !== handler);
|
|
23
|
+
}
|
|
24
|
+
if (this.onceListeners[event]) {
|
|
25
|
+
this.onceListeners[event] = this.onceListeners[event].filter(fn => fn !== handler);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onEvery(handler: (e: IEventWithPayload<T>) => void) {
|
|
30
|
+
this.everyListeners.push(handler);
|
|
31
|
+
return () => {
|
|
32
|
+
this.everyListeners = this.everyListeners.filter(h => h !== handler);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
emit<K extends keyof T>(event: K, payload: T[K]) {
|
|
37
|
+
const everyList = [...this.everyListeners];
|
|
38
|
+
for (const h of everyList) {
|
|
39
|
+
h({ event, payload } as IEventWithPayload<T>);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const list = this.listeners[event];
|
|
43
|
+
if (list) {
|
|
44
|
+
for (const h of [...list]) h(payload);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const onceList = this.onceListeners[event];
|
|
48
|
+
if (onceList && onceList.length > 0) {
|
|
49
|
+
this.onceListeners[event] = [];
|
|
50
|
+
for (const h of [...onceList]) h(payload);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
emitOnce<K extends keyof T>(event: K, payload: T[K]) {
|
|
55
|
+
this.emit(event, payload);
|
|
56
|
+
this.listeners[event] = [];
|
|
57
|
+
this.onceListeners[event] = [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./i-event.js";
|