botql 1.0.0

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/Connectors.js ADDED
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ /**
6
+ * ConnectorRegistry — resolve o nome de uma plataforma (ex: "TELEGRAM")
7
+ * para o módulo que sabe falar a API real dessa plataforma.
8
+ *
9
+ * O registry NÃO tenta cobrir todas as plataformas existentes — só regista
10
+ * as que o próprio projeto usa, tal como o xlang-modules.json faz para
11
+ * bibliotecas. Uma plataforma sem entrada dá erro claro, não falha
12
+ * silenciosa.
13
+ *
14
+ * O mapeamento plataforma -> ficheiro vive em Connectors.json (JSON puro,
15
+ * sem lógica), esta classe é que sabe carregar e instanciar esses ficheiros.
16
+ */
17
+ class ConnectorRegistry {
18
+ constructor(registryPathOrObject = path.join(__dirname, 'Connectors.json')) {
19
+ if (typeof registryPathOrObject === 'string') {
20
+ this.baseDir = path.dirname(path.resolve(registryPathOrObject));
21
+ this.registry = { ...require(path.resolve(registryPathOrObject)) };
22
+ } else {
23
+ this.baseDir = __dirname;
24
+ this.registry = { ...registryPathOrObject };
25
+ }
26
+ this.cache = new Map();
27
+ }
28
+
29
+ // Regista/substitui uma plataforma em runtime. Aceita:
30
+ // - uma função factory: (credential) => connector
31
+ // - o caminho (string) para um módulo que exporta essa factory
32
+ register(platform, moduleOrFactory) {
33
+ this.registry[platform] = typeof moduleOrFactory === 'function'
34
+ ? { factory: moduleOrFactory }
35
+ : moduleOrFactory;
36
+ }
37
+
38
+ resolve(platform, credential) {
39
+ const entry = this.registry[platform];
40
+ if (!entry) {
41
+ throw new Error(
42
+ `BotQL: nenhum connector registado para a plataforma "${platform}". ` +
43
+ `Regista-a em Connectors.json ou via registry.register("${platform}", adapter).`
44
+ );
45
+ }
46
+
47
+ const cacheKey = `${platform}:${credential}`;
48
+ if (this.cache.has(cacheKey)) return this.cache.get(cacheKey);
49
+
50
+ // entry pode ser: string (caminho de módulo), ou { factory } vindo de register().
51
+ const factory = typeof entry === 'string'
52
+ ? require(path.resolve(this.baseDir, entry))
53
+ : entry.factory || require(path.resolve(this.baseDir, entry.module));
54
+
55
+ const connector = factory(credential);
56
+ this.cache.set(cacheKey, connector);
57
+ return connector;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Liga um BotQLInterpreter já carregado a esta registry: para cada
63
+ * CONNECT registado no bot, resolve o connector real e passa a usá-lo
64
+ * como destino de REPLY / FORWARD TO / SEND TO para essa plataforma.
65
+ *
66
+ * Chamadas manuais a onReply/onForward/onSend passadas nas opções do
67
+ * interpreter continuam a ter prioridade — isto só preenche o que não
68
+ * foi definido à mão (onReply/onForward/onSend ficam `null` no
69
+ * interpreter enquanto não forem definidos, ver botql.js).
70
+ */
71
+ function attachConnectors(interpreter, registry) {
72
+ const byService = new Map();
73
+ for (const conn of interpreter.connections) {
74
+ byService.set(conn.service, registry.resolve(conn.service, conn.credential));
75
+ }
76
+
77
+ const platformConnector = interpreter.platform ? byService.get(interpreter.platform) : null;
78
+
79
+ if (!interpreter.onReply) {
80
+ interpreter.onReply = async ({ target, text, client }) => {
81
+ const connector = platformConnector || byService.get('ADMIN');
82
+ if (!connector) return;
83
+ await connector.send(target || client, text);
84
+ };
85
+ }
86
+
87
+ if (!interpreter.onForward) {
88
+ interpreter.onForward = async ({ target, client }) => {
89
+ if (!platformConnector) return;
90
+ await platformConnector.send(target, `Encaminhado de ${client}`);
91
+ };
92
+ }
93
+
94
+ if (!interpreter.onSend) {
95
+ interpreter.onSend = async ({ target, signal }) => {
96
+ const connector = byService.get(target);
97
+ if (!connector) return;
98
+ await connector.send(target, JSON.stringify(signal));
99
+ };
100
+ }
101
+
102
+ return interpreter;
103
+ }
104
+
105
+ module.exports = { ConnectorRegistry, attachConnectors };
package/Database.js ADDED
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Database.js — Camada de banco de dados do BotQL
5
+ *
6
+ * Qualquer adapter aqui implementa a mesma interface, usada pelo botql.js:
7
+ * createTable(name, columns, preventDefault)
8
+ * insert(table, columnNames, values) -> id
9
+ * update(table, column, value, whereColumn, whereValue)
10
+ * getRows(table) -> array de linhas
11
+ *
12
+ * MemoryDatabase: guarda tudo em memória, sem persistência — bom para
13
+ * testes e para o exemplo do botql.js.
14
+ *
15
+ * SQLiteDatabase: persiste num ficheiro .sqlite real, usando o módulo
16
+ * nativo `node:sqlite` (Node 22+), sem dependências externas.
17
+ */
18
+
19
+ const CONTEXT_SCHEMA = [
20
+ { name: 'id', columnType: 'INT', constraints: ['PRIMARY', 'KEY', 'AUTO_INCREMENT'] },
21
+ { name: 'client', columnType: 'TEXT', constraints: [] },
22
+ { name: 'message', columnType: 'TEXT', constraints: [] },
23
+ { name: 'reply', columnType: 'TEXT', constraints: [] },
24
+ { name: 'created_at', columnType: 'DATETIME', constraints: [] }
25
+ ];
26
+
27
+ // ===== MemoryDatabase =====
28
+
29
+ class MemoryDatabase {
30
+ constructor() {
31
+ this.tables = new Map();
32
+ this.autoIncrement = new Map();
33
+ }
34
+
35
+ createTable(name, columns, preventDefault) {
36
+ const resolvedColumns = (name === 'Context' && columns.length === 0)
37
+ ? CONTEXT_SCHEMA
38
+ : columns;
39
+
40
+ if (this.tables.has(name)) {
41
+ if (preventDefault) return;
42
+ throw new Error(`runtime error: table "${name}" already exists`);
43
+ }
44
+
45
+ this.tables.set(name, { columns: resolvedColumns, rows: [] });
46
+ this.autoIncrement.set(name, 0);
47
+ }
48
+
49
+ insert(table, columnNames, values) {
50
+ const t = this.tables.get(table);
51
+ if (!t) throw new Error(`runtime error: table "${table}" does not exist`);
52
+
53
+ const nextId = this.autoIncrement.get(table) + 1;
54
+ this.autoIncrement.set(table, nextId);
55
+
56
+ const row = { id: nextId };
57
+ columnNames.forEach((col, i) => { row[col] = values[i]; });
58
+ t.rows.push(row);
59
+
60
+ return nextId;
61
+ }
62
+
63
+ update(table, column, value, whereColumn, whereValue) {
64
+ const t = this.tables.get(table);
65
+ if (!t) throw new Error(`runtime error: table "${table}" does not exist`);
66
+
67
+ const row = whereColumn
68
+ ? t.rows.find((r) => r[whereColumn] === whereValue)
69
+ : t.rows[t.rows.length - 1];
70
+
71
+ if (row) row[column] = value;
72
+ return row || null;
73
+ }
74
+
75
+ getRows(table) {
76
+ const t = this.tables.get(table);
77
+ return t ? t.rows.slice() : [];
78
+ }
79
+ }
80
+
81
+ // ===== SQLiteDatabase =====
82
+
83
+ function mapColumnType(columnType) {
84
+ const type = (columnType || '').toUpperCase();
85
+ if (type === 'INT' || type === 'INTEGER') return 'INTEGER';
86
+ if (type === 'DATETIME' || type === 'DATE') return 'TEXT';
87
+ return 'TEXT';
88
+ }
89
+
90
+ function buildColumnDef(col) {
91
+ const type = mapColumnType(col.columnType);
92
+ const isPrimary = col.constraints.includes('PRIMARY') && col.constraints.includes('KEY');
93
+ const isAutoIncrement = col.constraints.includes('AUTO_INCREMENT');
94
+
95
+ let def = `${col.name} ${type}`;
96
+ if (isPrimary) def += ' PRIMARY KEY';
97
+ if (isPrimary && isAutoIncrement) def += ' AUTOINCREMENT';
98
+ return def;
99
+ }
100
+
101
+ class SQLiteDatabase {
102
+ /**
103
+ * @param {string} filePath Caminho do ficheiro .sqlite, ou ":memory:" para um banco temporário.
104
+ */
105
+ constructor(filePath = ':memory:') {
106
+ const { DatabaseSync } = require('node:sqlite');
107
+ this.driver = new DatabaseSync(filePath);
108
+ }
109
+
110
+ createTable(name, columns, preventDefault) {
111
+ const resolvedColumns = (name === 'Context' && columns.length === 0)
112
+ ? CONTEXT_SCHEMA
113
+ : columns;
114
+
115
+ const columnDefs = resolvedColumns.map(buildColumnDef).join(', ');
116
+ const ifNotExists = preventDefault ? 'IF NOT EXISTS ' : '';
117
+ this.driver.exec(`CREATE TABLE ${ifNotExists}${name} (${columnDefs})`);
118
+ }
119
+
120
+ insert(table, columnNames, values) {
121
+ const placeholders = columnNames.map(() => '?').join(', ');
122
+ const sql = `INSERT INTO ${table} (${columnNames.join(', ')}) VALUES (${placeholders})`;
123
+ const info = this.driver.prepare(sql).run(...values);
124
+ return Number(info.lastInsertRowid);
125
+ }
126
+
127
+ update(table, column, value, whereColumn, whereValue) {
128
+ if (whereColumn) {
129
+ const sql = `UPDATE ${table} SET ${column} = ? WHERE ${whereColumn} = ?`;
130
+ this.driver.prepare(sql).run(value, whereValue);
131
+ } else {
132
+ const sql = `UPDATE ${table} SET ${column} = ? WHERE id = (SELECT MAX(id) FROM ${table})`;
133
+ this.driver.prepare(sql).run(value);
134
+ }
135
+ }
136
+
137
+ getRows(table) {
138
+ return this.driver.prepare(`SELECT * FROM ${table}`).all();
139
+ }
140
+
141
+ close() {
142
+ this.driver.close();
143
+ }
144
+ }
145
+
146
+ module.exports = { MemoryDatabase, SQLiteDatabase };
package/FileSystem.js ADDED
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * FileSystem.js — Acesso a ficheiros isomorfico (Node ou browser)
5
+ *
6
+ * botql.js precisa de ler ficheiros para resolver IMPORT. Em Node isso e
7
+ * fs.readFileSync + path real. No browser (ou em qualquer ambiente sem
8
+ * fs), nao ha disco: os "ficheiros" sao apenas texto que o utilizador
9
+ * forneceu antecipadamente (ex: nos campos de import do editor).
10
+ *
11
+ * Qualquer adapter aqui implementa o mesmo contrato, usado por botql.js:
12
+ * exists(path) -> boolean
13
+ * readFile(path) -> string
14
+ * resolve(basePath, relativePath) -> string (caminho absoluto/canonico)
15
+ * dirname(path) -> string
16
+ *
17
+ * NodeFileSystem: usa fs/path reais do Node. E o omissao quando o codigo
18
+ * corre em Node (Bootstrap.js, CLI, testes).
19
+ *
20
+ * MemoryFileSystem: guarda ficheiros num Map (caminho -> conteudo) em
21
+ * memoria. Usado no browser: o Index.html regista ai o conteudo de cada
22
+ * ficheiro importado antes de correr o bot. Os caminhos sao tratados como
23
+ * chaves de texto simples, normalizadas (sem "./", sem duplicar "/").
24
+ */
25
+
26
+ class MemoryFileSystem {
27
+ constructor(files = {}) {
28
+ // files: { "caminho/ficheiro.sql": "conteudo..." }
29
+ this.files = new Map(
30
+ Object.entries(files).map(([k, v]) => [this._normalize(k), v])
31
+ );
32
+ }
33
+
34
+ _normalize(p) {
35
+ // Remove "./" do inicio e barras duplicadas, mantem caminho relativo simples.
36
+ return String(p).replace(/^\.\//, '').replace(/\/+/g, '/').trim();
37
+ }
38
+
39
+ setFile(path, content) {
40
+ this.files.set(this._normalize(path), content);
41
+ }
42
+
43
+ exists(path) {
44
+ return this.files.has(this._normalize(path));
45
+ }
46
+
47
+ readFile(path) {
48
+ const key = this._normalize(path);
49
+ if (!this.files.has(key)) {
50
+ throw new Error(`BotQL: ficheiro não encontrado: "${path}"`);
51
+ }
52
+ return this.files.get(key);
53
+ }
54
+
55
+ // basePath e' ignorado de proposito: no browser nao ha nocao real de
56
+ // diretorio corrente, os ficheiros importados sao identificados pelo
57
+ // nome/caminho tal como o utilizador os registou.
58
+ resolve(basePath, relativePath) {
59
+ return this._normalize(relativePath);
60
+ }
61
+
62
+ dirname(filePath) {
63
+ const norm = this._normalize(filePath);
64
+ const idx = norm.lastIndexOf('/');
65
+ return idx === -1 ? '' : norm.slice(0, idx);
66
+ }
67
+ }
68
+
69
+ class NodeFileSystem {
70
+ constructor() {
71
+ this._fs = require('fs');
72
+ this._path = require('path');
73
+ }
74
+
75
+ exists(path) {
76
+ return this._fs.existsSync(path);
77
+ }
78
+
79
+ readFile(path) {
80
+ return this._fs.readFileSync(path, 'utf8');
81
+ }
82
+
83
+ resolve(basePath, relativePath) {
84
+ return this._path.resolve(basePath, relativePath);
85
+ }
86
+
87
+ dirname(filePath) {
88
+ return this._path.dirname(filePath);
89
+ }
90
+ }
91
+
92
+ // Deteta o ambiente uma unica vez. `require` e `module` so existem em Node/CommonJS.
93
+ const isNode = typeof process !== 'undefined'
94
+ && process.versions
95
+ && !!process.versions.node;
96
+
97
+ function createDefaultFileSystem() {
98
+ return isNode ? new NodeFileSystem() : new MemoryFileSystem();
99
+ }
100
+
101
+ const api = { MemoryFileSystem, NodeFileSystem, createDefaultFileSystem };
102
+
103
+ if (typeof module !== 'undefined' && module.exports) {
104
+ module.exports = api;
105
+ } else {
106
+ // Browser sem bundler/CommonJS: expõe global.
107
+ this.BotQLFileSystem = api;
108
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adilson C. Rafael
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.