diff --git a/package.json b/package.json index 118704c..7d08291 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@outerbase/sdk", - "version": "1.0.15", + "version": "1.0.17", "description": "", "main": "dist/index.js", "module": "dist/index.js", diff --git a/playground/index.js b/playground/index.js index 964caef..e69de29 100644 --- a/playground/index.js +++ b/playground/index.js @@ -1,67 +0,0 @@ -import { CloudflareD1Connection, Outerbase } from '../dist/index.js'; -import express from 'express'; - -const app = express(); -const port = 4000; - -app.get('/test', async (req, res) => { - // Establish connection to your provider database - const d1 = new CloudflareD1Connection({ - apiKey: '', - accountId: '', - databaseId: '' - }); - - const db = Outerbase(d1); - // const dbSchema = await d1.fetchDatabaseSchema() - - // const { data, query } = await db - // .selectFrom([ - // { table: 'test2', columns: ['*'] } - // ]) - // .query() - - // let { data, query } = await db - // .insert({ fname: 'John' }) - // .into('test2') - // .returning(['id']) - // .query(); - - // let { data, query } = await db - // .update({ fname: 'Johnny' }) - // .into('test2') - // .where(equals('id', '3', d1.dialect)) - // .query(); - - // let { data, query } = await db - // .deleteFrom('test2') - // .where(equals('id', '3')) - // .query(); - - // let data = {} - // let query = await db - // .createTable('test3') - // .schema('public') - // .columns([ - // { name: 'id', type: ColumnDataType.NUMBER, primaryKey: true }, - // { name: 'fname', type: ColumnDataType.STRING } - // ]) - // .toString(); - - // let data = {} - // let query = await db - // .renameTable('test3', 'test4') - // .toString(); - - // let data = {} - // let query = await db - // .dropTable('test4') - // .toString(); - - // let { data } = await db.queryRaw('SELECT * FROM playing_with_neon WHERE id = $1', ['1']); - res.json(data); -}); - -app.listen(port, () => { - console.log(`Server is running on http://localhost:${port}`); -}); diff --git a/src/ai/prompts/ob1/3.ts b/src/ai/prompts/ob1/3.ts index 4c810c7..aa6912e 100644 --- a/src/ai/prompts/ob1/3.ts +++ b/src/ai/prompts/ob1/3.ts @@ -1,3 +1,42 @@ export const PROMPT = ` + Your job is to ONLY return a block of Javascript code for a Cloudflare worker. + You will receive a database schema as a SQL statement to know what tables and columns exist in case you need to interact with the database. + + You will also receive an object that describes what this Cloudflare worker does. + Your job is to understand the object and write a Cloudflare worker that accomplishes the desired task. + + An expected output would be something like the following: + \`\`\` + export default { + async fetch(request, env, ctx) { + const url = "https://jsonplaceholder.typicode.com/todos/1"; + + // gatherResponse returns both content-type & response body as a string + async function gatherResponse(response) { + const { headers } = response; + const contentType = headers.get("content-type") || ""; + if (contentType.includes("application/json")) { + return { contentType, result: JSON.stringify(await response.json()) }; + } + return { contentType, result: response.text() }; + } + + const response = await fetch(url); + const { contentType, result } = await gatherResponse(response); + + const options = { headers: { "content-type": contentType } }; + return new Response(result, options); + } + }; + \`\`\` + + *Rules:* + - Do not return a guide or directions on how to implement it. + - Only return a block of Javascript code! + - The code block should be able to be deployed to Cloudflare Workers. + - Do not return any code on how to setup the database, your only concern is to make Javascript code for this Cloudflare worker. + - If you need to interact with the database you can do so by doing "sdk.query()" and passing in the SQL query you need to run. + + Important: Only respond with a single block of Javascript code, nothing else. ` \ No newline at end of file diff --git a/src/ai/providers/cloudflare.ts b/src/ai/providers/cloudflare.ts index 3824771..1f968ca 100644 --- a/src/ai/providers/cloudflare.ts +++ b/src/ai/providers/cloudflare.ts @@ -7,8 +7,9 @@ export class CloudflareAi implements AiProvider { constructor(_?: { model: string, apiKey: string }) { if (!_) return; - this.model = _.model; - this.apiKey = _.apiKey; + + if (_.model) this.model = _.model; + if (_.apiKey) this.apiKey = _.apiKey; } async startConversation(systemPrompt: string, message: string): Promise { diff --git a/src/client.ts b/src/client.ts index 47abf43..d877f5e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -39,6 +39,8 @@ export interface QueryBuilder { asClass?: any groupBy?: string + selectRawValue?: string + // General operation values, such as when renaming tables referencing the old and new name originalValue?: string newValue?: string @@ -49,6 +51,8 @@ export interface OuterbaseType { selectFrom: ( columnsArray: { schema?: string; table: string; columns: string[] }[] ) => OuterbaseType + selectRaw: (statement: string) => OuterbaseType + insert: (data: { [key: string]: any }) => OuterbaseType update: (data: { [key: string]: any }) => OuterbaseType deleteFrom: (table: string) => OuterbaseType @@ -112,6 +116,12 @@ export function Outerbase(connection: Connection): OuterbaseType { return this }, + selectRaw(statement) { + this.queryBuilder.action = QueryBuilderAction.SELECT + this.queryBuilder.selectRawValue = statement + + return this + }, where(condition) { // Check if `condition` is an array of conditions if (Array.isArray(condition)) { diff --git a/src/connections/bigquery.ts b/src/connections/bigquery.ts index e3ba3fb..a40ce37 100644 --- a/src/connections/bigquery.ts +++ b/src/connections/bigquery.ts @@ -2,7 +2,7 @@ import { QueryType } from '../query-params' import { Query, constructRawQuery } from '../query' import { Connection } from './index' import { Database, Table, TableColumn } from '../models/database' -import { DefaultDialect } from '../query-builder/dialects/default' +import { BigQueryDialect } from '../query-builder/dialects/bigquery' import { BigQuery } from '@google-cloud/bigquery' @@ -19,7 +19,7 @@ export class BigQueryConnection implements Connection { queryType = QueryType.positional // Default dialect for BigQuery - dialect = new DefaultDialect() + dialect = new BigQueryDialect() /** * Creates a new BigQuery object. diff --git a/src/connections/motherduck.ts b/src/connections/motherduck.ts index 3f45c99..b340e8d 100644 --- a/src/connections/motherduck.ts +++ b/src/connections/motherduck.ts @@ -8,7 +8,7 @@ import { TableIndex, TableIndexType, } from '../models/database' -import { DefaultDialect } from '../query-builder/dialects/default' +import { DuckDbDialect } from '../query-builder/dialects/duckdb' import duckDB from 'duckdb' type DuckDBParameters = { @@ -24,11 +24,11 @@ export class DuckDBConnection implements Connection { queryType = QueryType.positional // Default dialect for MotherDuck - dialect = new DefaultDialect() + dialect = new DuckDbDialect() constructor(private _: DuckDBParameters) { this.duckDB = new duckDB.Database(_.path, { - motherduck_token: _.token + motherduck_token: _.token, }) this.connection = this.duckDB.connect() } @@ -82,10 +82,7 @@ export class DuckDBConnection implements Connection { ) result = res } else { - const { res } = await this.runQuery( - connection, - query.query - ) + const { res } = await this.runQuery(connection, query.query) result = res } } catch (e) { @@ -103,7 +100,7 @@ export class DuckDBConnection implements Connection { public async fetchDatabaseSchema(): Promise { let database: Database = [] - + type DuckDBTables = { database: string schema: string @@ -112,16 +109,32 @@ export class DuckDBConnection implements Connection { column_types: string[] temporary: boolean } - + type DuckDBSettingsRow = { + name: string + value: string + description: string + input_type: string + scope: string + } + const { data: currentDatabaseResponse, error } = await this.query({ + query: `SELECT * FROM duckdb_settings();`, + }) + + const currentDatabase = (currentDatabaseResponse as DuckDBSettingsRow[]) + .find((row) => row.name === 'temp_directory') + ?.value.split(':')[1] + .split('.')[0] const result = await this.query({ query: `PRAGMA show_tables_expanded;`, }) - + const tables = result.data as DuckDBTables[] - + const currentTables = tables.filter( + (table) => table.database === currentDatabase + ) const schemaMap: { [key: string]: Table[] } = {} - - for (const table of tables) { + + for (const table of currentTables) { type DuckDBTableInfo = { cid: number name: string @@ -133,9 +146,9 @@ export class DuckDBConnection implements Connection { const tableInfoResult = await this.query({ query: `PRAGMA table_info('${table.database}.${table.schema}.${table.name}')`, }) - + const tableInfo = tableInfoResult.data as DuckDBTableInfo[] - + const constraints: TableIndex[] = [] const columns = tableInfo.map((column) => { if (column.pk) { @@ -145,7 +158,7 @@ export class DuckDBConnection implements Connection { columns: [column.name], }) } - + const currentColumn: TableColumn = { name: column.name, type: column.type, @@ -156,50 +169,50 @@ export class DuckDBConnection implements Connection { unique: column.pk, references: [], // DuckDB currently doesn't have a pragma for foreign keys } - + return currentColumn }) - + const currentTable: Table = { name: table.name, - schema: table.schema, // Assign schema name to the table + schema: table.schema, // Assign schema name to the table columns: columns, indexes: constraints, } - + if (!schemaMap[table.schema]) { schemaMap[table.schema] = [] } - + schemaMap[table.schema].push(currentTable) } - + database = Object.entries(schemaMap).map(([schemaName, tables]) => { return { - [schemaName]: tables + [schemaName]: tables, } }) - + return database - } + } runQuery = async ( connection: duckDB.Connection, query: string, ...params: any[] - ): Promise<{ stmt: duckDB.Statement; res: any[]; }> => { + ): Promise<{ stmt: duckDB.Statement; res: any[] }> => { return new Promise((resolve, reject) => { connection.prepare(query, (err, stmt) => { if (err) { return reject(err) } - + stmt.all(...params, (err, res) => { if (err) { stmt.finalize() return reject(err) } - + resolve({ stmt, res }) stmt.finalize() }) diff --git a/src/query-builder/dialects/bigquery.ts b/src/query-builder/dialects/bigquery.ts new file mode 100644 index 0000000..caad71c --- /dev/null +++ b/src/query-builder/dialects/bigquery.ts @@ -0,0 +1,17 @@ +import { AbstractDialect } from '../index'; + +export class BigQueryDialect extends AbstractDialect { + formatSchemaAndTable(schema: string | undefined, table: string): string { + if (schema) { + return `\`${schema}.${table}\``; + } + return `\`${table}\``; + } + + formatFromSchemaAndTable(schema: string | undefined, table: string): string { + if (schema) { + return `\`${schema}.${table}\``; + } + return `\`${table}\``; + } +} \ No newline at end of file diff --git a/src/query-builder/dialects/duckdb.ts b/src/query-builder/dialects/duckdb.ts new file mode 100644 index 0000000..faa34f1 --- /dev/null +++ b/src/query-builder/dialects/duckdb.ts @@ -0,0 +1,7 @@ +import { AbstractDialect } from '../index'; + +export class DuckDbDialect extends AbstractDialect { + formatSchemaAndTable(schema: string | undefined, table: string): string { + return table; + } +} \ No newline at end of file diff --git a/src/query-builder/dialects/mysql.ts b/src/query-builder/dialects/mysql.ts index 591d916..ea1c01c 100644 --- a/src/query-builder/dialects/mysql.ts +++ b/src/query-builder/dialects/mysql.ts @@ -1,20 +1,14 @@ import { AbstractDialect, ColumnDataType } from '../index'; -import { QueryBuilder } from '../../client'; -import { QueryType } from '../../query-params'; export class MySQLDialect extends AbstractDialect { mapDataType(dataType: ColumnDataType): string { switch (dataType.toLowerCase()) { case ColumnDataType.STRING: - return 'VARCHAR(255)'; // MySQL specific VARCHAR length + return 'VARCHAR(255)'; case ColumnDataType.BOOLEAN: - return 'TINYINT(1)'; // MySQL uses TINYINT for boolean + return 'TINYINT(1)'; default: return super.mapDataType(dataType); } } - - // select(builder: QueryBuilder, type: QueryType): string { - // return `SELECT MYSQL` - // } } \ No newline at end of file diff --git a/src/query-builder/index.ts b/src/query-builder/index.ts index 1146a3a..04404a2 100644 --- a/src/query-builder/index.ts +++ b/src/query-builder/index.ts @@ -3,6 +3,8 @@ import { QueryBuilder } from "../client"; import { Query } from "../query"; interface Dialect { + formatSchemaAndTable(schema: string | undefined, table: string): string + select(builder: QueryBuilder, type: QueryType, query: Query): Query; insert(builder: QueryBuilder, type: QueryType, query: Query): Query; update(builder: QueryBuilder, type: QueryType, query: Query): Query; @@ -13,6 +15,21 @@ interface Dialect { dropTable(builder: QueryBuilder, type: QueryType, query: Query): Query; renameTable(builder: QueryBuilder, type: QueryType, query: Query): Query; + // Column operations + // addColumn?: (name: string, table: string, schema?: string) => Promise + // dropColumn?: (name: string, table: string, schema?: string) => Promise + // renameColumn?: (name: string, original: string, table: string, schema?: string) => Promise + // updateColumn?: (name: string, column: TableColumn, table: string, schema?: string) => Promise + + // // // Index operations + // createIndex?: (index: TableIndex, table: string, schema?: string) => Promise + // dropIndex?: (name: string, table: string, schema?: string) => Promise + // renameIndex?: (name: string, original: string, table: string, schema?: string) => Promise + + // // // Schema operations + // createSchema?: (name: string) => Promise + // dropSchema?: (name: string) => Promise + equals(a: any, b: string): string; equalsNumber(a: any, b: any): string; equalsColumn(a: any, b: any): string; @@ -47,21 +64,6 @@ interface Dialect { notBetweenNumbers(a: any, b: any, c: any): string; ascending(a: any): string; descending(a: any): string; - - // Column operations - // addColumn?: (name: string, table: string, schema?: string) => Promise - // dropColumn?: (name: string, table: string, schema?: string) => Promise - // renameColumn?: (name: string, original: string, table: string, schema?: string) => Promise - // updateColumn?: (name: string, column: TableColumn, table: string, schema?: string) => Promise - - // // Index operations - // createIndex?: (index: TableIndex, table: string, schema?: string) => Promise - // dropIndex?: (name: string, table: string, schema?: string) => Promise - // renameIndex?: (name: string, original: string, table: string, schema?: string) => Promise - - // // Schema operations - // createSchema?: (name: string) => Promise - // dropSchema?: (name: string) => Promise } export enum ColumnDataType { @@ -77,6 +79,16 @@ export enum ColumnDataType { } export abstract class AbstractDialect implements Dialect { + /** + * WORK IN PROGRESS! + * This code is not used anywhere in the SDK at the moment. It is a work in progress to add support for SQL functions + * in the query builder. The idea is to allow users to use SQL functions in their queries, and the query builder will + * automatically format the query to use the correct SQL function for the specific database dialect. + * + * The `sqlFunctions` object is a map of SQL function names to their implementations. The `getFunction` method is used + * to get the implementation of a specific SQL function. The `addFunction` method is used to add a new SQL function + * to the `sqlFunctions` object. + */ protected sqlFunctions: { [key: string]: (...args: string[]) => string } = { now: () => 'NOW()', concat: (...args: string[]) => `CONCAT(${args.join(', ')})`, @@ -84,6 +96,12 @@ export abstract class AbstractDialect implements Dialect { abs: (value: string) => `ABS(${value})`, }; + /** + * Retrieves the implementation of the SQL function with the given name. + * + * @param funcName + * @returns Returns the implementation of the SQL function with the given name. + */ getFunction(funcName: string): (...args: string[]) => string { if (this.sqlFunctions[funcName]) { return this.sqlFunctions[funcName]; @@ -91,12 +109,23 @@ export abstract class AbstractDialect implements Dialect { throw new Error(`SQL function '${funcName}' not supported in this dialect.`); } - // Allow specific dialects to add or override functions + /** + * Adds a new SQL function to the `sqlFunctions` object. If a function with the same name already exists, it will be + * overwritten with the new implementation. + * + * @param funcName + * @param implementation + */ protected addFunction(funcName: string, implementation: (...args: string[]) => string) { this.sqlFunctions[funcName] = implementation; } - // Maps generic data types to database-specific data types + /** + * Maps the data type from the SDK to the equivalent data type for the specific database dialect. + * + * @param dataType + * @returns Returns the equivalent data type for the specific database dialect. + */ mapDataType(dataType: ColumnDataType | string): string { switch (dataType) { case ColumnDataType.STRING: @@ -112,6 +141,11 @@ export abstract class AbstractDialect implements Dialect { } } + /** + * Words that are reserved in the query language and may require special character wrapping to prevent + * collisions with the database engine executing the query. Each dialect may need to override this property + * with the reserved keywords for the specific database engine. + */ reservedKeywords: string[] = [ 'ADD', 'ALL', @@ -175,6 +209,44 @@ export abstract class AbstractDialect implements Dialect { 'WHERE', ]; + /** + * Formats how the schema and table name should be used in the SELECT statement. + * + * @why When implementing support for BigQuery, the SELECT statement takes only a table name, where the FROM + * statement takes the schema and table name. It also requires both the schema and name to be wrapped in + * backticks together, and not separately. This method allows for formatting the schema and table name in a way + * that is compatible with the specific database dialect. + * See also - `formatFromSchemaAndTable` + * @param schema The schema name (optional). + * @param table The table name. + * @returns The formatted schema and table combination. + */ + formatSchemaAndTable(schema: string | undefined, table: string): string { + if (schema) { + return `"${schema}".${table}`; + } + return table; + } + + /** + * Formats how the schema and table name should be used in the FROM statement. + * + * @why When implementing support for BigQuery, the FROM statement takes a fully qualified schema and table name, + * where the SELECT statement only takes the table name. It also requires both the schema and name to be wrapped + * in backticks together, and not separately. This method allows for formatting the schema and table name in a way + * that is compatible with the specific database dialect. + * See also - `formatSchemaAndTable` + * @param schema + * @param table + * @returns The formatted schema and table combination. + */ + formatFromSchemaAndTable(schema: string | undefined, table: string): string { + if (schema) { + return `"${schema}".${table}`; + } + return table; + } + select(builder: QueryBuilder, type: QueryType, query: Query): Query { let selectColumns = '' let fromTable = '' @@ -185,11 +257,8 @@ export abstract class AbstractDialect implements Dialect { selectColumns += ', ' } - const schema = set.schema ? `"${set.schema}".` : '' - let useTable = this.reservedKeywords.includes(set.table) - ? `"${set.table}"` - : set.table - + const formattedTable = this.formatSchemaAndTable(set.schema, set.table); + const formattedFromTable = this.formatFromSchemaAndTable(set.schema, set.table); const columns = set.columns.map((column) => { let useColumn = column @@ -197,17 +266,17 @@ export abstract class AbstractDialect implements Dialect { useColumn = `"${column}"` } - return `${schema ?? ''}${useTable}.${useColumn}` + return `${formattedTable}.${useColumn}` }) selectColumns += columns.join(', ') if (index === 0) { - fromTable = `${schema}${useTable}` + fromTable = formattedFromTable } }) - query.query = `SELECT ${selectColumns} FROM ${fromTable}` + query.query = `SELECT ${selectColumns} ${builder.selectRawValue ? builder.selectRawValue : ''} FROM ${fromTable}` if (joinClauses) { query.query += ` ${joinClauses}` @@ -242,7 +311,8 @@ export abstract class AbstractDialect implements Dialect { ? columns.map((column) => `:${column}`).join(', ') : columns.map(() => '?').join(', ') - query.query = `INSERT INTO ${builder.schema ? `"${builder.schema}".` : ''}${builder.table || ''} (${columns.join( + const formattedTable = this.formatSchemaAndTable(builder.schema, builder.table || ''); + query.query = `INSERT INTO ${formattedTable} (${columns.join( ', ' )}) VALUES (${placeholders})` @@ -260,6 +330,7 @@ export abstract class AbstractDialect implements Dialect { return query } + const formattedTable = this.formatSchemaAndTable(builder.schema, builder.table || ''); const columnsToUpdate = Object.keys(builder.data || {}) const setClauses = type === QueryType.named @@ -270,7 +341,7 @@ export abstract class AbstractDialect implements Dialect { .map((column) => `${column} = ?`) .join(', ') - query.query = `UPDATE ${builder.schema ? `"${builder.schema}".` : ''}${builder.table || ''} SET ${setClauses}` + query.query = `UPDATE ${formattedTable} SET ${setClauses}` if (builder.whereClauses?.length > 0) { query.query += ` WHERE ${builder.whereClauses.join(' AND ')}` } @@ -292,7 +363,8 @@ export abstract class AbstractDialect implements Dialect { return query } - query.query = `DELETE FROM ${builder.schema ? `"${builder.schema}".` : ''}${builder.table || ''}` + const formattedTable = this.formatFromSchemaAndTable(builder.schema, builder.table || ''); + query.query = `DELETE FROM ${formattedTable}` if (builder.whereClauses?.length > 0) { query.query += ` WHERE ${builder.whereClauses.join(' AND ')}` } @@ -305,6 +377,7 @@ export abstract class AbstractDialect implements Dialect { // return query // } + const formattedTable = this.formatSchemaAndTable(builder.schema, builder.table || ''); const columns = builder?.columns?.map((column) => { const dataType = this.mapDataType(column.type) @@ -319,7 +392,7 @@ export abstract class AbstractDialect implements Dialect { query.query = ` CREATE TABLE IF NOT EXISTS - ${builder.schema ? `"${builder.schema}".` : ''}${builder.table} + ${formattedTable} (${columns.join(', ')}) `