From 0874660fa6407a4ce9ceb74e9e9841ba74691851 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sun, 13 Oct 2024 17:51:51 +0700 Subject: [PATCH] restructure the connection --- src/connections/bigquery.ts | 6 - src/connections/cloudflare.ts | 277 ------------------ src/connections/index.ts | 14 +- .../{neon-http.ts => neon-http.bk.txt} | 0 .../{outerbase.ts => outerbase.bk.txt} | 0 src/connections/sqlite/base.ts | 137 +++++++++ src/connections/sqlite/cloudflare.ts | 149 ++++++++++ src/connections/sqlite/starbase.ts | 112 +++++++ src/connections/{ => sqlite}/turso.ts | 8 +- src/connections/starbase.ts | 244 --------------- src/generators/generate-models.ts | 184 ++++++------ src/index.ts | 10 +- tests/connections/create-test-connection.ts | 2 - 13 files changed, 507 insertions(+), 636 deletions(-) delete mode 100644 src/connections/cloudflare.ts rename src/connections/{neon-http.ts => neon-http.bk.txt} (100%) rename src/connections/{outerbase.ts => outerbase.bk.txt} (100%) create mode 100644 src/connections/sqlite/base.ts create mode 100644 src/connections/sqlite/cloudflare.ts create mode 100644 src/connections/sqlite/starbase.ts rename src/connections/{ => sqlite}/turso.ts (86%) delete mode 100644 src/connections/starbase.ts diff --git a/src/connections/bigquery.ts b/src/connections/bigquery.ts index 3a66f14..0001577 100644 --- a/src/connections/bigquery.ts +++ b/src/connections/bigquery.ts @@ -3,14 +3,8 @@ import { Query, constructRawQuery } from '../query'; import { Connection } from './index'; import { Database, Table, TableColumn } from '../models/database'; import { BigQueryDialect } from '../query-builder/dialects/bigquery'; - import { BigQuery } from '@google-cloud/bigquery'; -type BigQueryParameters = { - keyFileName: string; - region: string; -}; - export class BigQueryConnection implements Connection { bigQuery: BigQuery; diff --git a/src/connections/cloudflare.ts b/src/connections/cloudflare.ts deleted file mode 100644 index e04d1da..0000000 --- a/src/connections/cloudflare.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { QueryType } from '../query-params' -import { Query, constructRawQuery } from '../query' -import { Connection, OperationResponse } from './index' -import { - Constraint, - ConstraintColumn, - Database, - Table, - TableColumn, - TableIndex, - TableIndexType, -} from '../models/database' -import { DefaultDialect } from '../query-builder/dialects/default' - -interface SuccessResponse { - result: Record[] // Array of objects representing results - success: true - meta: { - served_by: string - duration: number - changes: number - last_row_id: number - changed_db: boolean - size_after: number - rows_read: number - rows_written: number - } -} -interface ErrorResponse { - result: [] // Empty result on error - success: false - errors: { code: number; message: string }[] // Array of errors - messages: any[] // You can adjust this type based on your use case -} - -type APIResponse = SuccessResponse | ErrorResponse - -export type CloudflareD1ConnectionDetails = { - apiKey: string - accountId: string - databaseId: string -} - -export class CloudflareD1Connection implements Connection { - // The Cloudflare API key with D1 access - apiKey: string | undefined - accountId: string | undefined - databaseId: string | undefined - - // Default query type to positional for Cloudflare - queryType = QueryType.positional - - // Default dialect for Cloudflare - dialect = new DefaultDialect() - - /** - * Creates a new CloudflareD1Connection object with the provided API key, - * account ID, and database ID. - * - * @param apiKey - The API key to be used for authentication. - * @param accountId - The account ID to be used for authentication. - * @param databaseId - The database ID to be used for querying. - */ - constructor(private _: CloudflareD1ConnectionDetails) { - this.apiKey = _.apiKey - this.accountId = _.accountId - this.databaseId = _.databaseId - } - - /** - * Performs a connect action on the current Connection object. - * In this particular use case Cloudflare is a REST API and - * requires an API key for authentication. - * - * @param details - Unused in the Cloudflare scenario. - * @returns Promise - */ - async connect(): Promise { - return Promise.resolve() - } - - /** - * Performs a disconnect action on the current Connection object. - * In this particular use case Cloudflare is a REST API and does - * not require a disconnect action. - * - * @returns Promise - */ - async disconnect(): Promise { - return Promise.resolve() - } - - /** - * Triggers a query action on the current Connection object. The query - * is a SQL query that will be executed on a D1 database in the Cloudflare - * account. The query is sent to the Cloudflare API and the response - * is returned. - * - * The parameters object is sent along with the query to be used in the - * query. By default if the query has parameters the SQL statement will - * produce a string with `:property` values that the parameters object - * keys should map to, and will be replaced by. - * - * @param query - The SQL query to be executed. - * @param parameters - An object containing the parameters to be used in the query. - * @returns Promise<{ data: any, error: Error | null }> - */ - async query( - query: Query - ): Promise<{ data: any; error: Error | null; query: string }> { - if (!this.apiKey) throw new Error('Cloudflare API key is not set') - if (!this.accountId) throw new Error('Cloudflare account ID is not set') - if (!this.databaseId) - throw new Error('Cloudflare database ID is not set') - if (!query) throw new Error('A SQL query was not provided') - - const response = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${this.accountId}/d1/database/${this.databaseId}/query`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.apiKey}`, - }, - body: JSON.stringify({ - sql: query.query, - params: query.parameters, - }), - } - ) - - const json: APIResponse = await response.json() - - if (json.success) { - const items = json.result[0].results - const rawSQL = constructRawQuery(query) - - return { - data: items, - error: null, - query: rawSQL, - // There's additional metadata here we could pass in the future - // meta: json.meta, - } - } - - const error = json.errors.map((error) => error.message).join(', ') - const rawSQL = constructRawQuery(query) - - return { - data: [], - error: new Error(error), - query: rawSQL, - } - } - - public async fetchDatabaseSchema(): Promise { - const exclude_tables = ['_cf_kv', 'sqlite_schema', 'sqlite_temp_schema'] - - const schemaMap: Record> = {} - - const { data } = await this.query({ - query: `PRAGMA table_list`, - }) - - const allTables = ( - data as { - schema: string - name: string - type: string - }[] - ).filter( - (row) => - !row.name.startsWith('_lite') && - !row.name.startsWith('sqlite_') && - !exclude_tables.includes(row.name?.toLowerCase()) - ) - - for (const table of allTables) { - if (exclude_tables.includes(table.name?.toLowerCase())) continue - - const { data: pragmaData } = await this.query({ - query: `PRAGMA table_info('${table.name}')`, - }) - - const tableData = pragmaData as { - cid: number - name: string - type: string - notnull: 0 | 1 - dflt_value: string | null - pk: 0 | 1 - }[] - - const { data: fkConstraintResponse } = await this.query({ - query: `PRAGMA foreign_key_list('${table.name}')`, - }) - - const fkConstraintData = ( - fkConstraintResponse as { - id: number - seq: number - table: string - from: string - to: string - on_update: 'NO ACTION' | unknown - on_delete: 'NO ACTION' | unknown - match: 'NONE' | unknown - }[] - ).filter( - (row) => - !row.table.startsWith('_lite') && - !row.table.startsWith('sqlite_') - ) - - const constraints: Constraint[] = [] - - if (fkConstraintData.length > 0) { - const fkConstraints: Constraint = { - name: 'FOREIGN KEY', - schema: table.schema, - tableName: table.name, - type: 'FOREIGN KEY', - columns: [], - } - - fkConstraintData.forEach((fkConstraint) => { - const currentConstraint: ConstraintColumn = { - columnName: fkConstraint.from, - } - fkConstraints.columns.push(currentConstraint) - }) - constraints.push(fkConstraints) - } - - const indexes: TableIndex[] = [] - const columns = tableData.map((column) => { - // Primary keys are ALWAYS considered indexes - if (column.pk === 1) { - indexes.push({ - name: column.name, - type: TableIndexType.PRIMARY, - columns: [column.name], - }) - } - - const currentColumn: TableColumn = { - name: column.name, - type: column.type, - position: column.cid, - nullable: column.notnull === 0, - default: column.dflt_value, - primary: column.pk === 1, - unique: column.pk === 1, - references: [], - } - - return currentColumn - }) - - const currentTable: Table = { - name: table.name, - columns: columns, - indexes: indexes, - constraints: constraints, - } - - if (!schemaMap[table.schema]) { - schemaMap[table.schema] = {} - } - - schemaMap[table.schema][table.name] = currentTable - } - - return schemaMap - } -} diff --git a/src/connections/index.ts b/src/connections/index.ts index 37e8970..9f982bb 100644 --- a/src/connections/index.ts +++ b/src/connections/index.ts @@ -15,18 +15,18 @@ export interface QueryResult> { query: string; } -export interface Connection { - dialect: AbstractDialect; +export abstract class Connection { + abstract dialect: AbstractDialect; // Handles logic for securely connecting and properly disposing of the connection. - connect: () => Promise; - disconnect: () => Promise; + abstract connect(): Promise; + abstract disconnect(): Promise; // Raw query execution method that can be used to execute any query. - query: >( + abstract query>( query: Query - ) => Promise>; + ): Promise>; // Retrieve metadata about the database, useful for introspection. - fetchDatabaseSchema?: () => Promise; + abstract fetchDatabaseSchema(): Promise; } diff --git a/src/connections/neon-http.ts b/src/connections/neon-http.bk.txt similarity index 100% rename from src/connections/neon-http.ts rename to src/connections/neon-http.bk.txt diff --git a/src/connections/outerbase.ts b/src/connections/outerbase.bk.txt similarity index 100% rename from src/connections/outerbase.ts rename to src/connections/outerbase.bk.txt diff --git a/src/connections/sqlite/base.ts b/src/connections/sqlite/base.ts new file mode 100644 index 0000000..da788b9 --- /dev/null +++ b/src/connections/sqlite/base.ts @@ -0,0 +1,137 @@ +import { + Constraint, + ConstraintColumn, + TableIndex, + TableIndexType, + TableColumn, + Database, + Table, +} from 'src/models/database'; +import { Connection } from '..'; + +export abstract class SqliteBaseConnection extends Connection { + public async fetchDatabaseSchema(): Promise { + const exclude_tables = [ + '_cf_kv', + 'sqlite_schema', + 'sqlite_temp_schema', + ]; + + const schemaMap: Record> = {}; + + const { data } = await this.query({ + query: `PRAGMA table_list`, + }); + + const allTables = ( + data as { + schema: string; + name: string; + type: string; + }[] + ).filter( + (row) => + !row.name.startsWith('_lite') && + !row.name.startsWith('sqlite_') && + !exclude_tables.includes(row.name?.toLowerCase()) + ); + + for (const table of allTables) { + if (exclude_tables.includes(table.name?.toLowerCase())) continue; + + const { data: pragmaData } = await this.query({ + query: `PRAGMA table_info('${table.name}')`, + }); + + const tableData = pragmaData as { + cid: number; + name: string; + type: string; + notnull: 0 | 1; + dflt_value: string | null; + pk: 0 | 1; + }[]; + + const { data: fkConstraintResponse } = await this.query({ + query: `PRAGMA foreign_key_list('${table.name}')`, + }); + + const fkConstraintData = ( + fkConstraintResponse as { + id: number; + seq: number; + table: string; + from: string; + to: string; + on_update: 'NO ACTION' | unknown; + on_delete: 'NO ACTION' | unknown; + match: 'NONE' | unknown; + }[] + ).filter( + (row) => + !row.table.startsWith('_lite') && + !row.table.startsWith('sqlite_') + ); + + const constraints: Constraint[] = []; + + if (fkConstraintData.length > 0) { + const fkConstraints: Constraint = { + name: 'FOREIGN KEY', + schema: table.schema, + tableName: table.name, + type: 'FOREIGN KEY', + columns: [], + }; + + fkConstraintData.forEach((fkConstraint) => { + const currentConstraint: ConstraintColumn = { + columnName: fkConstraint.from, + }; + fkConstraints.columns.push(currentConstraint); + }); + constraints.push(fkConstraints); + } + + const indexes: TableIndex[] = []; + const columns = tableData.map((column) => { + // Primary keys are ALWAYS considered indexes + if (column.pk === 1) { + indexes.push({ + name: column.name, + type: TableIndexType.PRIMARY, + columns: [column.name], + }); + } + + const currentColumn: TableColumn = { + name: column.name, + type: column.type, + position: column.cid, + nullable: column.notnull === 0, + default: column.dflt_value, + primary: column.pk === 1, + unique: column.pk === 1, + references: [], + }; + + return currentColumn; + }); + + const currentTable: Table = { + name: table.name, + columns: columns, + indexes: indexes, + constraints: constraints, + }; + + if (!schemaMap[table.schema]) { + schemaMap[table.schema] = {}; + } + + schemaMap[table.schema][table.name] = currentTable; + } + + return schemaMap; + } +} diff --git a/src/connections/sqlite/cloudflare.ts b/src/connections/sqlite/cloudflare.ts new file mode 100644 index 0000000..05a4c51 --- /dev/null +++ b/src/connections/sqlite/cloudflare.ts @@ -0,0 +1,149 @@ +import { QueryType } from '../../query-params'; +import { Query, constructRawQuery } from '../../query'; +import { DefaultDialect } from '../../query-builder/dialects/default'; +import { SqliteBaseConnection } from './base'; + +interface SuccessResponse { + result: Record[]; // Array of objects representing results + success: true; + meta: { + served_by: string; + duration: number; + changes: number; + last_row_id: number; + changed_db: boolean; + size_after: number; + rows_read: number; + rows_written: number; + }; +} +interface ErrorResponse { + result: []; // Empty result on error + success: false; + errors: { code: number; message: string }[]; // Array of errors + messages: any[]; // You can adjust this type based on your use case +} + +type APIResponse = SuccessResponse | ErrorResponse; + +export type CloudflareD1ConnectionDetails = { + apiKey: string; + accountId: string; + databaseId: string; +}; + +export class CloudflareD1Connection extends SqliteBaseConnection { + // The Cloudflare API key with D1 access + apiKey: string | undefined; + accountId: string | undefined; + databaseId: string | undefined; + + // Default query type to positional for Cloudflare + queryType = QueryType.positional; + + // Default dialect for Cloudflare + dialect = new DefaultDialect(); + + /** + * Creates a new CloudflareD1Connection object with the provided API key, + * account ID, and database ID. + * + * @param apiKey - The API key to be used for authentication. + * @param accountId - The account ID to be used for authentication. + * @param databaseId - The database ID to be used for querying. + */ + constructor(private _: CloudflareD1ConnectionDetails) { + super(); + this.apiKey = _.apiKey; + this.accountId = _.accountId; + this.databaseId = _.databaseId; + } + + /** + * Performs a connect action on the current Connection object. + * In this particular use case Cloudflare is a REST API and + * requires an API key for authentication. + * + * @param details - Unused in the Cloudflare scenario. + * @returns Promise + */ + async connect(): Promise { + return Promise.resolve(); + } + + /** + * Performs a disconnect action on the current Connection object. + * In this particular use case Cloudflare is a REST API and does + * not require a disconnect action. + * + * @returns Promise + */ + async disconnect(): Promise { + return Promise.resolve(); + } + + /** + * Triggers a query action on the current Connection object. The query + * is a SQL query that will be executed on a D1 database in the Cloudflare + * account. The query is sent to the Cloudflare API and the response + * is returned. + * + * The parameters object is sent along with the query to be used in the + * query. By default if the query has parameters the SQL statement will + * produce a string with `:property` values that the parameters object + * keys should map to, and will be replaced by. + * + * @param query - The SQL query to be executed. + * @param parameters - An object containing the parameters to be used in the query. + * @returns Promise<{ data: any, error: Error | null }> + */ + async query( + query: Query + ): Promise<{ data: any; error: Error | null; query: string }> { + if (!this.apiKey) throw new Error('Cloudflare API key is not set'); + if (!this.accountId) + throw new Error('Cloudflare account ID is not set'); + if (!this.databaseId) + throw new Error('Cloudflare database ID is not set'); + if (!query) throw new Error('A SQL query was not provided'); + + const response = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${this.accountId}/d1/database/${this.databaseId}/query`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + sql: query.query, + params: query.parameters, + }), + } + ); + + const json: APIResponse = await response.json(); + + if (json.success) { + const items = json.result[0].results; + const rawSQL = constructRawQuery(query); + + return { + data: items, + error: null, + query: rawSQL, + // There's additional metadata here we could pass in the future + // meta: json.meta, + }; + } + + const error = json.errors.map((error) => error.message).join(', '); + const rawSQL = constructRawQuery(query); + + return { + data: [], + error: new Error(error), + query: rawSQL, + }; + } +} diff --git a/src/connections/sqlite/starbase.ts b/src/connections/sqlite/starbase.ts new file mode 100644 index 0000000..c0818d2 --- /dev/null +++ b/src/connections/sqlite/starbase.ts @@ -0,0 +1,112 @@ +import { QueryType } from '../../query-params'; +import { Query, constructRawQuery } from '../../query'; +import { DefaultDialect } from '../../query-builder/dialects/default'; +import { SqliteBaseConnection } from './base'; + +export type StarbaseConnectionDetails = { + url: string; + apiKey: string; +}; + +export class StarbaseConnection extends SqliteBaseConnection { + // The Starbase API key with + url: string | undefined; + apiKey: string | undefined; + + // Default query type to positional for Starbase + queryType = QueryType.positional; + + // Default dialect for Starbase + dialect = new DefaultDialect(); + + /** + * Creates a new StarbaseConnection object with the provided API key, + * account ID, and database ID. + * + * @param apiKey - The API key to be used for authentication. + * @param accountId - The account ID to be used for authentication. + * @param databaseId - The database ID to be used for querying. + */ + constructor(private _: StarbaseConnectionDetails) { + super(); + this.url = _.url; + this.apiKey = _.apiKey; + } + + /** + * Performs a connect action on the current Connection object. + * In this particular use case Starbase is a REST API and + * requires an API key for authentication. + * + * @param details - Unused in the Starbase scenario. + * @returns Promise + */ + async connect(): Promise { + return Promise.resolve(); + } + + /** + * Performs a disconnect action on the current Connection object. + * In this particular use case Starbase is a REST API and does + * not require a disconnect action. + * + * @returns Promise + */ + async disconnect(): Promise { + return Promise.resolve(); + } + + /** + * Triggers a query action on the current Connection object. The query + * is a SQL query that will be executed on a Starbase durable object + * database. The query is sent to the Starbase API and the response + * is returned. + * + * The parameters object is sent along with the query to be used in the + * query. By default if the query has parameters the SQL statement will + * produce a string with `:property` values that the parameters object + * keys should map to, and will be replaced by. + * + * @param query - The SQL query to be executed. + * @param parameters - An object containing the parameters to be used in the query. + * @returns Promise<{ data: any, error: Error | null }> + */ + async query( + query: Query + ): Promise<{ data: any; error: Error | null; query: string }> { + if (!this.url) throw new Error('Starbase URL is not set'); + if (!this.apiKey) throw new Error('Starbase API key is not set'); + if (!query) throw new Error('A SQL query was not provided'); + + const response = await fetch(this.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + sql: query.query, + params: query.parameters, + }), + }); + + const json = await response.json(); + const rawSQL = constructRawQuery(query); + + if (json.result) { + const items = json.result; + + return { + data: items, + error: null, + query: rawSQL, + }; + } + + return { + data: [], + error: Error('Unknown operation error'), + query: rawSQL, + }; + } +} diff --git a/src/connections/turso.ts b/src/connections/sqlite/turso.ts similarity index 86% rename from src/connections/turso.ts rename to src/connections/sqlite/turso.ts index ee98e2c..858ab81 100644 --- a/src/connections/turso.ts +++ b/src/connections/sqlite/turso.ts @@ -1,15 +1,17 @@ import { Client } from '@libsql/client'; import { AbstractDialect } from 'src/query-builder'; -import { Connection, QueryResult } from '.'; -import { Query } from '../query'; +import { QueryResult } from '..'; +import { Query } from '../../query'; import { PostgresDialect } from 'src/query-builder/dialects/postgres'; +import { SqliteBaseConnection } from './base'; -export class TursoConnection implements Connection { +export class TursoConnection extends SqliteBaseConnection { client: Client; dialect: AbstractDialect = new PostgresDialect(); constructor(client: Client) { + super(); this.client = client; } diff --git a/src/connections/starbase.ts b/src/connections/starbase.ts deleted file mode 100644 index 85c4149..0000000 --- a/src/connections/starbase.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { QueryType } from '../query-params' -import { Query, constructRawQuery } from '../query' -import { Connection, OperationResponse } from './index' -import { - Constraint, - ConstraintColumn, - Database, - Table, - TableColumn, - TableIndex, - TableIndexType, -} from '../models/database' -import { DefaultDialect } from '../query-builder/dialects/default' - -export type StarbaseConnectionDetails = { - url: string - apiKey: string -} - -export class StarbaseConnection implements Connection { - // The Starbase API key with - url: string | undefined - apiKey: string | undefined - - // Default query type to positional for Starbase - queryType = QueryType.positional - - // Default dialect for Starbase - dialect = new DefaultDialect() - - /** - * Creates a new StarbaseConnection object with the provided API key, - * account ID, and database ID. - * - * @param apiKey - The API key to be used for authentication. - * @param accountId - The account ID to be used for authentication. - * @param databaseId - The database ID to be used for querying. - */ - constructor(private _: StarbaseConnectionDetails) { - this.url = _.url - this.apiKey = _.apiKey - } - - /** - * Performs a connect action on the current Connection object. - * In this particular use case Starbase is a REST API and - * requires an API key for authentication. - * - * @param details - Unused in the Starbase scenario. - * @returns Promise - */ - async connect(): Promise { - return Promise.resolve() - } - - /** - * Performs a disconnect action on the current Connection object. - * In this particular use case Starbase is a REST API and does - * not require a disconnect action. - * - * @returns Promise - */ - async disconnect(): Promise { - return Promise.resolve() - } - - /** - * Triggers a query action on the current Connection object. The query - * is a SQL query that will be executed on a Starbase durable object - * database. The query is sent to the Starbase API and the response - * is returned. - * - * The parameters object is sent along with the query to be used in the - * query. By default if the query has parameters the SQL statement will - * produce a string with `:property` values that the parameters object - * keys should map to, and will be replaced by. - * - * @param query - The SQL query to be executed. - * @param parameters - An object containing the parameters to be used in the query. - * @returns Promise<{ data: any, error: Error | null }> - */ - async query( - query: Query - ): Promise<{ data: any; error: Error | null; query: string }> { - if (!this.url) throw new Error('Starbase URL is not set') - if (!this.apiKey) throw new Error('Starbase API key is not set') - if (!query) throw new Error('A SQL query was not provided') - - const response = await fetch( - this.url, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.apiKey}`, - }, - body: JSON.stringify({ - sql: query.query, - params: query.parameters, - }), - } - ) - - const json = await response.json() - const rawSQL = constructRawQuery(query) - - if (json.result) { - const items = json.result - - return { - data: items, - error: null, - query: rawSQL, - } - } - - return { - data: [], - error: Error('Unknown operation error'), - query: rawSQL, - } - } - - public async fetchDatabaseSchema(): Promise { - const exclude_tables = ['_cf_kv', 'sqlite_schema', 'sqlite_temp_schema'] - - const schemaMap: Record> = {} - - const { data } = await this.query({ - query: `PRAGMA table_list`, - }) - - const allTables = ( - data as { - schema: string - name: string - type: string - }[] - ).filter( - (row) => - !row.name.startsWith('_lite') && - !row.name.startsWith('sqlite_') && - !exclude_tables.includes(row.name?.toLowerCase()) - ) - - for (const table of allTables) { - if (exclude_tables.includes(table.name?.toLowerCase())) continue - - const { data: pragmaData } = await this.query({ - query: `PRAGMA table_info('${table.name}')`, - }) - - const tableData = pragmaData as { - cid: number - name: string - type: string - notnull: 0 | 1 - dflt_value: string | null - pk: 0 | 1 - }[] - - const { data: fkConstraintResponse } = await this.query({ - query: `PRAGMA foreign_key_list('${table.name}')`, - }) - - const fkConstraintData = ( - fkConstraintResponse as { - id: number - seq: number - table: string - from: string - to: string - on_update: 'NO ACTION' | unknown - on_delete: 'NO ACTION' | unknown - match: 'NONE' | unknown - }[] - ).filter( - (row) => - !row.table.startsWith('_lite') && - !row.table.startsWith('sqlite_') - ) - - const constraints: Constraint[] = [] - - if (fkConstraintData.length > 0) { - const fkConstraints: Constraint = { - name: 'FOREIGN KEY', - schema: table.schema, - tableName: table.name, - type: 'FOREIGN KEY', - columns: [], - } - - fkConstraintData.forEach((fkConstraint) => { - const currentConstraint: ConstraintColumn = { - columnName: fkConstraint.from, - } - fkConstraints.columns.push(currentConstraint) - }) - constraints.push(fkConstraints) - } - - const indexes: TableIndex[] = [] - const columns = tableData.map((column) => { - // Primary keys are ALWAYS considered indexes - if (column.pk === 1) { - indexes.push({ - name: column.name, - type: TableIndexType.PRIMARY, - columns: [column.name], - }) - } - - const currentColumn: TableColumn = { - name: column.name, - type: column.type, - position: column.cid, - nullable: column.notnull === 0, - default: column.dflt_value, - primary: column.pk === 1, - unique: column.pk === 1, - references: [], - } - - return currentColumn - }) - - const currentTable: Table = { - name: table.name, - columns: columns, - indexes: indexes, - constraints: constraints, - } - - if (!schemaMap[table.schema]) { - schemaMap[table.schema] = {} - } - - schemaMap[table.schema][table.name] = currentTable - } - - return schemaMap - } -} diff --git a/src/generators/generate-models.ts b/src/generators/generate-models.ts index 2d79a2e..14a71c4 100644 --- a/src/generators/generate-models.ts +++ b/src/generators/generate-models.ts @@ -1,53 +1,53 @@ #!/usr/bin/env node -import pkg from 'handlebars' -const { compile } = pkg -import { promises as fs } from 'fs' -import { API_URL } from '../connections/outerbase' +import pkg from 'handlebars'; +const { compile } = pkg; +import { promises as fs } from 'fs'; +import { API_URL } from '../connections/outerbase.backup'; -const path = require('path') -const handlebars = require('handlebars') +const path = require('path'); +const handlebars = require('handlebars'); handlebars.registerHelper('capitalize', function (str: string) { - return str?.charAt(0).toUpperCase() + str?.slice(1) -}) + return str?.charAt(0).toUpperCase() + str?.slice(1); +}); handlebars.registerHelper('camelCase', function (str: string) { - return str?.replace(/[-_](.)/g, (_, c) => c?.toUpperCase()) -}) + return str?.replace(/[-_](.)/g, (_, c) => c?.toUpperCase()); +}); -handlebars.registerHelper('neq', (a: any, b: any) => a !== b) +handlebars.registerHelper('neq', (a: any, b: any) => a !== b); function parseArgs(args: any[]): { API_KEY?: string; PATH?: string } { - const argsMap: Record = {} + const argsMap: Record = {}; args.slice(2).forEach((arg: { split: (arg0: string) => [any, any] }) => { - const [key, value] = arg.split('=') - argsMap[key] = value - }) + const [key, value] = arg.split('='); + argsMap[key] = value; + }); - return argsMap + return argsMap; } async function main() { - const args = parseArgs(process.argv) - const apiKey = args.API_KEY || '' - const folderPath = args.PATH || './' + const args = parseArgs(process.argv); + const apiKey = args.API_KEY || ''; + const folderPath = args.PATH || './'; try { - await fs.mkdir(folderPath, { recursive: true }) + await fs.mkdir(folderPath, { recursive: true }); // Load templates const modelTemplateSource = await fs.readFile( path.resolve(__dirname, 'model-template.handlebars'), 'utf-8' - ) + ); const indexTemplateSource = await fs.readFile( path.resolve(__dirname, 'index-template.handlebars'), 'utf-8' - ) + ); // Compile templates - const modelTemplate = compile(modelTemplateSource) - const indexTemplate = compile(indexTemplateSource) + const modelTemplate = compile(modelTemplateSource); + const indexTemplate = compile(indexTemplateSource); const response = await fetch(`${API_URL}/api/v1/ezql/schema`, { method: 'GET', @@ -55,23 +55,23 @@ async function main() { 'Content-Type': 'application/json', 'X-Source-Token': apiKey, }, - }) + }); - let json = await response.json() - let schemaResponse = json.response - let tables: Array = [] + let json = await response.json(); + let schemaResponse = json.response; + let tables: Array = []; - const deletedPathsMap: Record = {} + const deletedPathsMap: Record = {}; for (let key in schemaResponse) { - let isPublic = key.toLowerCase() === 'public' + let isPublic = key.toLowerCase() === 'public'; if (Array.isArray(schemaResponse[key])) { for (let table of schemaResponse[key]) { - if (table.type !== 'table') continue + if (table.type !== 'table') continue; // References will capture all columns that have foreign key constraints in this table - table.references = [] + table.references = []; // Loop through all columns in the table for (let column of table.columns) { @@ -80,21 +80,21 @@ async function main() { constraint.type?.toUpperCase() === 'PRIMARY KEY' && constraint.column === column.name - ) - column.primary = isPrimaryKey ? true : false + ); + column.primary = isPrimaryKey ? true : false; const isUnique = table.constraints?.find( (constraint: { type: string; column: any }) => constraint.type?.toUpperCase() === 'UNIQUE' && constraint.column === column.name - ) - column.unique = isUnique ? true : false + ); + column.unique = isUnique ? true : false; const foreignKey = table.constraints?.find( (constraint: { - type: string - column: any - columns: string | any[] + type: string; + column: any; + columns: string | any[]; }) => { if ( constraint.type?.toUpperCase() === @@ -102,27 +102,27 @@ async function main() { constraint.column === column.name && constraint.columns?.length > 0 ) { - const firstColumn = constraint.columns[0] + const firstColumn = constraint.columns[0]; const referenceExists = table.references.some( (ref: { - name: string - table: any - schema: any + name: string; + table: any; + schema: any; }) => ref.name === firstColumn.name && ref.table === firstColumn.table && ref.schema === firstColumn.schema - ) + ); if (!referenceExists) { table.references.push({ name: firstColumn.name, table: firstColumn.table, schema: firstColumn.schema, - }) + }); } } @@ -130,14 +130,14 @@ async function main() { constraint.type?.toUpperCase() === 'FOREIGN KEY' && constraint.column === column.name - ) + ); } - ) + ); column.reference = foreignKey?.columns[0]?.table ? foreignKey?.columns[0]?.table - : undefined + : undefined; - let currentType = column.type?.toLowerCase() + let currentType = column.type?.toLowerCase(); // Convert `currentType` from database column types to TypeScript types switch (column.type?.toLowerCase()) { @@ -150,15 +150,15 @@ async function main() { currentType = column.type?.toLowerCase() === 'bigint' ? 'bigint' - : 'number' - break + : 'number'; + break; case 'decimal': case 'numeric': case 'float': case 'double': case 'real': - currentType = 'number' - break + currentType = 'number'; + break; case 'varchar': case 'char': case 'character varying': @@ -166,92 +166,92 @@ async function main() { case 'tinytext': case 'mediumtext': case 'longtext': - currentType = 'string' - break + currentType = 'string'; + break; case 'timestamp': case 'datetime': case 'date': case 'time': - currentType = 'Date' - break + currentType = 'Date'; + break; case 'boolean': - currentType = 'boolean' - break + currentType = 'boolean'; + break; case 'json': case 'jsonb': - currentType = 'Record' - break + currentType = 'Record'; + break; case 'binary': case 'varbinary': case 'blob': case 'tinyblob': case 'mediumblob': case 'longblob': - currentType = 'Blob' - break + currentType = 'Blob'; + break; case 'enum': case 'set': - currentType = 'string' - break + currentType = 'string'; + break; case 'uuid': - currentType = 'string' - break + currentType = 'string'; + break; case 'bit': - currentType = 'number' - break + currentType = 'number'; + break; case 'array': - currentType = 'any[]' - break + currentType = 'any[]'; + break; case 'geometry': case 'geography': - currentType = 'GeoJSON.Geometry' - break + currentType = 'GeoJSON.Geometry'; + break; default: - currentType = 'any' - break + currentType = 'any'; + break; } - column.type = currentType + column.type = currentType; } const currentFolderPath = - folderPath + `${isPublic ? '' : '/' + key}` - const model = modelTemplate(table) + folderPath + `${isPublic ? '' : '/' + key}`; + const model = modelTemplate(table); const modelPath = path.resolve( currentFolderPath, `${table.name}.ts` - ) + ); // Remove the existing models directory and create a new one if it doesn't exist // but only if it hasn't been deleted already. if (!deletedPathsMap[currentFolderPath]) { - await fs.rmdir(currentFolderPath, { recursive: true }) - deletedPathsMap[currentFolderPath] = true + await fs.rmdir(currentFolderPath, { recursive: true }); + deletedPathsMap[currentFolderPath] = true; } - - await fs.mkdir(currentFolderPath, { recursive: true }) - await fs.writeFile(modelPath, model) + + await fs.mkdir(currentFolderPath, { recursive: true }); + await fs.writeFile(modelPath, model); tables.push({ name: isPublic ? table.name : `${key}/${table.name}`, - }) + }); } } } - console.log('Generated models for tables:', tables) + console.log('Generated models for tables:', tables); // Generate index file - const index = indexTemplate({ tables: tables }) - const indexPath = path.resolve(folderPath, 'index.ts') + const index = indexTemplate({ tables: tables }); + const indexPath = path.resolve(folderPath, 'index.ts'); // Write generated files - await fs.writeFile(indexPath, index) + await fs.writeFile(indexPath, index); - console.log('Models generated successfully') + console.log('Models generated successfully'); } catch (error) { - console.error('Error generating models:', error) + console.error('Error generating models:', error); } } -main() +main(); diff --git a/src/index.ts b/src/index.ts index 1b721ee..15546e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,14 @@ export * from './connections'; -export * from './connections/outerbase'; -export * from './connections/cloudflare'; -export * from './connections/neon-http'; +// export * from './connections/outerbase.backup'; +export * from './connections/sqlite/cloudflare'; +// export * from './connections/neon-http.bk'; export * from './connections/motherduck'; export * from './connections/bigquery'; -export * from './connections/starbase'; +export * from './connections/sqlite/starbase'; export * from './connections/mongodb'; export * from './connections/mysql'; export * from './connections/postgresql'; -export * from './connections/turso'; +export * from './connections/sqlite/turso'; export * from './client'; export * from './models'; export * from './models/decorators'; diff --git a/tests/connections/create-test-connection.ts b/tests/connections/create-test-connection.ts index ff84815..ffa1705 100644 --- a/tests/connections/create-test-connection.ts +++ b/tests/connections/create-test-connection.ts @@ -10,8 +10,6 @@ import { TursoConnection, } from '../../src'; -let DEFAULT_SCHEMA = ''; - export default function createTestClient(): { client: Connection; defaultSchema: string;